いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

.NET クラスライブラリ探訪-018 (AsyncOperation, AsyncOperationManager, SynchronizationContext)(コンテキスト,コンテキストの同期,非同期処理)


BackgroundWorker使った事ありますか?
TimerコンポーネントやFileSystemWatcherコンポーネント使った事ありますか?

これらのコントロールにイベントハンドラを設定しておいたらしかるべきタイミングで
きっちりメッセージスレッドでイベントが発生するようになっています。

普通に気にせずにつかっていると気にも留めない事ですが気になって調べだすと
以下のクラス達が出てきます。

  • System.ComponentModel.AsyncOperation
  • System.ComponentModel.AsyncOperationManager
  • System.Threading.SynchronizationContext
  • System.Windows.Forms.WindowsFormsSynchronizationContext
  • System.Windows.Threading.DispatcherSynchronizationContext


で、これらのクラスの説明を見てもあんまり分からないんですよね。w
AsyncOperationクラス, AsyncOperationManagerクラスに関しては
MSDNの「非同期処理を行うコンポーネントの実装」にもちょいと出てきます。

ちゃんと纏めて日記書こうと思ったんですが・・・
すみません、以下思いっきりメモ書きのままです。
(まとめるのが面倒くさくなったww)


■SynchronizationContextはコンテキストの同期を行う為に利用される。
  ■コンテキストの同期とは、対象となる処理を適切な場所にて走らせる為の同期の事。
    ■コンテキストは日本語で文脈という意味。
      ■つまり対象の処理を適切な文脈の位置で必ず処理させるという事。
    ■例えば、別スレッドで処理していて一部の処理のみメッセージスレッドで走らせる等。
      ■Control.Invokeはこれを行っている。
  ■SynchronizationContextは、スレッドに紐づく。
    ■SynchronizationContextには以下の派生クラスが存在する。
      ⇒System.Windows.Forms.WindowsFormsSynchronizationContext  (WindowsForms用)
      ⇒System.Windows.Threading.DispatcherSynchronizationContext (WPF用)
      ■それぞれの派生クラスは、基本機能に加え、各自独自の動作とプロパティを持っている。
    ■Windows Formsでは、デフォルトで最初のコントロールがnewされた際に自動的に
     メインスレッド(メッセージスレッド)にWindowsFormsSynchronizationContextが読み込まれる。
      ■自動的に読み込まれるのを止めたい場合は
         WindowsFormsSynchronizationContext.AutoInstall = false とする。
  ■SynchronizationContextは新たにスレッドを自前で作成した際はnullとなっている。
  ■新たに対象のスレッドにSynchronizationContextを設定する場合は
    SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); とする。
  ■SynchronizationContextは
    SynchronizationContext.CreateCopy();
   とすることで、コピーする事ができる。
   ■AsyncOperationManagerはCreateOperationする際に内部で現在のSynchronizationContextを
    コピーしているものと思われる。
  ■SynchronizationContextには、PostとSendというメソッドが存在し、
    Send : 処理が終わるまで待つ(同期)
    Post : 処理の終わりを待たない(非同期)
   となっている。
  ■SynchronizationContextはいろいろな場所で利用されており、以下のコントロールの動作も
   そのコントロールにSynchronizationContextが設定されているから行えるようになっている。
    ⇒Timerコンポーネント
    ⇒FileSystemWatcherコンポーネント
   つまり、非同期で処理を行うがイベントの発生事態はメッセージスレッドにて発生している
   コントロールには、プロパティとしてSynchronizationContextを公開している。
   (デザイナにて配置した時点で設定されている)
  ■現在のSynchronizationContextは、
    SynchronizationContext.Currentで取得できる。
  ■特定のスレッドのSynchronizationContextを別のスレッドにも連携する場合は
   あらかじめコピーを作成しておき、それを別のスレッドに連携して、その別のスレッド
   の内部でSetSynchronizationContextを利用して設定する。
  ■SynchronizationContextとWindowsFormsSynchronizationContextではPost,Sendした際の
   挙動が異なる。
    WindowsFormsSynchronizationContext : このSynchronizationContextが関連付いているスレッド(つまりメッセージスレッド)
    SynchronizationContext       : 新たにThreadPoolスレッドを作成してそこで処理。(ManagedThreadIdが異なる。)
