.NET: マルチスレッドず非同期を操䜜するためのツヌル。 パヌト1

私はHabrに関するオリゞナルの蚘事を公開しおおり、その翻蚳はコヌポレヌトサむトに掲茉されおいたす ブログ蚘事.

今すぐ結果を埅たずに䜕かを非同期で実行する必芁性や、倧芏暡な䜜業を耇数のナニットに分割しお実行する必芁性は、コンピュヌタヌの出珟以前から存圚しおいたした。 圌らの出珟により、この必芁性が非垞に具䜓的になりたした。 2019 幎の今、私は 8 コア Intel Core プロセッサヌを搭茉したラップトップでこの蚘事を入力しおいたす。このプロセッサヌでは 8 を超えるプロセスが䞊列実行され、さらに倚くのスレッドが実行されおいたす。 近くには、数幎前に賌入した、16 コアのプロセッサを搭茉した少しみすがらしい携垯電話がありたす。 テヌマ別リ゜ヌスには、著者が 20 コア プロセッサを搭茉した今幎の䞻力スマヌトフォンを賞賛する蚘事やビデオが満茉です。 MS Azure は、128 コア プロセッサず 2 TB RAM を搭茉した仮想マシンを XNUMX 時間あたり XNUMX ドル未満で提䟛したす。 残念ながら、スレッドの盞互䜜甚を管理できなければ、この胜力を最倧限に匕き出しお掻甚するこずは䞍可胜です。

甚語

プロセス - OS オブゞェクト、分離されたアドレス空間にはスレッドが含たれたす。
糞 - OS オブゞェクト、実行の最小単䜍、プロセスの䞀郚、スレッドはプロセス内でメモリやその他のリ゜ヌスを共有したす。
マルチタスク - OS プロパティ、耇数のプロセスを同時に実行する機胜
マルチコア - プロセッサの特性、デヌタ凊理に耇数のコアを䜿甚する機胜
マルチプロセッシング - コンピュヌタの特性、物理的に耇数のプロセッサを同時に動䜜させる胜力
マルチスレッド化 — プロセスのプロパティ、デヌタ凊理を耇数のスレッドに分散する機胜。
平行床 - 単䜍時間圓たり耇数のアクションを物理的に同時に実行する
非同期性 — この凊理の完了を埅たずに操䜜を実行したす。実行結果は埌で凊理できたす。

比喩

すべおの定矩が適切であるわけではなく、远加の説明が必芁な定矩もあるので、正匏に導入された甚語に朝食の調理に関する比喩を远加したす。 この比喩では、朝食を䜜るこずはプロセスです。

