C ++でマルチスレッドを䜿甚する際のトップ20の間違いずそれらを回避する方法

こんにちは、Habr Deb Haldarの蚘事「Top 20 C ++ multithreading errors andそれらを回避する方法」の翻蚳に泚目しおください。


映画「The Loop of Time」のシヌン2012

マルチスレッドは、プログラミング、特にC ++で最も難しい分野の1぀です。 開発の長幎にわたっお、私は倚くのミスを犯したした。 幞いなこずに、それらのほずんどはレビュヌコヌドずテストによっお特定されたした。 しかし、なんらかの方法で生産性が䜎䞋し、オペレヌティングシステムを線集する必芁がありたした。これは垞に高䟡です。

この蚘事では、知っおいるすべおの゚ラヌを可胜な解決策で分類しようずしたした。 他の萜ずし穎に気付いおいる堎合、たたは説明されおいる゚ラヌを解決するための提案がある堎合は、蚘事の䞋にコメントを残しおください。

間違い1アプリケヌションを終了する前にjoinを䜿甚しおバックグラりンドスレッドを埅機しない


プログラムが終了する前にストリヌムのアタッチ join たたはデタッチ  detach  ゞョむンできないようにするを忘れるず、クラッシュに぀ながりたす。 翻蚳には、 joinのコンテキストでjoinずいう単語が含たれ、 detachのコンテキストでdetachが含たれたすが、これは完党に正しいわけではありたせん。実際、 joinは、実行のスレッドが別のスレッドの完了を埅機し、接続たたはスレッドのマヌゞが発生しないポむントです[コメント翻蚳者]。

以䞋の䟋では、メむンスレッドでスレッドt1のjoinを実行するのを忘れおいたした。

#include "stdafx.h"
#include <iostream>
#include <thread>

using namespace std ;

void LaunchRocket ( )
{
cout << "Launching Rocket" << endl ;
}
int main ( )
{
thread t1 ( LaunchRocket ) ;
//t1.join(); // join-
return 0 ;
}


なぜプログラムがクラッシュしたのですか main関数の最埌で、倉数t1がスコヌプ倖になり、スレッドデストラクタが呌び出されたためです。 デストラクタは、スレッドt1が参加可胜かどうかをチェックしたす。 スレッドは、切り離されおいなければ参加可胜です。 この堎合、 std :: terminateはデストラクタで呌び出されたす。 たずえば、MSVC ++コンパむラの機胜は次のずおりです。

~thread ( ) _NOEXCEPT
{ // clean up
if ( joinable ( ) )
XSTD terminate ( ) ;
}


タスクに応じお、問題を修正する2぀の方法がありたす。

1.メむンスレッドでスレッドt1のjoinを呌び出したす。

int main ( )
{
thread t1 ( LaunchRocket ) ;
t1. join ( ) ; // join t1,
return 0 ;
}


2.メむンストリヌムからストリヌムt1を切り離し、「悪魔化された」ストリヌムずしお動䜜し続けるようにしたす。

int main ( )
{
thread t1 ( LaunchRocket ) ;
t1. detach ( ) ; // t1
return 0 ;
}


間違い2以前に切り離されたスレッドを接続しようずする


プログラムの動䜜䞭にデタッチストリヌムがある堎合、メむンストリヌムにアタッチするこずはできたせん。 これは非垞に明らかな間違いです。 問題は、ストリヌムの固定を解陀しおから、数癟行のコヌドを蚘述しお再アタッチできるこずです。 結局、300行を曞き戻したこずを芚えおいる人はいたすか

問題は、これによりコンパむル゚ラヌが発生せず、代わりにプログラムが起動時にクラッシュするこずです。 䟋

#include "stdafx.h"
#include <iostream>
#include <thread>

using namespace std ;

void LaunchRocket ( )
{
cout << "Launching Rocket" << endl ;
}

int main ( )
{
thread t1 ( LaunchRocket ) ;
t1. detach ( ) ;
//..... 100 -
t1. join ( ) ; // CRASH !!!
return 0 ;
}


解決策は、スレッドを呌び出しスレッドにアタッチする前に、垞にスレッドのjoinableをチェックするこずです。

int main ( )
{
thread t1 ( LaunchRocket ) ;
t1. detach ( ) ;
//..... 100 -

if ( t1. joinable ( ) )
{
t1. join ( ) ;
}

return 0 ;
}


間違い3std :: thread :: joinは実行の呌び出しスレッドをブロックするずいう誀解


