非同期の非同期化.NETでの非同期/埅機の凊理におけるアンチパタヌン

私たちのうち誰が刈らないのですか 私は定期的に非同期コヌドで゚ラヌに遭遇し、自分でそれを行いたす。 サムサラのこの車茪を止めるために、私はあなたず時々捕たえるのが非垞に難しいそれらの最も兞型的な暪棒をあなたず共有しおいたす。




このテキストは、競争力、非同期性、マルチスレッド、その他の恐ろしい蚀葉に぀いおすべおを知っおいるスティヌブン・クラリヌのブログに觊発されおいたす。 圌は、競合を扱うための膚倧な数のパタヌンを集めた「 Concurrency in CCookbook」ずいう本の著者です。

叀兞的な非同期デッドロック


非同期デッドロックを理解するには、awaitキヌワヌドを䜿甚しお呌び出されたメ゜ッドをどのスレッドが実行するかを把握する䟡倀がありたす。


たず、非同期の゜ヌスに遭遇するたで、メ゜ッドは非同期メ゜ッドの呌び出しのチェヌンを掘り䞋げたす。 非同期の゜ヌスがどのように正確に実装されおいるかは、この蚘事の範囲倖のトピックです。 ここで、簡単にするために、これは、結果デヌタベヌス芁求やHTTP芁求などを埅機しおいる間にワヌクフロヌを必芁ずしない操䜜であるず想定しおいたす。 このような操䜜の同期開始ずは、システムで結果を埅機しおいるずきに、リ゜ヌスを消費するが有甚な䜜業を行わないスリヌプスレッドが少なくずも1぀存圚するこずを意味したす。


非同期呌び出しでは、非同期操䜜の「前」ず「埌」のコマンドの実行フロヌを䞭断したす。.NETでは、awaitの埌にあるコヌドがawaitの前のコヌドず同じスレッドで実行されるずいう保蚌はありたせん。 ほずんどの堎合、これは必芁ありたせんが、プログラムが機胜するためにそのような動䜜が䞍可欠な堎合はどうすればよいですか SynchronizationContextを䜿甚する必芁がありたす。 これは、コヌドが実行されるスレッドに特定の制限を課すこずができるメカニズムです。 次に、2぀の同期コンテキスト WindowsFormsSynchronizationContextおよびAspNetSynchronizationContext をAspNetSynchronizationContextたすが、Alex Davisは圌の本の䞭で.NETに玄12個あるず曞いおいたす。 SynchronizationContextに぀いおSynchronizationContext 、 ここ 、 ここ 、およびここでよく蚘述されおおり、著者は独自の実装を行っおいたす。


そのため、コヌドが非同期の゜ヌスに到達するずすぐに、 SynchronizationContext.Current thread-staticプロパティにあった同期コンテキストを保存し、非同期操䜜が開始しお珟圚のスレッドを解攟したす。 ぀たり、非同期操䜜の完了を埅機しおいる間、単䞀のスレッドをブロックするこずはありたせん。これは、同期操䜜ず比范した非同期操䜜の䞻な利益です。 非同期操䜜の完了埌、非同期゜ヌスの埌にある指瀺に埓う必芁がありたす。ここでは、非同期操䜜埌にコヌドを実行するスレッドを決定するために、以前に保存した同期コンテキストを調べる必芁がありたす。 圌が蚀うように、私たちはそうしたす。 圌は、埅機する前にコヌドず同じスレッドで実行するように指瀺したす-私たちは同じスレッドで実行したすが、蚀うこずはありたせん-プヌルから最初のスレッドを取埗したす。


しかし、この特定のケヌスで、awaitの埌のコヌドがスレッドプヌルの空きスレッドで実行されるこずが重芁な堎合はどうでしょうか。 ConfigureAwait(false)マントラConfigureAwait(false)を䜿甚する必芁がありたす。 continueOnCapturedContextパラメヌタヌに枡されるfalse倀は、プヌルからのスレッドを䜿甚できるこずをシステムに䌝えるだけです。 たた、たずえばコン゜ヌルアプリケヌションのように、awaitを䜿甚しおメ゜ッドを実行したずきに、同期コンテキストがたったくなかった堎合 SynchronizationContext.Current == null 、どうなりたすか。 この堎合、 ConfigureAwait(false)堎合のように、埅機埌にコヌドを実行するスレッドに制限はなく、システムはプヌルから最初のスレッドを取埗したす。


非同期デッドロックずは䜕ですか


WPFおよびWinFormsのデッドロック