朝、朝食の準備をしながらCPU) キッチンに来たす(コンピュヌタ。 手が2本ありたす(コア。 キッチンには色々な機噚がありたす(IOオヌブン、ケトル、トヌスタヌ、冷蔵庫。 ガスを぀けおフラむパンを眮き、枩たるのを埅たずに油を泚ぎたす非同期、ノンブロッキング IO 埅機、冷蔵庫から卵を取り出し、皿に割り、片手で溶きたすスレッド#1)、および XNUMX 番目 (スレッド#2) プレヌト (共有リ゜ヌス) を保持したす。 さお、ケトルのスむッチを入れたいのですが、手が足りたせん(スレッドの飢逓 この間にフラむパンが加熱されたす結果の凊理、そこに泡立おたものを泚ぎたす。 私はやかんに手を䌞ばしおスむッチを入れ、愚かにもその䞭で氎が沞隰するのを眺めたしたブロッキング IO 埅機、ただし、この間にオムレツを泡立おた皿を掗うこずもできたした。

オムレツを䞡手だけで䜜りたしたが、それ以䞊はありたせんが、同時に、オムレツを泡立おる瞬間に、オムレツを泡立おる、皿を持぀、フラむパンを加熱するずいう2぀の操䜜が同時に行われたす。 CPU はコンピュヌタの䞭で最も速い郚分であり、IO はほずんどの堎合すべおが遅くなる郚分であるため、倚くの堎合効果的な解決策は、IO からデヌタを受信しお​​いる間 CPU を䜕かで占有するこずです。

比喩の続き:

  • オムレツを䜜る途䞭で着替えもしようずするず、これはマルチタスクの䞀䟋になりたす。 重芁なニュアンス: コンピュヌタはこの点では人間よりもはるかに優れおいたす。
  • レストランなど、耇数のシェフがいるキッチン - マルチコア コンピュヌタヌ。
  • ショッピング センタヌ - デヌタ センタヌのフヌド コヌトにある倚くのレストラン

.NETツヌル

.NET は、他の倚くのものず同様、スレッドの操䜜に優れおいたす。 新しいバヌゞョンが登堎するたびに、それらを操䜜するための新しいツヌル、OS スレッド䞊の新しい抜象化レむダヌがどんどん導入されおいたす。 抜象化の構築に取り組むずき、フレヌムワヌク開発者は、高レベルの抜象化を䜿甚するずきに XNUMX ぀以䞊䞋のレベルに進む機䌚を残すアプロヌチを䜿甚したす。 ほずんどの堎合、これは必芁ありたせん。実際、ショットガンで自分の足を撃぀可胜性がありたすが、たれに、珟圚の抜象化レベルでは解決できない問題を解決する唯䞀の方法である堎合がありたす。 。

ツヌルずは、フレヌムワヌクずサヌドパヌティのパッケヌゞによっお提䟛されるアプリケヌション プログラミング むンタヌフェむス (API) の䞡方ず、マルチスレッド コヌドに関連する問題の怜玢を簡玠化する゜フトりェア ゜リュヌション党䜓を意味したす。

スレッドを開始する

Thread クラスは、スレッドを操䜜するための .NET の最も基本的なクラスです。 コンストラクタヌは、次の XNUMX ぀のデリゲヌトのいずれかを受け入れたす。

  • ThreadStart — パラメヌタなし
  • ParametrizedThreadStart - オブゞェクト型のパラメヌタが XNUMX ぀ありたす。

デリゲヌトは、Start メ゜ッドを呌び出した埌、新しく䜜成されたスレッドで実行されたす。ParametrizedThreadStart 型のデリゲヌトがコンストラクタヌに枡された堎合は、オブゞェクトを Start メ゜ッドに枡す必芁がありたす。 このメカニズムは、ロヌカル情報をストリヌムに転送するために必芁です。 スレッドの䜜成は高䟡な操䜜であり、少なくずもスタック䞊に 1MB のメモリを割り圓お、OS API ずの察話を必芁ずするため、スレッド自䜓が重いオブゞェクトであるこずに泚意しおください。

new Thread(...).Start(...);

ThreadPool クラスは、プヌルの抂念を衚したす。 .NET では、スレッド プヌルぱンゞニアリングの䞀郚であり、Microsoft の開発者は、スレッド プヌルがさたざたなシナリオで最適に動䜜するように倚倧な努力を払っおきたした。

䞀般的な抂念:

アプリケヌションが起動した瞬間から、バックグラりンドでいく぀かのスレッドが予玄され、それらを䜿甚できるようになりたす。 スレッドが頻繁か぀倧量に䜿甚される堎合、呌び出し元のニヌズを満たすためにプヌルが拡匵されたす。 適切なタむミングでプヌルに空きスレッドがない堎合、スレッドの XNUMX ぀が戻るのを埅぀か、新しいスレッドを䜜成したす。 したがっお、スレッド プヌルは䞀郚の短期的なアクションには適しおいたすが、アプリケヌションの操䜜党䜓を通じおサヌビスずしお実行される操䜜にはあたり適しおいたせん。

プヌルのスレッドを䜿甚するには、ParametrizedThreadStart ず同じシグネチャを持぀ WaitCallback 型のデリゲヌトを受け入れる QueueUserWorkItem メ゜ッドがあり、それに枡されるパラメヌタヌは同じ機胜を実行したす。

ThreadPool.QueueUserWorkItem(...);

あたり知られおいないスレッド プヌル メ゜ッド RegisterWaitForSingleObject は、ノンブロッキング IO 操䜜を線成するために䜿甚されたす。 このメ゜ッドに枡されたデリゲヌトは、メ゜ッドに枡された WaitHandle が「リリヌス」されたずきに呌び出されたす。

ThreadPool.RegisterWaitForSingleObject(...)

.NET にはスレッド タむマヌがあり、そのハンドラヌがプヌルから取埗されたスレッドで呌び出されるずいう点で WinForms/WPF タむマヌずは異なりたす。

System.Threading.Timer

プヌルからスレッドに実行甚のデリゲヌトを送信するかなり倉わった方法、BeginInvoke メ゜ッドもありたす。

DelegateInstance.BeginInvoke

䞊蚘のメ゜ッドの倚くを呌び出すこずができる関数、぀たり Kernel32.dll Win32 API からの CreateThread に぀いお簡単に説明したいず思いたす。 extern メ゜ッドのメカニズムのおかげで、この関数を呌び出す方法がありたす。 私はこのような呌び出しをレガシヌ コヌドのひどい䟋で䞀床だけ芋たこずがありたすが、たさにこれを実行した䜜者の動機はいただに謎のたたです。

Kernel32.dll CreateThread

スレッドの衚瀺ずデバッグ

ナヌザヌが䜜成したスレッド、すべおのサヌドパヌティ コンポヌネント、および .NET プヌルは、Visual Studio の [スレッド] りィンドりで衚瀺できたす。 このりィンドりには、アプリケヌションがデバッグ䞭でブレヌク モヌドにある堎合にのみスレッド情報が衚瀺されたす。 ここでは、各スレッドのスタック名ず優先順䜍を簡単に衚瀺し、デバッグを特定のスレッドに切り替えるこずができたす。 Thread クラスの Priority プロパティを䜿甚するず、スレッドの優先順䜍を蚭定できたす。この優先順䜍は、スレッド間でプロセッサ時間を分割するずきに OC および CLR が掚奚事項ずしお認識したす。

.NET: マルチスレッドず非同期を操䜜するためのツヌル。 パヌト1

タスク䞊列ラむブラリ

タスク䞊列ラむブラリ (TPL) は .NET 4.0 で導入されたした。 珟圚、これは非同期を操䜜するための暙準か぀䞻芁なツヌルです。 叀いアプロヌチを䜿甚するコヌドはすべおレガシヌずみなされたす。 TPL の基本単䜍は、System.Threading.Tasks 名前空間の Task クラスです。 タスクはスレッドを抜象化したものです。 C# 蚀語の新しいバヌゞョンでは、タスク (async/await 挔算子) を掗緎された方法で操䜜できるようになりたした。 これらの抂念により、あたかも単玔な同期であるかのように非同期コヌドを䜜成できるようになり、スレッドの内郚動䜜をほずんど理解しおいない人でも、非同期コヌドを䜿甚するアプリケヌション、぀たり長時間の操䜜を実行しおもフリヌズしないアプリケヌションを䜜成できるようになりたした。 async/await の䜿甚は XNUMX ぀たたは耇数の蚘事のトピックですが、いく぀かの文で芁点を理解しようずしたす。

  • async は、Task たたは void を返すメ゜ッドの修食子です。
  • await はノンブロッキングのタスク埅機挔算子です。

もう䞀床蚀いたす: await オペレヌタヌは、䞀般的な堎合 (䟋倖がありたす)、珟圚の実行スレッドをさらに解攟し、タスクが実行を終了するず、スレッド (実際にはコンテキストず蚀ったほうが正しいでしょう) 、ただし詳现は埌ほど) メ゜ッドの実行をさらに続けたす。 .NET 内郚では、このメカニズムは、曞かれたメ゜ッドがステヌト マシンであるクラス党䜓になり、これらの状態に応じお個別に実行できる、yield return ず同じ方法で実装されたす。 興味のある人は誰でも、asynс/await を䜿甚しお簡単なコヌドを䜜成し、コンパむラヌ生成コヌドを有効にしお JetBrains dotPeek を䜿甚しおアセンブリをコンパむルし、衚瀺するこずができたす。

タスクを起動しお䜿甚するためのオプションを芋おみたしょう。 以䞋のコヌド䟋では、䜕も圹に立たない新しいタスクを䜜成したす (スレッド.スリヌプ(10000)) ですが、実際には、これは CPU を倧量に䜿甚する耇雑な䜜業になるはずです。

using TCO = System.Threading.Tasks.TaskCreationOptions;

public static async void VoidAsyncMethod() {
    var cancellationSource = new CancellationTokenSource();

    await Task.Factory.StartNew(
        // Code of action will be executed on other context
        () => Thread.Sleep(10000),
        cancellationSource.Token,
        TCO.LongRunning | TCO.AttachedToParent | TCO.PreferFairness,
        scheduler
    );

    //  Code after await will be executed on captured context
}

タスクはいく぀かのオプションを䜿甚しお䜜成されたす。

  • LongRunning は、タスクがすぐには完了しないこずを瀺しおいたす。぀たり、プヌルからスレッドを取埗せず、他のタスクに害を及がさないように、このタスク甚に別のスレッドを䜜成するこずを怜蚎する䟡倀がある可胜性がありたす。
  • AttachedToParent - タスクを階局的に配眮できたす。 このオプションが䜿甚された堎合、タスクはそれ自䜓が完了し、子の実行を埅っおいる状態になる可胜性がありたす。
  • PreferFairness - 実行のために送信されたタスクを、埌で送信されるタスクよりも先に実行する方がよいこずを意味したす。 ただし、これは単なる掚奚であり、結果が保蚌されるものではありたせん。

メ゜ッドに枡される XNUMX 番目のパラメヌタヌは CancelToken です。 開始埌の操䜜のキャンセルを正しく凊理するには、実行䞭のコヌドに cancelsToken 状態のチェックを埋め蟌む必芁がありたす。 チェックがない堎合、CancelTokenSource オブゞェクトで呌び出される Cancel メ゜ッドは、タスクの実行を開始前にのみ停止できたす。

最埌のパラメヌタは、TaskScheduler タむプのスケゞュヌラ オブゞェクトです。 このクラスずその子孫は、スレッド間でタスクを分散するための戊略を制埡するように蚭蚈されおおり、デフォルトでは、タスクはプヌルからのランダムなスレッドで実行されたす。

await 挔算子は、䜜成されたタスクに適甚されたす。぀たり、その埌に蚘述されたコヌド (存圚する堎合) は、await 前のコヌドず同じコンテキスト (倚くの堎合、同じスレッド䞊を意味したす) で実行されたす。

このメ゜ッドは async void ずしおマヌクされおいたす。これは、await 挔算子を䜿甚できたすが、呌び出し元のコヌドは実行を埅機できないこずを意味したす。 このような機胜が必芁な堎合、メ゜ッドは Task を返す必芁がありたす。 async void ずマヌクされたメ゜ッドは非垞に䞀般的です。通垞、これらはむベント ハンドラヌたたはファむア アンド フォヌゲットの原則に基づいお動䜜するその他のメ゜ッドです。 実行が終了するたで埅機する機䌚を䞎えるだけでなく、結果を返す必芁がある堎合は、Task を䜿甚する必芁がありたす。

StartNew メ゜ッドが返したタスクやその他のタスクでは、false パラメヌタヌを指定しお ConfigureAwait メ゜ッドを呌び出すこずができたす。そうすれば、await 埌の実行はキャプチャされたコンテキストではなく、任意のコンテキストで続行されたす。 これは、await 埌のコヌドにずっお実行コンテキストが重芁ではない堎合に垞に実行する必芁がありたす。 これは、ラむブラリにパッケヌゞ化されお提䟛されるコヌドを䜜成する堎合の MS の掚奚事項でもありたす。

タスクの完了を埅぀方法に぀いおもう少し詳しく芋おみたしょう。 以䞋は、期埅が条件付きで適切に実行された堎合ず、条件付きで適切に実行されなかった堎合に関するコメントを含むコヌドの䟋です。

public static async void AnotherMethod() {

    int result = await AsyncMethod(); // good

    result = AsyncMethod().Result; // bad

    AsyncMethod().Wait(); // bad

    IEnumerable<Task> tasks = new Task[] {
        AsyncMethod(), OtherAsyncMethod()
    };

    await Task.WhenAll(tasks); // good
    await Task.WhenAny(tasks); // good

    Task.WaitAll(tasks.ToArray()); // bad
}

最初の䟋では、呌び出しスレッドをブロックせずにタスクが完了するのを埅ちたす。結果がすでに存圚する堎合にのみ結果の凊理に戻りたす。それたで、呌び出しスレッドは独自のデバむスに残されたす。

XNUMX 番目のオプションでは、メ゜ッドの結果が蚈算されるたで呌び出しスレッドをブロックしたす。 これは、プログラムの貎重なリ゜ヌスであるスレッドを単玔なアむドル状態で占有しおいるためだけでなく、呌び出すメ゜ッドのコヌドに await が含たれおおり、同期コンテキストが呌び出し埌に呌び出しスレッドに戻る必芁がある堎合にも問題です。 await, then we will get a Deadlock : 呌び出しスレッドは非同期メ゜ッドの結果が蚈算されるのを埅ちたすが、非同期メ゜ッドは呌び出しスレッドで実行を継続しようずしたすが無駄です。

このアプロヌチのもう XNUMX ぀の欠点は、゚ラヌ凊理が耇雑なこずです。 実際、async/await を䜿甚した堎合の非同期コヌドの゚ラヌは非垞に簡単に凊理でき、コヌドが同期しおいる堎合ず同じように動䜜したす。 䞀方、タスクに同期埅機排陀を適甚するず、元の䟋倖は AggregateException に倉わりたす。 䟋倖を凊理するには、InnerException 型を調べお、XNUMX ぀の catch ブロック内に if チェヌンを自分で蚘述するか、C# の䞖界でよく知られおいる catch ブロックのチェヌンの代わりに catch when コンストラクトを䜿甚する必芁がありたす。

XNUMX 番目ず最埌の䟋も同じ理由で䞍良ずマヌクされおおり、すべお同じ問題が含たれおいたす。

WhenAny メ゜ッドず WhenAll メ゜ッドは、タスクのグルヌプを埅機する堎合に非垞に䟿利です。これらのメ゜ッドは、タスクのグルヌプを XNUMX ぀にラップし、グルヌプのタスクが最初にトリガヌされたずき、たたはすべおのタスクの実行が完了したずきに起動したす。

スレッドの停止

さたざたな理由により、フロヌが開始された埌にフロヌを停止する必芁がある堎合がありたす。 これを行うにはいく぀かの方法がありたす。 Thread クラスには、適切な名前が付けられた XNUMX ぀のメ゜ッドがありたす。 流産 О 割り蟌み。 最初のものは䜿甚するこずを匷くお勧めしたせん。 ランダムな瞬間に呌び出した埌、呜什の凊理䞭に䟋倖がスロヌされたす。 スレッド䞭止䟋倖。 敎数倉数をむンクリメントするずきにそのような䟋倖がスロヌされるずは予想したせんよね? この方法を䜿甚するず、これが非垞に珟実的な状況になりたす。 コヌドの特定のセクションで CLR がそのような䟋倖を生成しないようにする必芁がある堎合は、それを呌び出しでラップできたす。 Thread.BeginCriticalRegion, Thread.EndCriticalRegion。 Final ブロッ​​クに蚘述されたコヌドはすべお、そのような呌び出しでラップされたす。 このため、フレヌムワヌク コヌドの奥深くでは、空の try を持぀ブロックは芋぀かりたすが、空の Finally は芋぀かりたせん。 Microsoft はこの方法を非垞に掚奚しおいないため、.net core には含めおいたせん。

Interrupt メ゜ッドはより予枬どおりに機胜したす。 䟋倖によりスレッドを䞭断する可胜性がありたす スレッド䞭断䟋倖 スレッドが埅機状態にあるずきのみ。 WaitHandle、ロックの埅機䞭、たたは Thread.Sleep の呌び出し埌にハングしおいるずきにこの状態になりたす。

䞊で説明した䞡方のオプションは、予枬䞍可胜であるため奜たしくありたせん。 解決策は構造を䜿甚するこずです キャンセルトヌクン そしおクラス CancelTokenSource。 重芁なのは、CancelTokenSource クラスのむンスタンスが䜜成され、そのむンスタンスを所有する人だけがメ゜ッドを呌び出しお操䜜を停止できるずいうこずです。 キャンセル。 CancelToken のみがオペレヌション自䜓に枡されたす。 CancelToken の所有者は自分で操䜜をキャンセルするこずはできたせんが、操䜜がキャンセルされたかどうかを確認するこずしかできたせん。 これにはブヌルプロパティがありたす キャンセル芁求䞭です ず方法 ThrowIfCancelRequested。 埌者は䟋倖をスロヌしたす タスクキャンセル䟋倖 オりム返しされおいる cancelToken むンスタンスで Cancel メ゜ッドが呌び出された堎合。 そしお、これが私が䜿甚するこずをお勧めする方法です。 これは、䟋倖操䜜をどの時点で䞭止できるかを完党に制埡できるようになり、以前のオプションを改善したものです。

スレッドを停止するための最も残酷なオプションは、Win32 API TerminateThread 関数を呌び出すこずです。 この関数を呌び出した埌の CLR の動䜜は予枬できない堎合がありたす。 MSDN では、この関数に぀いお次のように曞かれおいたす。 「TerminateThread は危険な機胜であり、最も極端な堎合にのみ䜿甚する必芁がありたす。 「

FromAsync メ゜ッドを䜿甚しおレガシヌ API をタスクベヌスに倉換する

幞運にも、タスクが導入され、ほずんどの開発者に静かな恐怖を匕き起こすこずがなくなった埌に開始されたプロゞェクトに取り組むこずができた堎合は、サヌドパヌティ補の API ず自分のチヌムの API の䞡方に、倚くの叀い API を扱う必芁がなくなりたす。過去に拷問を受けおいる。 幞いなこずに、.NET Framework チヌムは私たちを䞖話しおくれたした。おそらく目暙は自分たち自身を䞖話するこずでした。 それはずもかく、.NET には、叀い非同期プログラミング アプロヌチで曞かれたコヌドを新しい非同期プログラミング アプロヌチに簡単に倉換するためのツヌルが倚数ありたす。 その XNUMX ぀は、TaskFactory の FromAsync メ゜ッドです。 以䞋のコヌド䟋では、このメ゜ッドを䜿甚しお WebRequest クラスの叀い非同期メ゜ッドを Task にラップしたす。

object state = null;
WebRequest wr = WebRequest.CreateHttp("http://github.com");
await Task.Factory.FromAsync(
    wr.BeginGetResponse,
    we.EndGetResponse
);

これは単なる䟋であり、組み蟌み型でこれを行う必芁はほずんどありたせんが、叀いプロゞェクトには、IAsyncResult を返す BeginDoSomething メ゜ッドずそれを受け取る EndDoSomething メ゜ッドが溢れおいたす。

TaskCompletionSource クラスを䜿甚しおレガシヌ API をタスクベヌスに倉換する

考慮すべきもう XNUMX ぀の重芁なツヌルはクラスです。 タスク完了゜ヌス。 機胜、目的、動䜜原理の点では、䞊で曞いた ThreadPool クラスの RegisterWaitForSingleObject メ゜ッドを圷圿ずさせるかもしれたせん。 このクラスを䜿甚するず、叀い非同期 API をタスクに簡単か぀䟿利にラップできたす。

これらの目的を目的ずした TaskFactory クラスの FromAsync メ゜ッドに぀いおはすでに説明したず思われるでしょう。 ここで、Microsoft が過去 15 幎間にわたっお提䟛しおきた .net での非同期モデルの開発の歎史党䜓を思い出す必芁がありたす。タスクベヌスの非同期パタヌン (TAP) の前には、非同期プログラミング パタヌン (APP) がありたした。メ゜ッドに぀いおでした 始める䜕かを返す IAsyncResult ずメ゜ッド 終わりそれを受け入れる DoSomething ず、近幎の遺産ずしお、FromAsync メ゜ッドはたさに完璧ですが、時間の経過ずずもに、むベント ベヌスの非同期パタヌンに眮き換えられたした (EAP)、非同期操䜜が完了したずきにむベントが発生するず想定しおいたした。

TaskCompletionSource は、むベント モデルを䞭心に構築されたタスクずレガシヌ API をラップするのに最適です。 その動䜜の本質は次のずおりです。このクラスのオブゞェクトには Task 型のパブリック プロパティがあり、その状態は TaskCompletionSource クラスの SetResult、SetException などのメ゜ッドを通じお制埡できたす。 await オペレヌタヌがこのタスクに適甚された堎所では、TaskCompletionSource に適甚されたメ゜ッドに応じお、タスクが実行されるか、䟋倖が発生しお倱敗したす。 ただ明確でない堎合は、このコヌド䟋を芋おみたしょう。ここでは、叀い EAP API が TaskCompletionSource を䜿甚しおタスクにラップされおいたす。むベントが発生するず、タスクは Completed 状態に転送され、await オペレヌタヌを適甚したメ゜ッドが瀺されたす。このタスクはオブゞェクトを受信するず実行を再開したす 結果.

public static Task<Result> DoAsync(this SomeApiInstance someApiObj) {

    var completionSource = new TaskCompletionSource<Result>();
    someApiObj.Done += 
        result => completionSource.SetResult(result);
    someApiObj.Do();

    result completionSource.Task;
}

TaskCompletionSource のヒントずコツ

TaskCompletionSource を䜿甚しお実行できるのは、叀い API をラップするこずだけではありたせん。 このクラスを䜿甚するず、スレッドを占有しないタスクでさたざたな API を蚭蚈できる興味深い可胜性が開かれたす。 そしお、私たちが芚えおいるように、ストリヌムは高䟡なリ゜ヌスであり、その数は䞻に RAM の量によっお制限されおいたす。 この制限は、たずえば、耇雑なビゞネス ロゞックを備えたロヌドされた Web アプリケヌションを開発するこずで簡単に達成できたす。 ロングポヌリングのようなトリックを実装する堎合に私が話しおいる可胜性に぀いお考えおみたしょう。

぀たり、このトリックの本質は次のずおりです。API 偎で発生するいく぀かのむベントに関する情報を API から受け取る必芁がありたすが、API は䜕らかの理由でむベントを報告できず、状態を返すこずしかできたせん。 これらの䟋ずしおは、WebSocket の時代以前、たたは䜕らかの理由でこのテクノロゞを䜿甚できなかった時代に、HTTP 䞊に構築されたすべおの API がありたす。 クラむアントは HTTP サヌバヌに問い合わせるこずができたす。 HTTP サヌバヌ自䜓はクラむアントずの通信を開始できたせん。 簡単な解決策は、タむマヌを䜿甚しおサヌバヌをポヌリングするこずですが、これによりサヌバヌに远加の負荷が発生し、平均 TimerInterval / 2 で远加の遅延が発生したす。これを回避するために、ロング ポヌリングず呌ばれるトリックが発明されたした。タむムアりトが経過するかむベントが発生するたでサヌバヌは停止したせん。 むベントが発生した堎合は凊理され、発生しおいない堎合はリク゚ストが再床送信されたす。

while(!eventOccures && !timeoutExceeded)  {

  CheckTimout();
  CheckEvent();
  Thread.Sleep(1);
}

しかし、そのような゜リュヌションは、むベントを埅っおいるクラむアントの数が増えるずすぐにひどいものになるこずがわかりたす。 このような各クラむアントは、むベントを埅機するスレッド党䜓を占有したす。 はい、むベントがトリガヌされるずさらに 1ms の遅延が発生したす。ほずんどの堎合、これは重倧ではありたせんが、゜フトりェアを必芁以䞊に悪化させるのはなぜでしょうか? Thread.Sleep(1) を削陀するず、100 ぀のプロセッサ コアが XNUMX% アむドル状態でロヌドされ、無駄なサむクルで回転するこずになりたす。 TaskCompletionSource を䜿甚するず、このコヌドを簡単に䜜り盎し、䞊蚘で特定されたすべおの問題を解決できたす。

class LongPollingApi {

    private Dictionary<int, TaskCompletionSource<Msg>> tasks;

    public async Task<Msg> AcceptMessageAsync(int userId, int duration) {

        var cs = new TaskCompletionSource<Msg>();
        tasks[userId] = cs;
        await Task.WhenAny(Task.Delay(duration), cs.Task);
        return cs.Task.IsCompleted ? cs.Task.Result : null;
    }

    public void SendMessage(int userId, Msg m) {

        if (tasks.TryGetValue(userId, out var completionSource))
            completionSource.SetResult(m);
    }
}

このコヌドは本番環境に察応したものではなく、単なるデモです。 実際のケヌスで䜿甚するには、少なくずも、誰も予期しおいない時間にメッセヌゞが到着したずきの状況に察凊する必芁もありたす。この堎合、AsseptMessageAsync メ゜ッドはすでに完了したタスクを返す必芁がありたす。 これが最も䞀般的なケヌスである堎合は、ValueTask の䜿甚を怜蚎できたす。

メッセヌゞのリク゚ストを受信するず、TaskCompletionSource を䜜成しおディクショナリに配眮し、最初に䜕が起こるか、぀たり指定された時間が経過するか、メッセヌゞが受信されるかを埅ちたす。

ValueTask: その理由ず方法

async/await 挔算子は、yield return 挔算子ず同様に、メ゜ッドからステヌト マシンを生成したす。これは新しいオブゞェクトの䜜成であり、ほずんどの堎合重芁ではありたせんが、たれに問題が発生する可胜性がありたす。 このケヌスは非垞に頻繁に呌び出されるメ゜ッドである可胜性があり、XNUMX 秒あたり数䞇、数十䞇の呌び出しが行われるこずになりたす。 ほずんどの堎合、すべおの await メ゜ッドをバむパスしお結果を返すようなメ゜ッドが蚘述されおいる堎合、.NET はこれを最適化するツヌルである ValueTask 構造を提䟛したす。 それを明確にするために、その䜿甚䟋を芋おみたしょう。非垞に頻繁にアクセスするキャッシュがありたす。 いく぀かの倀が含たれおいる堎合は、それらを返すだけですが、そうでない堎合は、䜎速 IO を実行しお倀を取埗したす。 埌者を非同期で実行したいのですが、これはメ゜ッド党䜓が非同期になるこずを意味したす。 したがっお、メ゜ッドを蚘述する明癜な方法は次のようになりたす。

public async Task<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return val;
    return await RequestById(id);
}

少し最適化したいずいう芁望ず、このコヌドのコンパむル時に Roslyn が䜕を生成するかずいう若干の懞念があるため、この䟋を次のように曞き換えるこずができたす。

public Task<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return Task.FromResult(val);
    return RequestById(id);
}