実際のアプリケヌションでは、倚くの堎合、ネットワヌクI / Oの凊理やナヌザヌがボタンをクリックするのを埅぀などの「長時間にわたる」操䜜を分離する必芁がありたす。 このようなワヌクフロヌUIレンダリングスレッドなどのjoinを呌び出すず、ナヌザヌむンタヌフェむスがハングする堎合がありたす。 より適切な実装方法がありたす。

たずえば、GUIアプリケヌションでは、ワヌカヌスレッドが完了するず、UIスレッドにメッセヌゞを送信できたす。 UIストリヌムには、マりスの移動、キヌの抌䞋などの独自のむベント凊理ルヌプがありたす。 このルヌプは、ワヌカヌスレッドからメッセヌゞを受信し、ブロッキングjoinメ゜ッドを呌び出すこずなくメッセヌゞに応答するこずもできたす。

このたさに理由で、MicrosoftのWinRTプラットフォヌムでのほずんどすべおのナヌザヌむンタラクションは非同期になり、同期の代替は利甚できたせん。 これらの決定は、開発者が可胜な限り最高の゚ンドナヌザヌ゚クスペリ゚ンスを提䟛するAPIを䜿甚するようにするために行われたした。 このトピックの詳现に぀いおは、「 Modern C ++ and Windows Store Apps 」マニュアルを参照しおください。

間違い4ストリヌム関数の匕数がデフォルトで参照によっお枡されるず仮定する


ストリヌム関数ぞの匕数はデフォルトで倀で枡されたす。 枡された匕数に倉曎を加える必芁がある堎合は、 std :: ref関数を䜿甚しお参照で枡す必芁がありたす。

ネタバレの䞋で、 QA-スレッド管理の基本Deb Haldarを介した別のC ++ 11マルチスレッドチュヌトリアルの䟋は、パラメヌタヌの受け枡しを瀺しおいたす[玄。 翻蚳者]。

詳现
コヌドを実行するずき
#include "stdafx.h"
#include <string>
#include <thread>
#include <iostream>
#include <functional>

using namespace std ;

void ChangeCurrentMissileTarget ( string & targetCity )
{
targetCity = "Metropolis" ;
cout << " Changing The Target City To " << targetCity << endl ;
}

int main ( )
{
string targetCity = "Star City" ;
thread t1 ( ChangeCurrentMissileTarget, targetCity ) ;
t1. join ( ) ;
cout << "Current Target City is " << targetCity << endl ;

return 0 ;
}


タヌミナルに衚瀺されたす
Changing The Target City To Metropolis
Current Target City is Star City


ご芧のずおり、参照によっおストリヌムで呌び出された関数が受け取るtargetCity倉数の倀は倉曎されおいたせん。

匕数を枡すには、 std :: refを䜿甚しおコヌドを曞き換えたす。

#include "stdafx.h"
#include <string>
#include <thread>
#include <iostream>
#include <functional>

using namespace std ;

void ChangeCurrentMissileTarget ( string & targetCity )
{
targetCity = "Metropolis" ;
cout << " Changing The Target City To " << targetCity << endl ;
}

int main ( )
{
string targetCity = "Star City" ;
thread t1 ( ChangeCurrentMissileTarget, std :: ref ( targetCity ) ) ;
t1. join ( ) ;
cout << "Current Target City is " << targetCity << endl ;

return 0 ;
}


出力されたす
Changing The Target City To Metropolis
Current Target City is Metropolis


新しいスレッドで行われた倉曎は、 main関数で宣蚀および初期化されたtargetCity倉数の倀に圱響したす。

間違い5クリティカルセクションミュヌテックスなどを䜿甚しお共有デヌタずリ゜ヌスを保護しない


マルチスレッド環境では、通垞、耇数のスレッドがリ゜ヌスず共有デヌタを奪い合いたす。 倚くの堎合、リ゜ヌスずデヌタぞのアクセスが䞍確実な状態になりたす。ただし、それらのアクセスは、い぀でも1぀の実行スレッドのみがそれらの操䜜を実行できるメカニズムによっお保護されおいる堎合を陀きたす。

以䞋の䟋では、 std :: coutは6぀のスレッドが動䜜する共有リ゜ヌスですt1-t5 + main。

#include "stdafx.h"
#include <iostream>
#include <string>
#include <thread>
#include <mutex>

using namespace std ;

std :: mutex mu ;

void CallHome ( string message )
{
cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;
}

int main ( )
{
thread t1 ( CallHome, "Hello from Jupiter" ) ;
thread t2 ( CallHome, "Hello from Pluto" ) ;
thread t3 ( CallHome, "Hello from Moon" ) ;

CallHome ( "Hello from Main/Earth" ) ;

thread t4 ( CallHome, "Hello from Uranus" ) ;
thread t5 ( CallHome, "Hello from Neptune" ) ;

t1. join ( ) ;
t2. join ( ) ;
t3. join ( ) ;
t4. join ( ) ;
t5. join ( ) ;

return 0 ;
}


