C#とSynchronizationContextの非同期

継続: パートIII



非同期についての前回の投稿( パートI )は紹介です。 これで、私が始めたトピックを続けます。非同期がSynchronizationContextとどのように相互作用し、これが非同期グラフィックスアプリケーションの開発にどのように影響するかについて説明します。



テストサイトはDiningPhilosophersの例で、非同期プログラミングの拡張機能が付属しています。 このプログラムは、ダイクストラの有名な哲学者の食事の視覚化です( リンク )。 さらに読む前に、問題の状態をよく理解することをお勧めします。



画像



プログラムは短いので、レビューしてみましょう。 初めに、問題の主な本質を表すクラスが定義されます:哲学者、哲学者、およびフォークの状態。



BufferBlockクラスはフォークとして使用され、System.Threading.Tasks.Dataflow名前空間にあります。 このスペースにより、データフローに基づいてマルチスレッドアプリケーションを作成できます。 このアプローチの最も簡単な例は、Limbo、Go、Axumで使用されるチャネルです。 その本質は、2つのストリームがチャネル(キューの類似物)を介して相互作用し、書き込みと読み取りができることです。ストリームがチャネルから読み取ろうとしてチャネルが空の場合、データがチャネルに表示されるまでストリームはブロックされます。 一般的なオブジェクトを拒否し、データ交換および同期ツールにチャネルを使用すると、より理解しやすく安全なコードを作成できます。 BufferBlockはそのようなチャネルであり、Postメソッドはデータを追加し、Receiveは受信し、ReceiveAsyncはデータを非同期的に受信する拡張メソッドです。 問題の本質はこのクラスに完全にあります:アクセス可能なフォークは、何かがあるチャネル、空のチャネルによるビジーフォーク、哲学者が実行のフローである場合、フリーフォーク(チャネル)にアクセスすると実行を継続し、ビジーなフォークになります待つ



このプログラムでは、哲学者クラスは哲学者自身ではなく、視覚化された彼の状態を表しています。 この場合、これは標準のWPFプリミティブです-楕円と哲学者の異なる状態は異なる色で表されます。 これはグラフィックオブジェクトであるため、1つのストリームからのみアクセスできることが重要です。



すでに書いたように、哲学者自身は実行のスレッド(RunPhilosopherAsyncメソッド)で表されます。



using Philosopher = Ellipse; using Fork = BufferBlock<bool>;
      
      