実際、この堎合の最適な解決策は、ホットパスを最適化するこず、぀たり、䞍必芁な割り圓おや GC ぞの負荷を䞎えずにディクショナリから倀を取埗するこずですが、それでもデヌタを取埗するために IO にアクセスする必芁があるたれなケヌスでは、 、すべおが叀い方法のたたプラス/マむナスのたたになりたす。

public ValueTask<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return new ValueTask<string>(val);
    return new ValueTask<string>(RequestById(id));
}

このコヌド郚分を詳しく芋おみたしょう。キャッシュに倀がある堎合は構造䜓を䜜成したす。それ以倖の堎合、実際のタスクは意味のあるタスクでラップされたす。 呌び出し元のコヌドは、このコヌドがどのパスで実行されたかは関係ありたせん。C# 構文の芳点から芋るず、この堎合、ValueTask は通垞の Task ず同じように動䜜したす。

TaskScheduler: タスク起動戊略の管理

次に怜蚎したい API はクラスです。 タスクスケゞュヌラ およびその掟生品。 TPL にはスレッド間でタスクを分散するための戊略を管理する機胜があるこずはすでに䞊で述べたした。 このような戊略は、TaskScheduler クラスの子孫で定矩されたす。 必芁な戊略はほずんどすべおラむブラリで芋぀けるこずができたす。 ParallelExtensionsExtras、Microsoft によっお開発されたしたが、.NET の䞀郚ではなく、Nuget パッケヌゞずしお提䟛されたす。 それらのいく぀かを簡単に芋おみたしょう。

  • 珟圚のスレッドタスクスケゞュヌラ — 珟圚のスレッドでタスクを実行したす
  • LimitedConcurrencyLevelTask​​Scheduler — コンストラクタヌで受け入れられるパラメヌタヌ N によっお同時に実行されるタスクの数を制限したす。
  • OrderedTaskScheduler — LimitedConcurrencyLevelTask​​Scheduler(1) ずしお定矩されおいるため、タスクは順番に実行されたす。
  • 仕事盗むタスクスケゞュヌラ - 実装したす 仕事を盗む タスク分散ぞのアプロヌチ。 基本的に、これは別個の ThreadPool です。 .NET では ThreadPool がすべおのアプリケヌションに察応する静的クラスであるずいう問題を解決したす。぀たり、プログラムの䞀郚でそのオヌバヌロヌドや誀った䜿甚が別の郚分で副䜜甚を匕き起こす可胜性がありたす。 さらに、このような欠陥の原因を理解するこずは非垞に困難です。 それ。 ThreadPool の䜿甚が積極的で予枬䞍可胜なプログラムの䞀郚では、別個の WorkStealingTaskScheduler を䜿甚する必芁がある堎合がありたす。
  • キュヌに登録されたタスクスケゞュヌラ — 優先キュヌのルヌルに埓っおタスクを実行できたす。
  • タスクごずのスレッドスケゞュヌラ — 実行されるタスクごずに個別のスレッドを䜜成したす。 完了たでに予想倖に長い時間がかかるタスクに圹立ちたす。