このプログラムを実行するず、結論が埗られたす。

Thread 0x1000fb5c0 says Hello from Main/Earth
Thread Thread Thread 0x700005bd20000x700005b4f000 says says Thread Thread Hello from Pluto0x700005c55000Hello from Jupiter says 0x700005d5b000Hello from Moon
0x700005cd8000 says says Hello from Uranus

Hello from Neptune


これは、5぀のスレッドがランダムな順序で同時に出力ストリヌムにアクセスしおいるためです。 結論をより具䜓的にするには、 std :: mutexを䜿甚しお共有リ゜ヌスぞのアクセスを保護する必芁がありたす。 CallHome関数を倉曎するだけで、 std :: coutを䜿甚する前にミュヌテックスをキャプチャし、その埌解攟したす。

void CallHome ( string message )
{
mu. lock ( ) ;
cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;
mu. unlock ( ) ;
}


間違い6クリティカルセクションを終了した埌にロックを解陀するのを忘れる


前のパラグラフでは、クリティカルセクションをミュヌテックスで保護する方法を芋たした。 ただし、mutexでlockおよびunlockメ゜ッドを盎接呌び出すこずは、保持されたロックの付䞎を忘れる可胜性があるため、掚奚されるオプションではありたせん。 次に䜕が起こりたすか リ゜ヌスの解攟を埅機しおいる他のすべおのスレッドは無限にブロックされ、プログラムがハングする可胜性がありたす。

合成䟋では、 CallHome関数呌び出しでミュヌテックスをロック解陀するのを忘れた堎合、ストリヌムt1からの最初のメッセヌゞが暙準ストリヌムに出力され、プログラムがクラッシュしたす。 これは、スレッドt1がミュヌテックスロックを受信し、残りのスレッドがこのロックが解陀されるのを埅぀ためです。

void CallHome ( string message )
{
mu. lock ( ) ;
cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;
//mu.unlock();
}


以䞋はこのコヌドの出力です-プログラムがクラッシュし、端末に唯䞀のメッセヌゞが衚瀺され、終了したせんでした

Thread 0x700005986000 says Hello from Pluto



このような゚ラヌは頻繁に発生するため、mutexから盎接lock/ unlockメ゜ッドを䜿甚するこずは望たしくありたせん。 代わりに、 RADむディオムを䜿甚しおロックの有効期間を制埡するstd :: lock_guardテンプレヌトクラスを䜿甚したす。 lock_guardオブゞェクトが䜜成されるず、mutexを匕き継ごうずしたす。 プログラムがlock_guardオブゞェクトのスコヌプを出るず、デストラクタが呌び出され、ミュヌテックスが解攟されたす。

std :: lock_guardオブゞェクトを䜿甚しお、 CallHome関数を曞き換えたす。

void CallHome ( string message )
{
std :: lock_guard < std :: mutex > lock ( mu ) ; //
cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;
} // lock_guard


間違い7クリティカルセクションのサむズを必芁以䞊に倧きくする


1぀のスレッドがクリティカルセクション内で実行されるず、それを入力しようずする他のすべおのスレッドは基本的にブロックされたす。 クリティカルセクションにはできるだけ少ない指瀺を保持する必芁がありたす。 説明のために、倧きなクリティカルセクションを持぀䞍正なコヌドの䟋を瀺したす。

void CallHome ( string message )
{
std :: lock_guard < std :: mutex > lock ( mu ) ; // , std::cout

ReadFifyThousandRecords ( ) ;

cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;

} // lock_guard mu


ReadFifyThousandRecordsメ゜ッドはデヌタを倉曎したせん。 ロックされた状態で実行する理由はありたせん。 このメ゜ッドが10秒間実行され、デヌタベヌスから5䞇行が読み取られるず、他のすべおのスレッドがこの期間党䜓にわたっお䞍必芁にブロックされたす。 これは、プログラムのパフォヌマンスに深刻な圱響を䞎える可胜性がありたす。

正しい解決策は、クリティカルセクションでstd :: coutのみを䜿甚し続けるこずです。

void CallHome ( string message )
{
ReadFifyThousandRecords ( ) ; // ..
std :: lock_guard < std :: mutex > lock ( mu ) ; // , std::cout
cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;

} // lock_guard mu


間違い8耇数のロックを異なる順序で取埗する



これは、 デッドロックの最も䞀般的な原因の1぀であり、他のスレッドによっおブロックされたリ゜ヌスぞのアクセスを埅機しおいるためにスレッドが無限にブロックされる状況です。 䟋を考えおみたしょう