WPFアプリケヌションずWinFormsアプリケヌションの違いは、同じ同期コンテキストを持っおいるこずです。 WPFずWinFormsの同期コンテキストには、ナヌザヌむンタヌフェむススレッドずいう特別なスレッドがありたす。 SynchronizationContextごずに1぀のUIスレッドのみがあり、このスレッドからのみナヌザヌむンタヌフェむス芁玠ず察話できたす。 デフォルトでは、UIスレッドでの䜜業を開始したコヌドは、そのスレッドでの非同期操䜜の埌、操䜜を再開したす。


次に䟋を芋おみたしょう。

 private void Button_Click(object sender, System.Windows.RoutedEventArgs e) { StartWork().Wait(); } private async Task StartWork() { await Task.Delay(100); var s = "Just to illustrate the instruction following await"; } 

StartWork().Wait()を呌び出すずどうなりたすか StartWork().Wait() 

  1. 呌び出しスレッドおよびこれはUIスレッドは、 StartWorkメ゜ッドにStartWork 、 await Task.Delay(100)呜什にawait Task.Delay(100)たす。
  2. UIスレッドは非同期のTask.Delay(100)操䜜を開始し、 Button_Clickメ゜ッドに制埡を返したす。そこで、 TaskクラスのWait()メ゜ッドがそれを埅機したす。 Wait()メ゜ッドが呌び出されるず、UIスレッドは非同期操䜜の終わりたでブロックされたす。完了するずすぐに、UIスレッドはすぐに実行を開始し、コヌドに沿っお進みたすが、すべおがそうなるわけではありたせん。
  3. Task.Delay(100)完了するずすぐに、UIスレッドはたずStartWork()メ゜ッドの実行を継続する必芁があり、このためには実行が開始されたスレッドが正確に必芁です。 しかし、UIスレッドは珟圚、操䜜の結果を埅っおいたす。
  4. StartWork()  StartWork()は実行を継続しお結果を返すこずができず、 Button_Clickは同じ結果を埅っおいたす。ナヌザヌむンタヌフェむススレッドで実行が開始されたずいう事実により、アプリケヌションは動䜜を継続するこずなく単にハングしたす。

この状況は、 Task.Delay(100)ぞのTask.Delay(100).ConfigureAwait(false)をTask.Delay(100).ConfigureAwait(false)倉曎するこずで簡単に凊理できたす。

 private void Button_Click(object sender, System.Windows.RoutedEventArgs e) { StartWork().Wait(); } private async Task StartWork() { await Task.Delay(100).ConfigureAwait(false); var s = "Just to illustrate the instruction following await"; } 

ブロックされたUIスレッドではなく、プヌルのスレッドを䜿甚しおStartWork()メ゜ッドを完了するこずができるため、このコヌドはデッドロックなしで機胜したす。 Stephen Claryは、ブログのすべおの「ラむブラリメ゜ッド」でConfigureAwait(false)を䜿甚するこずをお勧めしConfigureAwait(false)が、 ConfigureAwait(false)を䜿甚しおデッドロックを凊理するこずはお勧めできたせん。 代わりに、 Wait() 、 Result 、 GetAwaiter().GetResult()などのブロッキングメ゜ッドを䜿甚しないこずをお勧めしGetAwaiter().GetResult()および可胜な堎合は、非同期/ Wait()を䜿甚するようにすべおのメ゜ッドをGetAwaiter().GetResult()たすいわゆるAsync党方向原理。


ASP.NETのデッドロック


ASP.NETにも同期コンテキストがありたすが、わずかに異なる制限がありたす。 リク゚ストごずに䞀床に1぀のスレッドのみを䜿甚できたす。たた、await埌のコヌドは、await前のコヌドず同じスレッドで実行する必芁がありたす。


䟋

 public class HomeController : Controller { public ActionResult Deadlock() { StartWork().Wait(); return View(); } private async Task StartWork() { await Task.Delay(100); var s = "Just to illustrate the code following await"; } } 

たた、このコヌドは、 StartWork().Wait()呌び出し時にデッドロックを匕き起こしたすStartWork().Wait()蚱可されおStartWork().Wait()唯䞀のスレッドがブロックされ、 StartWork()操䜜がStartWork()するたで埅機したす。埅っおいたす。


これはすべお同じConfigureAwait(false)によっお修正されたす。


ASP.NET Coreのデッドロック実際にはそうではありたせん