MainWindowメソッドは実際には興味深いものではなく、クラス構造がその中で初期化されます。 彼について気づくことができる唯一のことは、彼が署名に非同期voidを含むメソッドを呼び出すことです。そのようなメソッドを呼び出すことで、非同期操作を起動し、その制御を失います。たとえば、完了するまで待つことができません。



 public MainWindow() { for (int i = 0; i < philosophers.Length; i++) { diningTable.Children.Add(philosophers[i] = CreatePhilosopher()); forks[i] = new Fork(); forks[i].Post(true); } //      for (int i = 0; i < philosophers.Length; i++) { RunPhilosopherAsync( philosophers[i], i < philosophers.Length - 1 ? forks[i] : forks[1], i < philosophers.Length - 1 ? forks[i + 1] : forks[i] ); } }
      
      





RunPhilosopherAsyncコードは哲学者の行動を説明します。特に非同期の場合はかなり簡単です。考えて、フォークを待って、食べて、フォークを戻して、もう一度考え直してください。 一時停止(TaskEx.Delay)は、さまざまな段階を観察できるように配置されています。



 private async void RunPhilosopherAsync(Philosopher philosopher, Fork fork1, Fork fork2) { // ,  , ,     while (true) { //  () philosopher.Fill = Brushes.Yellow; await TaskEx.Delay(_rand.Next(10) * TIMESCALE); //   () philosopher.Fill = Brushes.Red; await fork1.ReceiveAsync(); await fork2.ReceiveAsync(); //  () philosopher.Fill = Brushes.Green; await TaskEx.Delay(_rand.Next(10) * TIMESCALE); //       fork1.Post(true); fork2.Post(true); } }
      
      





このコードはどうですか?



まず、哲学者の数が次のような理由で2人の場合、機能しません(デッドロック)。



 i < philosophers.Length - 1 ? forks[i] : forks[1]
      
      





fork [1]をfork [0]に置き換える必要があります。 しかし、これはちょっとした選択です;哲学者の数を5人にします。



第二に、別のスレッドからgui要素にアクセスしているため、コードは機能しませんが、最も奇妙なことは機能することです。 コードのasync / awaitを取り除き、「await x」を「x.Wait()」に置き換え、「RunPhilosopherAsync(...)」を「new Task(()=> RunPhilosopherAsync(...))」に置き換えます。 Start()」および非同期トークンを削除すると、予期されるInvalidOperationExceptionが発生します。



画像



今、すべての魔法が待っていることは明らかです。



.Net FrameworkのGUIストリームとの相互作用


.Net FrameworkのGUIスレッドとやり取りするのが慣習的であることを思い出してください。



WinFormsの場合、コントロールがInvokeメソッドを呼び出して、GUIスレッドで実行する必要があるコードとともにデリゲートを渡すだけで十分です。 WPFの場合、コントロールのDispatcherプロパティにアクセスし、Invokeメソッドを呼び出して、再度デリゲートを渡す必要があります。



これは、クリストファーの古典的なパターンの定義の始まりを思い起こさせます。それは、繰り返し繰り返される問題です。 初期化子が別のスレッドからのコードである場合、問題自体は特定のスレッドでコードを実行する必要があると説明できます。 実際、この問題はguiに固有のものではなく、COMおよびWCFでも表示されます...解決策があるはずです。スレッドは、WinFormsおよびWPFコントロールと同様に、その中のコードを実行する手段を提供する必要があります。 このような解決策があります。SynchronizationContext型のオブジェクトは、別のスレッドでコードを実行するためのSendおよびPost(非同期)メソッドを提供します。 スレッドのSynchronizationContext型のオブジェクトにアクセスするには、このスレッドのSynchronizationContext.Current(スレッドシングルトンごと)にアクセスする必要があります。



SynchronizationContextとストリームが一貫していることが重要です。たとえば、ストリームがイベントループに基づいている場合、SynchronizationContextはイベントキューにアクセスできる必要があります。 つまり、各イベントループの実装は、SynchronizationContextメソッドを継承およびオーバーライドする必要があります。 SynchronizationContextオブジェクトを作成すると、SendおよびPostを呼び出すときに、ThreadPoolのスレッドが使用されます。



これで、非同期操作awaitの結果は、最初の呼び出しで受け取るSynchronizationContextオブジェクトを使用して実行されると想定できます。 この仮説は簡単に確認できます。SynchronizationContextを設定します。



 class MyContext : SynchronizationContext { public override void Post(SendOrPostCallback d, object state) { Console.WriteLine("MyContext.Post"); base.Post(d, state); } public override void Send(SendOrPostCallback d, object state) { Console.WriteLine("MyContext.Send"); base.Send(d, state); } } class Program { static async Task SavePage(string file, string a) { using (var stream = File.AppendText(file)) { var html = await new WebClient().DownloadStringTaskAsync(a); await stream.WriteAsync(html); } } static void Main(string[] args) { SynchronizationContext.SetSynchronizationContext(new MyContext()); var task = SavePage("habrahabr", "http://habrahabr.ru"); task.Wait(); } }
      
      





起動時に、「MyContext.Post」が数回表示されるため、awaitは本当にSynchronizationContextにアクセスします。



おわりに


awaitが職場でSynchronizationContextを使用するという事実は、グラフィック要素にアクセスしているスレッドを心配する必要がないため、非同期グラフィックアプリケーションの作成をさらに簡単にします。



ところで、SynchronizationContextは、シングルトンが絶対的な悪ではない場合の良い例です。



11月1日のikvm.netブログ投稿で、このメモを書くように促されました。 また、「SynchronizationContextを理解する」という記事( パートIパートII 、およびパートIII )は、このトピックを理解するのに役立ちました。



All Articles