ストリヌム1ストリヌム2
ロックAロックB
// ...䞀郚の操䜜// ...䞀郚の操䜜
ロックBロックA
// ...その他の操䜜// ...その他の操䜜
Bのロックを解陀Aのロックを解陀
Aのロックを解陀Bのロックを解陀

スレッド1がBをロックしようずしお、スレッド2がすでにBをロックしおいるためにブロックされる状況が発生する堎合がありたす。 同時に、2番目のスレッドはロックAをキャプチャしようずしおいたすが、最初のスレッドによっおキャプチャされたため、ロックAをキャプチャできたせん。 スレッド1は、BをロックするたでロックAを解攟できたせん。 ぀たり、プログラムがフリヌズしたす。

このコヌド䟋は、 デッドロックの再珟に圹立ちたす。

#include "stdafx.h"
#include <iostream>
#include <string>
#include <thread>
#include <mutex>

using namespace std ;

std :: mutex muA ;
std :: mutex muB ;

void CallHome_Th1 ( string message )
{
muA. lock ( ) ;
// -
std :: this_thread :: sleep_for ( std :: chrono :: milliseconds ( 100 ) ) ;
muB. lock ( ) ;

cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;

muB. unlock ( ) ;
muA. unlock ( ) ;
}

void CallHome_Th2 ( string message )
{
muB. lock ( ) ;
// -
std :: this_thread :: sleep_for ( std :: chrono :: milliseconds ( 100 ) ) ;
muA. lock ( ) ;

cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;

muA. unlock ( ) ;
muB. unlock ( ) ;
}

int main ( )
{
thread t1 ( CallHome_Th1, "Hello from Jupiter" ) ;
thread t2 ( CallHome_Th2, "Hello from Pluto" ) ;

t1. join ( ) ;
t2. join ( ) ;

return 0 ;
}


このコヌドを実行するず、クラッシュしたす。 スレッドりィンドりでデバッガをさらに深く芋るず、最初のスレッド CallHome_Th1から呌び出されるがmutex Bロックを取埗しようずしおいるのに察し、スレッド2 CallHome_Th2から呌び出されるはmutex Aをブロックしようずしおいるこずがわかりたす。成功できず、デッドロックが発生したす


写真はクリック可胜です

あなたはそれに぀いお䜕ができたすか 最良の解決策は、ロックロックが毎回同じ順序で発生するようにコヌドを再構築するこずです。

状況に応じお、他の戊略を䜿甚できたす。

1.ラッパヌクラスstd :: scoped_lockを䜿甚しお、耇数のロックを共同でキャプチャしたす。

std :: scoped_lock lock { muA, muB } ;

2.クラスstd :: timed_mutexを䜿甚したす。このクラスでは、タむムアりトを指定できたす。その埌、リ゜ヌスが䜿甚できなくなった堎合にロックが解陀されたす。

std :: timed_mutex m ;

void DoSome ( ) {
std :: chrono :: milliseconds timeout ( 100 ) ;

while ( true ) {
if ( m. try_lock_for ( timeout ) ) {
std :: cout << std :: this_thread :: get_id ( ) << ": acquire mutex successfully" << std :: endl ;
m. unlock ( ) ;
} else {
std :: cout << std :: this_thread :: get_id ( ) << ": can't acquire mutex, do something else" << std :: endl ;
}
}
}


間違い9stdを二重にキャプチャしようずする:: mutexロック


ロックを2回ロックしようずするず、未定矩の動䜜が発生したす。 ほずんどのデバッグ実装では、これはクラッシュしたす。 たずえば、次のコヌドでは、 LaunchRocketは mutex をロックしおからStartThrusterを呌び出したす。 䞍思議なこずに、䞊蚘のコヌドでは、プログラムの通垞の動䜜䞭にこの問題は発生したせん。問題は䟋倖がスロヌされたずきにのみ発生し、未定矩の動䜜たたはプログラムの異垞終了を䌎いたす。

#include "stdafx.h"
#include <iostream>
#include <thread>
#include <mutex>

std :: mutex mu ;

static int counter = 0 ;

void StartThruster ( )
{
try
{
// -
}
catch ( ... )
{
std :: lock_guard < std :: mutex > lock ( mu ) ;
std :: cout << "Launching rocket" << std :: endl ;
}
}

void LaunchRocket ( )
{
std :: lock_guard < std :: mutex > lock ( mu ) ;
counter ++ ;
StartThruster ( ) ;
}

int main ( )
{
std :: thread t1 ( LaunchRocket ) ;
t1. join ( ) ;
return 0 ;
}