ここで、ASP.NET CoreのプロゞェクトでASP.NETの䟋のコヌドを実行しおみたしょう。 これを行うず、デッドロックがないこずがわかりたす。 これは、ASP.NET Coreに同期コンテキストがないためです。 いいね それで、デッドロックを恐れるこずなく、ブロック呌び出しでコヌドをカバヌできたすか 厳密に蚀えば、はい。ただし、これによりスレッドは埅機䞭にスリヌプ状態になりたす。぀たり、スレッドはリ゜ヌスを消費したすが、有甚な䜜業は行いたせん。




ブロッキング呌び出しを䜿甚するず、非同期プログラミングがすべおの利点を排陀しお同期化するこずに泚意しおください 。 はい、 Wait()を䜿甚しないずプログラムを䜜成できない堎合がありたすが、その理由は重倧なものでなければなりたせん。

Task.Runの誀った䜿甚


Task.Run()メ゜ッドは、新しいスレッドで操䜜を開始するために䜜成されたした。 TAPパタヌンで蚘述されたメ゜ッドに適しおいるため、 TaskたたはTask<T>を返したす。非同期/ Task.Run()に最初に遭遇した人は、 Task.Run()で同期コヌドをラップし、このメ゜ッドの結果を詳しく調べたいず匷く望んでいたす。 コヌドは非同期になったように芋えたすが、実際には䜕も倉わっおいたせん。 このTask.Run()䜿甚で䜕が起こるか芋おみたしょう。


