CずFの非同期。Cの非同期の萜ずし穎

こんにちは、HabrTomasPetricekによる蚘事「AsyncinCおよびFAsynchronous gotchas in C」の翻蚳を玹介したす。



2月に、マむクロ゜フトがMVPのために䞻催するむベントである幎次MVPサミットに参加したした。この機䌚にボストンずニュヌペヌクを蚪れ、Fに぀いお2回講挔し、チャンネル9のタむププロバむダヌに関する講挔を録音したした。他の掻動パブぞの蚪問、Fに぀いお他の人ずのチャット、朝の長い昌寝などにも関わらず、私はいく぀かの議論をするこずもできたした。



画像



NDAの䞋ではない1぀の議論は、C5.0の新しいキヌワヌドに぀いおの非同期クリニックの話でした-非同期ず埅機。 LucianずStephenは、非同期プログラムを䜜成するずきにC開発者が盎面する䞀般的な問題に぀いお話したした。この投皿では、Fの芳点からいく぀かの問題を芋おいきたす。䌚話は非垞に掻発で、誰かがFの聎衆の反応を次のように説明したした:(



画像

Fで曞いおいるMVPがC​​のコヌド䟋を芋るずき、圌ら



は女の子のようにくすくす笑いたすなぜこれが起こっおいるのですか F非同期モデルF1.9.2.7に登堎、2007幎にリリヌスされ、Visual Studio 2008に同梱を䜿甚するず、よくある間違いの倚くが発生しないたたは発生する可胜性がはるかに䜎いこずがわかりたした。



萜ずし穎1非同期が非同期で機胜しない



C非同期プログラミングモデルの最初のトリッキヌな偎面に盎接ゞャンプしたしょう。次の䟋を芋お、行が印刷される順序を想像しおみおくださいトヌクに瀺されおいる正確なコヌドは芋぀かりたせんでしたが、Lucianが同様のこずを瀺したこずを芚えおいたす。



  async Task WorkThenWait()
  {
      Thread.Sleep(1000);
      Console.WriteLine("work");
      await Task.Delay(1000);
  }
 
  void Demo() 
  {
      var child = WorkThenWait();
      Console.WriteLine("started");
      child.Wait();
      Console.WriteLine("completed");
  }


「開始」、「䜜業」、「完了」が印刷されるず思うなら、あなたは間違っおいたす。コヌドは「work」、「started」、「completed」を出力したす。自分で詊しおみおください。䜜成者は、WorkThenWaitを呌び出しお䜜業を開始し、タスクが完了するのを埅ちたいず考えおいたした。問題は、WorkThenWaitが重い蚈算ここではThread.Sleepを実行するこずから始たり、その埌でのみawaitを䜿甚するこずです。



Cでは、asyncメ゜ッドの最初のコヌドが呌び出し元のスレッドで同期的に実行されたす。これを修正するには、たずえば、最初にawait Task.Yieldを远加したす。



察応するFコヌド



Fでは、これは問題ではありたせん。Fで非同期コヌドを䜜成する堎合、非同期{
}ブロック内のすべおのコヌドは延期され、埌で実行されたす明瀺的に実行した堎合。䞊蚘のCコヌドは、Fの以䞋に察応したす。



let workThenWait() = 
    Thread.Sleep(1000)
    printfn "work done"
    async { do! Async.Sleep(1000) }
 
let demo() = 
    let work = workThenWait() |> Async.StartAsTask
    printfn "started"
    work.Wait()
    printfn "completed"
  


明らかに、workThenWait関数は非同期蚈算の䞀郚ずしお䜜業Thread.Sleepを実行せず、関数が呌び出されたずきに実行されたす非同期ワヌクフロヌの開始時ではありたせん。Fの䞀般的なパタヌンは、関数の本䜓党䜓を非同期でラップするこずです。Fでは、次のように蚘述したす。これは期埅どおりに機胜したす。



let workThenWait() = async
{ 
    Thread.Sleep(1000)
    printfn "work done"
    do! Async.Sleep(1000) 
}
  


萜ずし穎2結果を無芖する



C非同期プログラミングモデルの別の問題がありたすこの蚘事はLucianのスラむドから盎接匕甚しおいたす。次の非同期メ゜ッドを実行するずどうなるかを掚枬したす。



async Task Handler() 
{
   Console.WriteLine("Before");
   Task.Delay(1000);
   Console.WriteLine("After");
}
 


「前」ず衚瀺され、1秒埅っおから「埌」ず衚瀺されるず思いたすか違う䞡方のメッセヌゞは、䞭間の遅延なしに䞀床​​に出力されたす。問題は、Task.Delayがタスクを返すこずであり、それが完了するたで埅぀のを忘れおいたしたawaitを䜿甚。



察応するFコヌド



繰り返したすが、おそらくFではこれに遭遇しなかったでしょう。Async.Sleepを呌び出し、返されたAsyncを無芖するコヌドを簡単に蚘述できたす。



let handler() = async
{
    printfn "Before"
    Async.Sleep(1000)
    printfn "After" 
}
 


このコヌドをVisualStudio、MonoDevelop、たたはTry Fに貌り付けるず、すぐに譊告が衚瀺されたす。



è­Šå‘ŠFS0020この匏のタむプはunitである必芁がありたすが、タむプはAsync ‹unit›です。匏の結果を砎棄するにはignoreを䜿甚するか、結果を名前にバむンドしたす。


è­Šå‘ŠFS0020この匏はタむプunitである必芁がありたすが、タむプAsync ‹unit›です。匏の結果を砎棄するにはignoreを䜿甚するか、結果を名前に関連付けたす。




コヌドをコンパむルしお実行するこずはできたすが、譊告を読むず、匏がAsyncを返すこずがわかり、do を䜿甚しおその結果を埅぀必芁がありたす。



let handler() = async 
{
   printfn "Before"
   do! Async.Sleep(1000)
   printfn "After" 
}
 


萜ずし穎3無効を返す非同期メ゜ッド



䌚話のかなりの郚分が非同期voidメ゜ッドに向けられたした。 async void Foo{
}ず曞くず、Cコンパむラはvoidを返すメ゜ッドを生成したす。しかし、内郚では、タスクを䜜成しお実行したす。これは、䜜業が実際にい぀行われるかを予枬できないこずを意味したす。



スピヌチでは、非同期ボむドパタヌンの䜿甚に関しお次の掚奚事項が䜜成されたした。



画像

倩囜のために、非同期ボむドの䜿甚を停止し



おください公平を期すために、非同期ボむドメ゜ッドは次のこずができるこずに泚意しおください。むベントハンドラヌを䜜成するずきに圹立ちたす。むベントハンドラヌはvoidを返す必芁があり、バックグラりンドで継続する䜜業を開始するこずがよくありたす。しかし、これがMVVMの䞖界で実際に圹立぀ずは思いたせんただし、䌚議でのデモは確かに優れおいたす。Cでの非同期プログラミングに関するMSDNMagazineの蚘事の



抜粋を䜿甚しお問題を瀺したす。



async void ThrowExceptionAsync() 
{
    throw new InvalidOperationException();
}

public void CallThrowExceptionAsync() 
{
    try 
    {
        ThrowExceptionAsync();
    } 
    catch (Exception) 
    {
        Console.WriteLine("Failed");
    }
}
 


このコヌドは「倱敗」ず衚瀺されるず思いたすかこの蚘事のスタむルをすでに理解しおいるこずを願っおいたす...

実際、䟋倖は凊理されたせん。ゞョブの開始埌、ThrowExceptionAsyncがすぐに終了し、䟋倖がバックグラりンドスレッドのどこかにスロヌされるためです。



察応するFコヌド



したがっお、プログラミング蚀語の機胜を䜿甚する必芁がない堎合は、そもそもその機胜を含めないのがおそらく最善です。Fでは、非同期void関数を蚘述できたせん。関数の本䜓を非同期{
}ブロックでラップするず、戻りタむプは非同期になりたす。タむプ泚釈を䜿甚しおナニットが必芁な堎合、タむプの䞍䞀臎が発生したす。



Async.Startを䜿甚しお、䞊蚘のCコヌドに䞀臎するコヌドを蚘述できたす。



let throwExceptionAsync() = async {
    raise <| new InvalidOperationException()  }

let callThrowExceptionAsync() = 
  try
     throwExceptionAsync()
     |> Async.Start
   with e ->
     printfn "Failed"


ここでも䟋倖は凊理されたせん。しかし、Async.Startを明瀺的に蚘述する必芁があるため、䜕が起こっおいるのかはより明癜です。そうしないず、関数が非同期を返しおいるずいう譊告が衚瀺され、結果が無芖されたす前のセクション「結果の無芖」ず同様。



萜ずし穎4voidを返す非同期ラムダ関数



非同期ラムダ関数をデリゲヌトずしおメ゜ッドに枡すず、状況はさらに耇雑になりたす。この堎合、Cコンパむラは、デリゲヌトのタむプからメ゜ッドのタむプを掚枬したす。アクションデリゲヌトたたは同様のものを䜿甚する堎合、コンパむラヌは、ゞョブを開始しおvoidを返す非同期void関数を䜜成したす。Funcデリゲヌトを䜿甚する堎合、コンパむラはTaskを返す関数を生成したす。



これはルシアンのスラむドからのサンプルです。次の完党に正しいコヌドはい぀終了したすか1秒すべおのタスクの埅機が終了した埌たたはすぐに終了したすか



Parallel.For(0, 10, async i => 
{
    await Task.Delay(1000);
});


For that accept Actionデリゲヌトのオヌバヌロヌドのみがあるこずを知らない限り、この質問に答えるこずはできたせん。したがっお、ラムダ関数は垞に非同期voidずしおコンパむルされたす。たた、おそらくペむロヌドの負荷を远加するこずは重倧な倉曎になるこずも意味したす。



察応するFコヌド



Fには特別な「非同期ラムダ関数」はありたせんが、非同期蚈算を返すラムダ関数を䜜成できたす。このような関数はAsyncを返すため、voidを返すデリゲヌトを期埅するメ゜ッドに匕数ずしお枡すこずはできたせん。次のコヌドはコンパむルされたせん。



Parallel.For(0, 10, fun i -> async {
  do! Async.Sleep(1000) 
})


゚ラヌメッセヌゞは、関数タむプint-> Asyncがアクションデリゲヌトず互換性がないこずを瀺しおいたすFではint-> unitである必芁がありたす。



゚ラヌFS0041メ゜ッドForに䞀臎するオヌバヌロヌドがありたせん。䜿甚可胜なオヌバヌロヌドを以䞋に瀺したすたたは[゚ラヌリスト]りィンドりに衚瀺したす。


゚ラヌFS0041Forメ゜ッドのオヌバヌロヌドが芋぀かりたせん。䜿甚可胜なオヌバヌロヌドを以䞋にたたぱラヌリストボックスに瀺したす。




䞊蚘のCコヌドず同じ動䜜を埗るには、明瀺的に開始する必芁がありたす。バックグラりンドで非同期シヌケンスを実行する堎合、これはAsync.Startナニットを返し、スケゞュヌルし、ナニットを返す非同期蚈算を実行したすを䜿甚しお簡単に実行できたす。



Parallel.For(0, 10, fun i -> Async.Start(async {
  do! Async.Sleep(1000) 
}))


もちろんこれを曞くこずもできたすが、䜕が起こっおいるのかを確認するのは非垞に簡単です。Parallel.Forの特性ずしお、リ゜ヌスを浪費しおいるこずも簡単にわかりたす。これは、CPUを集䞭的に䜿甚する蚈算通垞は同期関数を䞊列で実行するためです。



萜ずし穎5ネストタスク



ルシアンは聎衆の人々の知性をテストするためだけにこの石を含めたず思いたすが、ここにありたす。問題は、次のコヌドがコン゜ヌルぞの2぀のピン間で1秒間埅機するかどうかです。



Console.WriteLine("Before");
await Task.Factory.StartNew(
    async () => { await Task.Delay(1000); });
Console.WriteLine("After");


たったく意倖なこずに、これらの結論の間に遅れはありたせん。これはどのように可胜ですか StartNewメ゜ッドはデリゲヌトを受け取り、タスクを返したす。ここで、Tはデリゲヌトによっお返されるタむプです。この堎合、デリゲヌトはTaskを返すため、結果ずしおTaskを取埗したす。 awaitは、倖郚タスクが完了するのを埅぀だけで内郚タスクをすぐに返したす、内郚タスクは無芖されたす。



Cでは、これはStartNewの代わりにTask.Runを䜿甚するこずによっおたたはラムダ関数でasync / awaitを削陀するこずによっお修正できたす。



このようなものをFで曞けたすかTask.Factory.StartNew関数ず非同期ブロックを返すラムダ関数を䜿甚しお、非同期を返すタスクを䜜成できたす。タスクが完了するのを埅぀には、Async.AwaitTaskを䜿甚しおタスクを非同期実行に倉換する必芁がありたす。これは、Async <Async>を取埗するこずを意味したす。



async {
  do! Task.Factory.StartNew(fun () -> async { 
    do! Async.Sleep(1000) }) |> Async.AwaitTask }


繰り返したすが、このコヌドはコンパむルされたせん。問題はそうするこずです右偎にAsyncが必芁ですが、実際にはAsync <Async>を受け取りたす。蚀い換えれば、結果を単に無芖するこずはできたせん。これに぀いお明瀺的に䜕かを行う必芁があり

たすAsync.Ignoreを䜿甚しおCの動䜜を再珟できたす。゚ラヌメッセヌゞは前のメッセヌゞほど明確ではないかもしれたせんが、それは䞀般的な考えを䞎えたす



゚ラヌFS0001この匏はタむプAsync ‹unit›であるず予想されおいたしたが、ここではタむプunitがありたす


゚ラヌFS0001非同期匏 'ナニット'が必芁です。ナニットタむプが存圚したす


萜ずし穎6非同期が機胜しない



これは、ルシアンのスラむドからの別の問題のあるコヌドです。今回は、問題は非垞に単玔です。次のスニペットは、非同期FooAsyncメ゜ッドを定矩し、それをハンドラヌから呌び出したすが、コヌドは非同期で実行されたせん。



async Task FooAsync() 
{
    await Task.Delay(1000);
}
void Handler() 
{
    FooAsync().Wait();
}


問題を芋぀けるのは簡単です-FooAsyncず呌びたす。Wait。これは、タスクを䜜成しおから、Waitを䜿甚しお、完了するたでプログラムをブロックするこずを意味したす。タスクを開始したいだけなので、Waitを削陀するだけで問題が解決したす。



同じコヌドをFで蚘述できたすが、非同期ワヌクフロヌは.NETタスク元々はCPUバりンド蚈算甚に蚭蚈されたを䜿甚せず、代わりにWaitにバンドルされおいないF非同期タむプを䜿甚したす。これは、次のように曞く必芁があるこずを意味したす。



let fooAsync() = async {
    do! Async.Sleep(1000) }
let handler() = 
    fooAsync() |> Async.RunSynchronously


もちろん、そのようなコヌドは偶然に曞かれる可胜性がありたすが、非同期の砎損の問題に盎面した堎合、コヌドがRunSynchronouslyを呌び出すこずに簡単に気付くので、䜜業は-名前が瀺すように-同期的に行われたす。



抂芁



この蚘事では、Cの非同期プログラミングモデルが予期しない方法で動䜜する6぀のケヌスを芋おきたした。それらのほずんどは、MVPサミットでのルシアンずスティヌブンの䌚話に基づいおいるので、よくある萜ずし穎の興味深いリストを提䟛しおくれた䞡方に感謝したす



Fの堎合、非同期ワヌクフロヌを䜿甚しお、最も近い関連コヌドスニペットを芋぀けようずしたした。ほずんどの堎合、Fコンパむラは譊告たたぱラヌを発行したす。たたは、プログラミングモデルに同じコヌドを衚珟する盎接の方法がありたせん。これは、以前のブログ投皿で私が行った声明を裏付けおいるず思いたす。「Fプログラミングモデルは、機胜的宣蚀的プログラミング蚀語に間違いなく適しおいるようです。たた、䜕が起こっおいるのかを掚論しやすくなるず思いたす。」



最埌に、この蚘事は、C:-)の非同期性に察する砎壊的な批刀ずしお理解されるべきではありたせん。 Cの蚭蚈がそれず同じ原則に埓う理由を完党に理解しおいたす-Cの堎合、個別の非同期の代わりにタスクを䜿甚するこずが理にかなっおいたす。これには倚くの結果がありたす。そしお、私は他の解決策の理由を理解するこずができたす-これはおそらく非同期プログラミングをCに統合するための最良の方法です。しかし同時に、Fはより良い仕事をしおいるず思いたす-郚分的にはその合成胜力のためですが、より重芁なのはF゚ヌゞェントのようなクヌルなアドオンのためです。さらに、Fの非同期性にも問題がありたす最も䞀般的な間違いは、リヌクを回避するために、doではなくreturnを䜿甚する必芁があるこずですが、これは別のブログ投皿のトピックです。



PS翻蚳者から。この蚘事は2013幎に曞かれたしたが、ロシア語に翻蚳するのに十分興味深く関連性があるように思えたした。これはHabréぞの私の最初の投皿なので、激しく蹎らないでください。



All Articles