この問題を解決するには、以前に受信したロックの再取埗を防ぐようにコヌドを修正する必芁がありたす。 std :: recursive_mutexを束葉杖の゜リュヌションずしお䜿甚できたすが、そのような゜リュヌションはほずんどの堎合、プログラムのアヌキテクチャが貧匱であるこずを瀺しおいたす。

間違い10std ::原子型で十分な堎合にmutexを䜿甚する



ブヌル倀や敎数カりンタヌなどの単玔なデヌタ型を倉曎する必芁がある堎合、 stdatomicを䜿甚するず、䞀般にミュヌテックスを䜿甚するよりもパフォヌマンスが向䞊したす。

たずえば、次の構成を䜿甚する代わりに

int counter ;
...
mu. lock ( ) ;
counter ++ ;
mu. unlock ( ) ;


倉数をstd :: atomicずしお宣蚀するこずをお勧めしたす。

std :: atomic < int > counter ;
...
counter ++ ;


ミュヌテックスずアトミックの詳现な比范に぀いおは、蚘事「比范C ++ 11 vs. ミュヌテックスずRWロック»

間違い11空きスレッドのプヌルを䜿甚する代わりに、倚数のスレッドを盎接䜜成および砎棄する


スレッドの䜜成ず砎棄は、プロセッサ時間の芳点からするず高䟡な操䜜です。 システムが蚈算負荷の高い操䜜、たずえばグラフィックスのレンダリングやゲヌム物理孊の蚈算を実行しおいる間にストリヌムを䜜成しようずするこずを想像しおください。 このようなタスクによく䜿甚されるアプロヌチは、プロセスのラむフサむクル党䜓を通しおディスクぞの曞き蟌みやネットワヌク経由のデヌタ送信などのルヌチンタスクを凊理できる事前割り圓おスレッドのプヌルを䜜成するこずです。

自分でスレッドを生成および砎棄するこずず比范したスレッドプヌルのもう1぀の利点は、スレッドのオヌバヌサブスクリプション スレッドの数が䜿甚可胜なコアの数を超え、プロセッサヌ時間のかなりの郚分がコンテキストの切り替えに費やされる状況を心配する必芁がないこずです翻蚳者]。 これは、システムのパフォヌマンスに圱響を䞎える可胜性がありたす。

さらに、プヌルを䜿甚するず、スレッドのラむフサむクルを管理する手間が省け、最終的に゚ラヌの少ないよりコンパクトなコヌドになりたす。

スレッドプヌルを実装する2぀の最も䞀般的なラむブラリは、 IntelスレッドビルディングブロックTBBずMicrosoft Parallel Patterns LibraryPPLです。

゚ラヌ12バックグラりンドスレッドで発生する䟋倖を凊理しない


あるスレッドでスロヌされた䟋倖を別のスレッドで凊理するこずはできたせん。 䟋倖をスロヌする関数があるず想像しおみたしょう。 メむンの実行スレッドから分岐した別のスレッドでこの関数を実行し、远加のスレッドからスロヌされた䟋倖をキャッチするず予想される堎合、これは機胜したせん。 䟋を考えおみたしょう

#include "stdafx.h"
#include<iostream>
#include<thread>
#include<exception>
#include<stdexcept>

static std :: exception_ptr teptr = nullptr ;

void LaunchRocket ( )
{
throw std :: runtime_error ( "Catch me in MAIN" ) ;
}

int main ( )
{
try
{
std :: thread t1 ( LaunchRocket ) ;
t1. join ( ) ;
}
catch ( const std :: exception & ex )
{
std :: cout << "Thread exited with exception: " << ex. what ( ) << " \n " ;
}

return 0 ;
}


このプログラムが実行されるずクラッシュしたすが、main関数のcatchブロックは実行されず、スレッドt1でスロヌされた䟋倖を凊理したせん。

この問題の解決策は、C ++ 11の機胜を䜿甚するこずです。std:: exception_ptrを䜿甚しお、バックグラりンドスレッドでスロヌされた䟋倖を凊理したす。 実行する必芁がある手順は次のずおりです。


参照による䟋倖の呌び出しは、それが䜜成されたスレッドでは発生しないため、この機胜は異なるスレッドで䟋倖を凊理するのに最適です。

以䞋のコヌドでは、バックグラりンドスレッドでスロヌされた䟋倖を安党に凊理できたす。

#include "stdafx.h"
#include<iostream>
#include<thread>
#include<exception>
#include<stdexcept>

static std :: exception_ptr globalExceptionPtr = nullptr ;

void LaunchRocket ( )
{
try
{
std :: this_thread :: sleep_for ( std :: chrono :: milliseconds ( 100 ) ) ;
throw std :: runtime_error ( "Catch me in MAIN" ) ;
}
catch ( ... )
{
//
globalExceptionPtr = std :: current_exception ( ) ;
}
}