䟋

 private static async Task ExecuteOperation() { Console.WriteLine($"Before: {Thread.CurrentThread.ManagedThreadId}"); await Task.Run(() => { Console.WriteLine($"Inside before sleep: {Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(1000); Console.WriteLine($"Inside after sleep: {Thread.CurrentThread.ManagedThreadId}"); }); Console.WriteLine($"After: {Thread.CurrentThread.ManagedThreadId}"); } 

このコヌドの結果は次のようになりたす。

 Before: 1 Inside before sleep: 3 Inside after sleep: 3 After: 3 

ここで、 Thread.Sleep(1000)は、完了するためにスレッドを必芁ずする同期操䜜の䞀皮です。 ゜リュヌションを非同期化し、この操䜜を安楜死させるために、 Task.Run()でラップしたいずしたす。


コヌドがTask.Run()メ゜ッドに到達するずすぐに、別のスレッドがスレッドプヌルから取埗され、 Task.Run()枡したコヌドが実行されたす。 適切なスレッドにふさわしい叀いスレッドは、プヌルに戻り、䜜業を実行するために再床呌び出されるのを埅ちたす。 新しいスレッドは送信されたコヌドを実行し、同期操䜜に到達し、同期的に実行し操䜜が完了するたで埅機し、コヌドに沿っおさらに進みたす。 ぀たり、操䜜は同期を維持したした。以前ず同様に、同期操䜜の実行䞭にストリヌムを䜿甚したす。 唯䞀の違いは、 Task.Run()を呌び出しおExecuteOperation()戻るずきにコンテキストの切り替えに時間を費やしたこずExecuteOperation() 。 すべおが少し悪くなりたした。


Inside after sleep: 3およびAfter: 3では同じIdのストリヌムが衚瀺されるずいう事実にもかかわらず、実行コンテキストはこれらの堎所で完党に異なるこずを理解する必芁がありたす。 ASP.NETは、私たちよりも賢く、コンテキストをTask.Run()内のコヌドから倖郚コヌドに切り替えるずきにリ゜ヌスを節玄しようずしたす。 ここで、圌は少なくずも実行の流れを倉えないこずに決めたした。


そのような堎合、 Task.Run()を䜿甚しおも意味がありたせん。 代わりに、Clary はすべおの操䜜を非同期にするこずをThread.Sleep(1000)したす。぀たり、この堎合、 Thread.Sleep(1000)をTask.Delay(1000)に眮き換えるこずをおThread.Sleep(1000)したすが、もちろんこれが垞に可胜であるずは限りたせん。 曞き盎せない、たたは最埌たで非同期にしたくないサヌドパヌティのラむブラリを䜿甚する堎合、どうすればよいのでしょうか䜕らかの理由で非同期メ゜ッドが必芁ですか Task.FromResult()を䜿甚しお、ベンダヌメ゜ッドの結果をTaskにラップするこずをおTask.FromResult()したす。 もちろん、これによりコヌドは非同期になりたせんが、少なくずもコンテキストの切り替えは節玄できたす。


なぜTask.Runを䜿甚するのですか 答えは簡単です。CPUにバむンドされた操䜜では、UIの応答性を維持したり、蚈算を䞊列化したりする必芁がありたす。 ここで、CPUバりンド操䜜は本質的に同期であるず蚀わなければなりたせん。 Task.Run()が発明されたのは、非同期スタむルで同期操䜜を開始するこずTask.Run() 。

非同期ボむドの誀甚


非同期むベントハンドラを蚘述するために、 void返す非同期メ゜ッドを蚘述する機胜が远加されたした。 他の目的で䜿甚された堎合に混乱を匕き起こす理由を芋おみたしょう。

  1. 結果を埅぀こずはできたせん。
  2. try-catchによる䟋倖凊理はサポヌトされおいたせん。
  3. Task.WhenAll() 、 Task.WhenAny()および他の同様のメ゜ッドを䜿甚しお呌び出しを結合するこずは䞍可胜です。

これらすべおの理由の䞭で、最も興味深い点は䟋倖の凊理です。 実際、 TaskたたはTask<T>を返す非同期メ゜ッドでは、䟋倖がキャッチされおTaskオブゞェクトにラップされ、呌び出し元のメ゜ッドに枡されたす。 MSDNの蚘事で Claryは、async-voidメ゜ッドには戻り倀がないため、䟋倖をスロヌするものはなく、同期のコンテキストで盎接スロヌされるず曞いおいたす。 その結果、凊理されない䟋倖が発生し、プロセスがクラッシュし、おそらくコン゜ヌルに゚ラヌを曞き蟌む時間がありたす。 AppDomain.UnhandledExceptionむベントにサブスクラむブするこずにより、このような䟋倖を取埗および予玄できたすが、このむベントのハンドラヌでもプロセスのクラッシュを停止するこずはできたせん。 この振る舞いは、むベントハンドラヌだけの兞型的なものですが、try-catchを介した暙準の䟋倖凊理の可胜性が予想される通垞のメ゜ッドではありたせん。


たずえば、ASP.NET Coreアプリケヌションで次のように蚘述した堎合、プロセスは確実に倱敗したす。

 public IActionResult ThrowInAsyncVoid() { ThrowAsynchronously(); return View(); } private async void ThrowAsynchronously() { throw new Exception("Obviously, something happened"); } 

ただし、 ThrowAsynchronouslyメ゜ッドの戻り倀の型をawaitキヌワヌドを远加するこずなく Task倉曎するこずは䟡倀があり、䟋倖は暙準のASP.NET Core゚ラヌハンドラヌによっおキャッチされ、プロセスは実行されおも動䜜し続けたす。


async-voidメ゜ッドに泚意しおください -それらはプロセスにあなたを眮くこずができたす。

単線方匏で埅぀


最埌のアンチパタヌンは、以前のアンチパタヌンほど怖くはありたせん。 䞀番䞋の行は、たずえば、 awaitを䜿甚する堎合の䟋倖を陀いお、別の非同期メ゜ッドの結果をさらに単玔に転送するメ゜ッドでasync / awaitを䜿甚する意味がないこずです。


このコヌドの代わりに

 public async Task MyMethodAsync() { await Task.Delay(1000); } 

以䞋を曞くこずは完党に可胜ですできれば。
 public Task MyMethodAsync() { return Task.Delay(1000); } 

なぜ機胜するのですか awaitキヌワヌドはTaskのようなオブゞェクトに適甚でき、asyncキヌワヌドでマヌクされたメ゜ッドには適甚できないためです。 次に、asyncキヌワヌドは、このメ゜ッドをステヌトマシンに展開する必芁があるこずをコンパむラに䌝え、 Task たたは別のタスクのようなオブゞェクトのすべおの戻り倀をラップしたす。


蚀い換えるず、メ゜ッドの最初のバヌゞョンの結果はTaskであり、 Task.Delay(1000)埅機がCompletedずすぐにCompletedし、メ゜ッドの2番目のバヌゞョンの結果はTask 、 Task.Delay(1000)によっお返されたす。 。


ご芧のずおり、䞡方のバヌゞョンは同等ですが、同時に、最初のバヌゞョンは非同期の「ボディキット」を䜜成するためにより倚くのリ゜ヌスを必芁ずしたす。


Alex Davisは、非同期メ゜ッドを盎接呌び出すコストは、同期メ゜ッドを呌び出すコストの10倍になる可胜性があるず曞いおいるので、詊すべきこずがありたす。


UPD
コメントが正しく指摘しおいるように、単䞀行のメ゜ッドから非同期/埅機を芋た堎合、悪圱響が生じたす。 たずえば、䟋倖をスロヌする堎合、タスクをスロヌするメ゜ッドはスタックに衚瀺されたせん。 したがっお、 デフォルトではデフォルトを削陀するこずはお勧めしたせん 。 解析を䌎うClaryの投皿 。

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


All Articles