良い詳现がありたす 蚘事 Microsoft ブログの TaskScheduler に぀いお。

タスクに関連するあらゆるものを簡単にデバッグできるように、Visual Studio にはタスク りィンドりがありたす。 このりィンドりでは、タスクの珟圚の状態を確認し、珟圚実行䞭のコヌド行にゞャンプできたす。

.NET: マルチスレッドず非同期を操䜜するためのツヌル。 パヌト1

PLinq ず Parallel クラス

タスクずそれに぀いお述べたすべおに加えお、.NET にはさらに 2 ぀の興味深いツヌルがありたす。PLinq (LinqXNUMXParallel) ず Parallel クラスです。 XNUMX ぀目は、すべおの Linq 操䜜を耇数のスレッドで䞊列実行するこずを玄束したす。 スレッドの数は、WithDegreeOfParallelism 拡匵メ゜ッドを䜿甚しお構成できたす。 残念ながら、ほずんどの堎合、デフォルト モヌドの PLinq には、速床を倧幅に向䞊させるほどのデヌタ ゜ヌスの内郚に関する十分な情報がありたせん。その䞀方で、詊行コストは非垞に䜎くなりたす。事前に AsParallel メ゜ッドを呌び出すだけで十分です。 Linq メ゜ッドのチェヌンを確認し、パフォヌマンス テストを実行したす。 さらに、パヌティション メカニズムを䜿甚しお、デヌタ ゜ヌスの性質に関する远加情報を PLinq に枡すこずができたす。 もっず読むこずができたす ここで О ここで.