int main ( )
{
std :: thread t1 ( LaunchRocket ) ;
t1. join ( ) ;

if ( globalExceptionPtr )
{
try
{
std :: rethrow_exception ( globalExceptionPtr ) ;
}
catch ( const std :: exception & ex )
{
std :: cout << "Thread exited with exception: " << ex. what ( ) << " \n " ;
}
}

return 0 ;
}


間違い13std :: asyncを䜿甚する代わりに、スレッドを䜿甚しお非同期操䜜をシミュレヌトする


非同期に実行するコヌドが必芁な堎合、぀たり メむンスレッドの実行をブロックせずに、 std :: asyncを䜿甚するのが最良の遞択です。 これは、ストリヌムを䜜成し、関数ラムダの圢匏の関数たたはパラメヌタヌぞのポむンタヌを介しおこのストリヌムに実行するために必芁なコヌドを枡すこずず同等です。 ただし、埌者の堎合、このスレッドで発生する可胜性のあるすべおの䟋倖の凊理ず同様に、このスレッドの䜜成、接続/切断を監芖する必芁がありたす。 std :: asyncを䜿甚するず、これらの問題から解攟され、 デッドロック状態になる可胜性が倧幅に枛少したす。

std :: asyncを䜿甚するもう1぀の重芁な利点は、 std :: futureオブゞェクトを䜿甚しお、呌び出し元のスレッドに非同期操䜜の結果を取埗できるこずです。 intを返すConjureMagic関数があるずしたす。 タスクが完了するず、 未来のオブゞェクトに将来の倀を蚭定する非同期操䜜を開始でき、操䜜の呌び出し元の実行フロヌでこのオブゞェクトから実行結果を抜出できたす。

// future
std :: future asyncResult2 = std :: async ( & ConjureMagic ) ;

//... - future

// future
int v = asyncResult2. get ( ) ;


実行䞭のスレッドから呌び出し元に結果を取埗するこずは、より面倒です。 次の2぀の方法が可胜です。

  1. 出力倉数ぞの参照を、結果を保存するストリヌムに枡したす。
  2. 結果をワヌクフロヌオブゞェクトのフィヌルド倉数に保存したす。この倉数は、スレッドの実行が完了するずすぐに読み取るこずができたす。

Kurt Guntherothは、パフォヌマンスの芳点から、ストリヌムを䜜成するオヌバヌヘッドがasyncを䜿甚するオヌバヌヘッドの14倍であるこずを発芋したした。

結論 std :: threadを盎接䜿甚するこずを支持する匷力な匕数が芋぀かるたで、デフォルトでstd :: asyncを䜿甚したす。

゚ラヌ14非同期が必芁な堎合は、std :: launch :: asyncを䜿甚しないでください


std :: async関数は、デフォルトでは非同期に実行されない可胜性があるため、完党に正しいわけではありたせん

2぀のstd ::非同期ランタむムポリシヌがありたす。

  1. std :: launch :: async 枡された関数は別のスレッドですぐに実行を開始したす
  2. std :: launch :: deferred 枡された関数はすぐには開始されず、 std :: futureオブゞェクトでgetたたはwait呌び出しが行われる前にその起動が遅延され、 std :: async呌び出しから返されたす。 これらのメ゜ッドが呌び出される堎所で、関数は同期的に実行されたす。

デフォルトのパラメヌタヌを指定しおstd :: asyncを呌び出すず、これら2぀のパラメヌタヌの組み合わせで開始され、実際には予枬できない動䜜が発生したす。 デフォルトの起動ポリシヌでstdasyncを䜿甚するこずには、他にも倚くの困難が䌎いたす。


これらのすべおの問題を回避するには、 垞に std :: launch :: async launchポリシヌでstd :: asyncを呌び出したす。

これをしないでください

// myFunction std::async
auto myFuture = std :: async ( myFunction ) ;


代わりに、これを行いたす

// myFunction
auto myFuture = std :: async ( std :: launch :: async , myFunction ) ;


この点に぀いおは、スコット・マむダヌズの著曞「Effective and Modern C ++」でさらに詳しく怜蚎されおいたす。

間違い15stdのgetメ゜ッドの呌び出し::実行時間が重芁なコヌドブロック内のfutureオブゞェクト


以䞋のコヌドは、非同期操䜜のstd :: futureオブゞェクトから取埗した結果を凊理したす。 ただし、 whileルヌプは、非同期操䜜が完了するたでロックされたす この堎合、10秒間。 このルヌプを䜿甚しお画面に情報を衚瀺する堎合、ナヌザヌむンタヌフェむスのレンダリングに䞍快な遅延が発生する可胜性がありたす。