■BackgroundWorkerコンポーネントも裏でSynchronizationContextを利用している。
  ■以下のような委譲関係となっている。
    ⇒BackgroundWorker => AsyncOperation => SynchronizationContext
  ■最終的にSynchronizationContextがコンテキストの同期処理を行っているので
   BackgroundWorkerのRunWorkerCompletedやProgressChangedは呼び出し元と同じ
   スレッドで処理される事となる。
   (つまり、メッセージスレッドでBackgroundWorkerを作成していたら、メッセージスレッド、別スレッドで作成していたらそのスレッド。)
■AsyncOperationクラスは、非同期処理を行いつつ、コンテキストの同期処理が必要な場合に
 利用される。
  ■つまり、画面との連携が存在し、一部の処理を非同期で行い終了したタイミングで
   元のスレッドにて処理を行う際に利用される。
  ■BackgroundWorkerや非同期処理を行えるコンポーネントで利用されている。
  ■AsyncOperationは、AsyncOperationManagerクラスより生成できる。
    ■このクラスのCreateOperationメソッドよりAsyncOperationオブジェクトが生成される。
    ■AsyncOperationManagerはCreateOperationメソッドが呼ばれた際に現在の
  ■AsyncOperatioManagerはCreateOperationメソッドが呼ばれた際に現在のSynchronizationContextを
   コピーしてAsyncOperationのコンストラクタに渡している模様。なので、AsyncOperationのPost,PostOperationCompletedが
   正確にメッセージスレッドに同期できるかどうかは、このAsyncOperationオブジェクトを作成した時点でのスレッドに
   適切なSynchronizationContextがあるかどうかによる。
   (新規でスレッドを作成した場合はSynchronizationContextはnullとなっている。)
■まとめ
  ■AsyncOperationは作成された場所が大事。
    ⇒作成された時点のSynchronizationContextが内部に設定されるため。
      ⇒つまりBackgroundWorkerの場合も作成される場所が大事となる。


// vim:set ts=4 sw=4 et ws is nowrap ft=cs:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;

namespace Demo{

    public class DemoForm : Form{

        public DemoForm(){
            //
            // 現在のスレッドに名前を付与.
            //
            Thread.CurrentThread.Name = "Main Thread";

            InitializeComponent();
        }