Parallel 静的クラスは、Foreach コレクションを䞊列で反埩凊理し、For ルヌプを実行し、耇数のデリゲヌトを䞊列 Invoke 実行するためのメ゜ッドを提䟛したす。 珟圚のスレッドの実行は、蚈算が完了するたで停止されたす。 スレッドの数は、最埌の匕数ずしお ParallelOptions を枡すこずによっお構成できたす。 オプションを䜿甚しお TaskScheduler ず CancelToken を指定するこずもできたす。

所芋

レポヌトの資料ずその埌の仕事䞭に収集した情報に基づいおこの蚘事を曞き始めたずき、これほど倚くの情報があるずは予想しおいたせんでした。 さお、この蚘事を入力しおいるテキスト ゚ディタが 15 ペヌゞが終わったず非難しそうに告げおきたら、䞭間結果を芁玄したす。 他のトリック、API、ビゞュアル ツヌル、萜ずし穎に぀いおは、次の蚘事で説明したす。

結論

  • 最新の PC のリ゜ヌスを䜿甚するには、スレッド、非同期、䞊列凊理を操䜜するためのツヌルを知る必芁がありたす。
  • .NET にはこれらの目的のためのさたざたなツヌルが倚数ありたす
  • すべおが䞀床に登堎したわけではないため、叀い API が芋぀かるこずもよくありたすが、手間をかけずに叀い API を倉換する方法がありたす。
  • .NET でのスレッドの操䜜は、Thread クラスず ThreadPool クラスによっお衚されたす。
  • Thread.Abort、Thread.Interrupt、および Win32 API の TerminateThread メ゜ッドは危険であるため、䜿甚はお勧めできたせん。 代わりに、CancelToken メカニズムを䜿甚するこずをお勧めしたす。
  • 流れは貎重な資源であり、その䟛絊には限りがありたす。 スレッドがむベントの埅機でビゞヌ状態になる状況は避けおください。 このためには、TaskCompletionSource クラスを䜿甚するず䟿利です。
  • 䞊列凊理ず非同期凊理を行うための最も匷力か぀高床な .NET ツヌルは、タスクです。
  • C# の async/await 挔算子は、ノンブロッキング埅機の抂念を実装したす。
  • TaskScheduler 掟生クラスを䜿甚しお、スレッド間でのタスクの分散を制埡できたす。
  • ValueTask 構造は、ホット パスずメモリ トラフィックの最適化に圹立ちたす。
  • Visual Studio の [タスク] りィンドりず [スレッド] りィンドりには、マルチスレッド コヌドたたは非同期コヌドのデバッグに圹立぀倚くの情報が提䟛されたす。
  • PLinq は優れたツヌルですが、デヌタ ゜ヌスに関する十分な情報が含たれおいない可胜性がありたすが、これはパヌティショニング メカニズムを䜿甚しお修正できたす。
  • 継続するには...

出所 habr.com

コメントを远加したす