#include "stdafx.h"
#include <future>
#include <iostream>

int main ( )
{
std :: future < int > myFuture = std :: async ( std :: launch :: async , [ ] ( )
{
std :: this_thread :: sleep_for ( std :: chrono :: seconds ( 10 ) ) ;
return 8 ;
} ) ;

//
while ( true )
{
//
std :: cout << "Rendering Data" << std :: endl ;
int val = myFuture. get ( ) ; // 10
// - Val
}

return 0 ;
}


泚 䞊蚘のコヌドのもう1぀の問題は、 std :: futureオブゞェクトの状態がルヌプの最初の反埩で取埗され、取埗できなかったにもかかわらず、 std :: futureオブゞェクトに2回アクセスしようずするこずです。

正しい解決策は、 getメ゜ッドを呌び出す前に、 std :: futureオブゞェクトの有効性をチェックするこずです。 したがっお、非同期タスクの完了をブロックせず、既に抜出されたstd :: futureオブゞェクトに問い合わせようずしたせん。

このコヌドスニペットを䜿甚するず、これを実珟できたす。

#include "stdafx.h"
#include <future>
#include <iostream>

int main ( )
{
std :: future < int > myFuture = std :: async ( std :: launch :: async , [ ] ( )
{
std :: this_thread :: sleep_for ( std :: chrono :: seconds ( 10 ) ) ;
return 8 ;
} ) ;

//
while ( true )
{
//
std :: cout << "Rendering Data" << std :: endl ;

if ( myFuture. valid ( ) )
{
int val = myFuture. get ( ) ; // 10

// - Val
}
}

return 0 ;
}


№16: , , , std::future::get()


次のコヌドフラグメントがあるず想像しおください。std:: future :: getを呌び出した結果はどうなるず思いたすかプログラムがクラッシュするず思われる堎合-あなたは絶察に正しいです非同期操䜜でスロヌされる䟋倖は、std :: futureオブゞェクトでgetメ゜ッドが呌び出された堎合にのみスロヌされたす。たた、getメ゜ッドが呌び出されない堎合、std :: futureオブゞェクトがスコヌプ倖になるず、䟋倖は無芖されおスロヌされたす。あなたの非同期操䜜が䟋倖を投げるこずができる堎合は、い぀でもコヌルラップする必芁があるのstd ::未来:: GET内のtry / catchのブロックを。これがどのように芋えるかの䟋

#include "stdafx.h"
#include <future>
#include <iostream>

int main ( )
{
std :: future < int > myFuture = std :: async ( std :: launch :: async , [ ] ( )
{
throw std :: runtime_error ( "Catch me in MAIN" ) ;
return 8 ;
} ) ;

if ( myFuture. valid ( ) )
{
int result = myFuture. get ( ) ;
}

return 0 ;
}








#include "stdafx.h"
#include <future>
#include <iostream>

int main ( )
{
std :: future < int > myFuture = std :: async ( std :: launch :: async , [ ] ( )
{
throw std :: runtime_error ( "Catch me in MAIN" ) ;
return 8 ;
} ) ;

if ( myFuture. valid ( ) )
{
try
{
int result = myFuture. get ( ) ;
}
catch ( const std :: runtime_error & e )
{
std :: cout << "Async task threw exception: " << e. what ( ) << std :: endl ;
}
}
return 0 ;
}


№17: std::async,


がのstd ::非同期は、ほずんどのケヌスで十分です、あなたは、ストリヌムであなたのコヌドの実行䞊の泚意深い監芖が必芁な可胜性のある状況がありたす。たずえば、特定のスレッドをマルチプロセッサシステムXboxなどの特定のプロセッサコアにバむンドする堎合。

指定されたコヌドフラグメントは、システム内の5番目のプロセッサコアぞのスレッドのバむンドを確立したす。これは、std :: threadオブゞェクトのnative_handleメ゜ッドず、Win32 APIストリヌム関数に枡すこずで可胜です。ストリヌミングWin32 APIには、std :: threadたたはstd :: asyncで利甚できない機胜が他にもたくさんありたす。䜜業するずき

#include "stdafx.h"
#include <windows.h>
#include <iostream>
#include <thread>

using namespace std ;

void LaunchRocket ( )
{
cout << "Launching Rocket" << endl ;
}

int main ( )
{
thread t1 ( LaunchRocket ) ;

DWORD result = :: SetThreadIdealProcessor ( t1. native_handle ( ) , 5 ) ;

t1. join ( ) ;

return 0 ;
}


std :: async、これらの基本的なプラットフォヌム関数は䜿甚できないため、このメ゜ッドはより耇雑なタスクには適しおいたせん。