        protected void InitializeComponent(){
            SuspendLayout();

            Text = "SynchronizationContext Sample.";
            Size = new Size(400, 400);
            StartPosition = FormStartPosition.CenterScreen;

            Button b1 = new Button{Name="btn01", Text="メッセージスレッドでAsyncOperation作成"};
            Button b2 = new Button{Name="btn02", Text="別スレッドでAsyncOperation作成"};

            b1.Width = 380;
            b2.Width = 380;

            b1.Click += (s, e) => {
                MessageBox.Show(GetCurrentThreadName("Clickハンドラの最初"));

                // この場所でAsyncOprationを作成するとWindowsFormsSynchronizationContextオブジェクトになる。
                // (現在のスレッドのコンテキストがWindows Formsのため)
                // (WindowsFormsSynchronizationContext.AutoInstallの値がデフォルトでtrueになっているので、最初のコントロールが
                //  newされた時にメッセージスレッド (ここではMain Thread)にWindowsFormsSynchronizationContextが読み込まれる)
                //
                // SynchonizationContextはSystem.Threading名前空間に存在し、以下の派生クラスを持つ。
                //      ⇒System.Windows.Forms.WindowsFormsSynchronizationContext   (WindowsForms用)
                //      ⇒System.Windows.Threading.DispatcherSynchronizationContext (WPF用)
                // それそれの派生クラスは、基本機能に加え、各自独自の動作とプロパティを持っている。
                AsyncOperation asyncOp = AsyncOperationManager.CreateOperation(s);

                Thread t = new Thread(() => {
                    MessageBox.Show(GetCurrentThreadName("別スレッド開始"));
                    MessageBox.Show(string.Format("別スレッドのSynchronizationContext={0}", SynchronizationContext.Current));

                    // ここで表示されるSynchronizationContextは, AsyncOperationがどのスレッドで作成されたかによって
                    // 出力される値が変わる。
                    //
                    // ■イベントハンドラ側でAsyncOperationが生成された場合:WindowsFormsSynchronizationContext
                    // ■別スレッド側でAsyncOperationが生成された場合:      SynchronizationContext
                    MessageBox.Show(string.Format("AsyncOpに紐付くSynchronizationContext={0}", asyncOp.SynchronizationContext));

                    // Post及びPostOperationCompletedメソッドの呼び出しは、実際にはAsyncOperationが内部で保持しているSynchronizationContext.Postを
                    // 呼び出しているので、対象となるSynchronizationContextによって同期されるスレッドが異なる。
                    //
                    // ■イベントハンドラ側でAsyncOperationが生成された場合:メッセージスレッド側に同期 (Main Thread)
                    // ■別スレッド側でAsyncOperationが生成された場合:      新たにスレッドが作成されその中で処理 (Thread Pool)
                    asyncOp.Post((state) => {
                        MessageBox.Show(GetCurrentThreadName("AsyncOp.Post"));
                    }, asyncOp);

                    asyncOp.PostOperationCompleted((state) => {
                        MessageBox.Show(GetCurrentThreadName("AsyncOp.PostOpearationCompleted"));
                    }, asyncOp);
                });

                t.Name = "Sub Thread";
                t.IsBackground = true;
                t.Start();
            };

            b2.Click += (s, e) => {
                MessageBox.Show(GetCurrentThreadName("Clickハンドラの最初"));


                Thread t = new Thread(() => {
                    MessageBox.Show(GetCurrentThreadName("別スレッド開始"));

                    MessageBox.Show(string.Format("別スレッドのSynchronizationContext(AsyncOperation作成前)={0}", SynchronizationContext.Current));
                    AsyncOperation asyncOp = AsyncOperationManager.CreateOperation(s);
                    MessageBox.Show(string.Format("別スレッドのSynchronizationContext(AsyncOperation作成後)={0}", SynchronizationContext.Current));

                    MessageBox.Show(string.Format("AsyncOpに紐付くSynchronizationContext={0}", asyncOp.SynchronizationContext));

                    asyncOp.Post((state) => {
                        MessageBox.Show(GetCurrentThreadName("AsyncOp.Post"));
                    }, asyncOp);

                    asyncOp.PostOperationCompleted((state) => {
                        MessageBox.Show(GetCurrentThreadName("AsyncOp.PostOpearationCompleted"));
                    }, asyncOp);
                });

                t.Name = "Sub Thread";
                t.IsBackground = true;
                t.Start();
            };

            FlowLayoutPanel contentPane = new FlowLayoutPanel();
            contentPane.Dock = DockStyle.Fill;

            contentPane.Controls.AddRange(new Control[]{b1, b2});
            Controls.Add(contentPane);

            ResumeLayout();
        }

        string GetCurrentThreadName(string prefix){
            Thread curThread = Thread.CurrentThread;
            return string.Format("{0} : {1}-{2} IsThreadPool={3}", prefix, curThread.Name, curThread.ManagedThreadId, curThread.IsThreadPoolThread);
        }
    }

    class EntryPoint{
        
        [STAThread]
        static void Main(){
            // 以下のコメントを外す事でコントロールが最初にnewされた際に
            // WindowsFormsSynchronizationContextが読み込まれないように出来ます。
            // falseにすると、デフォルトでSynchronizationContextが読み込まれます。
            //WindowsFormsSynchronizationContext.AutoInstall = false;
            
            Application.EnableVisualStyles();
            Application.Run(new DemoForm());
        }
    }
}


認識違いしているところが多々ありそうですが、一応記述。
間違っているところあったら教えてください。m(_ _)m

参照したリソース:


================================
過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。