別の方法は、std :: packaged_taskを䜜成し、スレッドのプロパティを蚭定した埌、目的の実行スレッドに移動するこずです。

間違い18コアが利甚できるよりもはるかに倚くの「実行䞭の」スレッドを䜜成する


アヌキテクチャの芳点から、フロヌは「実行䞭」ず「埅機䞭」の2぀のグルヌプに分類できたす。

実行䞭のスレッドは、実行䞭のカヌネルのプロセッサ時間の100を利甚したす。耇数の実行䞭のスレッドが1぀のコアに割り圓おられるず、CPU䜿甚効率が䜎䞋したす。 1぀のプロセッサコアで耇数の実行䞭のスレッドを実行しおもパフォヌマンスは向䞊したせん。実際、远加のコンテキスト切り替えによりパフォヌマンスが䜎䞋したす。

埅機スレッドは、システムむベントやネットワヌクI / Oなどを埅機しおいる間に実行される数クロックサむクルのみを䜿甚したす。この堎合、カヌネルの利甚可胜なプロセッサヌ時間のほずんどは未䜿甚のたたです。 1぀の埅機スレッドはデヌタを凊理でき、他のスレッドはむベントのトリガヌを埅機しおいたす。これが、耇数の埅機スレッドを1぀のコアに分散するこずが有利な理由です。コアごずに耇数の保留䞭のスレッドをスケゞュヌルするず、プログラムのパフォヌマンスが倧幅に向䞊したす。

それでは、システムがサポヌトする実行䞭のスレッドの数をどのように理解するのでしょうかstd :: thread :: hardware_concurrencyメ゜ッドを䜿甚したす。通垞、この関数はプロセッサコアの数を返したすが、2぀以䞊の論理コアのように動䜜するコアを考慮に入れたすgipertredinga。

タヌゲットプラットフォヌムの取埗倀を䜿甚しお、プログラムで同時に実行されるスレッドの最倧数を蚈画する必芁がありたす。保留䞭のすべおのスレッドに1぀のコアを割り圓お、スレッドの実行に残りのコア数を䜿甚するこずもできたす。たずえば、クアッドコアシステムでは、すべおの保留䞭のスレッドに1぀のコアを䜿甚し、残りの3぀のコアに3぀の実行䞭のスレッドを䜿甚したす。スレッドスケゞュヌラの効率に応じお、実行可胜スレッドの䞀郚はペヌゞアクセスの倱敗などによりコンテキストを切り替え、カヌネルをしばらく非アクティブのたたにする堎合がありたす。プロファむリング䞭にこの状況を芳察する堎合、実行するスレッドの数をコアの数よりもわずかに倚く䜜成し、この倀をシステムに構成する必芁がありたす。

間違い19同期にvolatileキヌワヌドを䜿甚する


volatileキヌワヌドは、倉数のタむプを指定する前に、この倉数に察する操䜜をアトミックたたはスレッドセヌフにしたせん。おそらく必芁なのはstd :: atomicです。

詳现に぀いおは、stackoverflowの説明を参照しおください。

間違い20絶察に必芁でない限り、Lock Free Architectureを䜿甚する


すべおの゚ンゞニアが奜む耇雑なものがありたす。ロックなしで動䜜するプログラムを䜜成するこずは、ミュヌテックス、条件倉数、非同期などの埓来の同期メカニズムず比范しお非垞に魅力的です。しかし、私が話した経隓豊富なC ++開発者は皆意芋がありたした。初期オプションずしお非ロックプログラミングを䜿甚するこずは、最も䞍適切な時点で暪向きになる可胜性がある䞀皮の時期尚早な最適化です完党なヒヌプダンプがない堎合のオペレヌティングシステムのクラッシュに぀いお考えおください。

C ++での私のキャリアでは、ロックのないコヌドの実行を必芁ずする状況は1぀だけでした。これは、リ゜ヌスが限られたシステムで䜜業し、コンポヌネントの各トランザクションが10マむクロ秒以内で枈むためです。

ブロックせずに開発アプロヌチを適甚するこずを考える前に、3぀の質問に答えおください。


芁玄するず、通垞のアプリケヌション開発では、他のすべおの遞択肢を䜿い果たした堎合にのみ、非ロックプログラミングを怜蚎しおください。これを芋るもう1぀の方法は、䞊蚘の19個の゚ラヌのいく぀かをただ䜜成しおいる堎合、おそらくブロックせずにプログラミングから離れる必芁があるずいうこずです。

[から。翻蚳者この蚘事の準備を手䌝っおくれたvovo4Kに感謝したす。]

Source: https://habr.com/ru/post/J443406/


All Articles