いろいろ備忘録日記

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

.NET クラスライブラリ探訪-040 (System.Windows.Forms.WindowsFormsSynchronizationContext)(SynchronizationContext, 同期コンテキスト, Send, Post)


System.Windows.Forms.WindowsFormsSynchronizationContextは、SynchronizationContextクラスの派生クラスです。
Windows Formsにて、同期コンテキストを処理する際に裏で利用されています。


どんな役割を担っているのかをざっくりと言うと

別スレッドで動作している処理から、特定のコンテキスト(スレッド)上で処理が動くようにしてくれる機能

と思っていると分かりやすいかと思います。


この概念、UIを持つアプリの場合はすごく重要になります。
なぜなら、Windows FormsもWPFJava Swingも、以下の決まり事があるからです。

UIを更新できるのは、UIスレッドからしか出来ない。

このあたりの事情に関しては、Java(一つは.NET)の記事になりますが
昔書いたので、よろしければご参照下さい。


全ての処理をUIスレッド上で行えば問題ありませんが、その代わり時間がかかる処理の場合は
UIがフリーズする事になります。なので、どうしてもマルチスレッドで処理する必要がある
部分が出てきます。そのようなときにSynchronizationContextが裏で頑張ってくれています。


SynchronizationContextやAsyncOperationManagerなどの同期コンテキスト周りに関しては

にて、記述していますので、ご参考までにどうぞ。


SynchronizationContextは、各機能毎に派生クラスが用意されており
Windows Formsの場合は前述したWindowsFormsSynchronizationContextクラスとなり
WPFSilverlightの場合はDispatcherSynchronizationContextとなります。


これらのクラスは、非同期処理を行う際に裏で頑張ってくれているので
普段は見ることがありませんが、.NETアプリを作成する上で常に裏側で
存在していますので、知っておくと便利です。


WindowsFormsSynchronizationContextは、通常最初のフォームが作成された
タイミングで、自動的に作成されインストールされます。
あまり無いとは思いますが、以下のようにするとこの挙動を変更することも出来ます。

WindowsFormsSynchronizationContext.AutoInstall = false;

上記のようにすると、自動的にインストールされなくなります。
その場合、初期時は既定のSynchronizationContextとなります。



WindowsFormsSynchronizationContextクラスの基底クラスである
SynchronizationContextクラスには、以下のメソッドがあります。

  • Send
  • Post

Sendメソッドは、指定されたデリゲートを紐づくコンテキストに対して同期にキュー登録します。
Postメソッドは、指定されたデリゲートを紐づくコンテキストに対して非同期でキュー登録します。


WindowsFormsSynchronizationContextの場合、コンテキストが対応するのは
UIスレッドとなりますので、上記のメソッドはそのまま

  • SendMessage
  • PostMessage

と同じような動きになります。


以下、サンプルです。
サンプルでは、各イベントハンドラにてSendとPostを発行して
どのタイミングで出力が行われるかを見ています。

    #region WindowsFormsSynchronizationContextSamples-01
    /// <summary>
    /// WindowsFormsSynchronizationContextクラスについてのサンプルです。
    /// </summary>
    /// <remarks>
    /// WindowsFormsSynchronizationContextは、SynchronizationContextクラスの派生クラスです。
    /// デフォルトでは、Windows Formsにて、最初のフォームが作成された際に自動的に設定されます。
    /// (AutoInstall静的プロパティにて、動作を変更可能。)
    /// </remakrs>
    public class WindowsFormsSynchronizationContextSamples01 : IExecutable
    {
        class SampleForm : Form
        {
            public string ContextTypeName { get; set; }
        
            public SampleForm()
            {
                Load += (s, e) =>
                {
                    //
                    // UIスレッドのスレッドIDを表示.
                    //
                    PrintMessageAndThreadId("UI Thread");
                    
                    //
                    // 現在の同期コンテキストを取得.
                    //   Windows Formsの場合は、WinFormsSynchronizationContextとなる。
                    //
                    SynchronizationContext context = SynchronizationContext.Current;
                    ContextTypeName = context.ToString();
                    
                    //
                    // Sendは、同期コンテキストに対して同期メッセージを送る。
                    // Postは、同期コンテキストに対して非同期メッセージを送る。
                    //
                    // つまり、SendMessageとPostMessageと同じ.
                    //
                    context.Send((obj) => { PrintMessageAndThreadId("Send"); }, null);
                    context.Post((obj) => { PrintMessageAndThreadId("Post"); }, null);
                    
                    //
                    // UIスレッドと関係ない別のスレッド.
                    //
                    Task.Factory.StartNew(() => { PrintMessageAndThreadId("Task.Factory"); });
                    
                    PrintMessageAndThreadId("Form.Load");
                    Close();
                };
                
                FormClosing += (s, e) => 
                {
                    //
                    // SendとPostを呼び出し、どのタイミングで出力されるか確認.
                    //
                    SynchronizationContext context = SynchronizationContext.Current;
                    context.Send((obj) => { PrintMessageAndThreadId("Send--2"); }, null);
                    context.Post((obj) => { PrintMessageAndThreadId("Post--2"); }, null);
                    
                    //
                    // UIスレッドと関係ない別のスレッド.
                    //
                    Task.Factory.StartNew(() => { PrintMessageAndThreadId("Task.Factory"); });
                    
                    PrintMessageAndThreadId("Form.FormClosing");
                };
                
                FormClosed += (s, e) =>
                {
                    //
                    // SendとPostを呼び出し、どのタイミングで出力されるか確認.
                    //
                    SynchronizationContext context = SynchronizationContext.Current;
                    context.Send((obj) => { PrintMessageAndThreadId("Send--3"); }, null);
                    context.Post((obj) => { PrintMessageAndThreadId("Post--3"); }, null);

                    //
                    // UIスレッドと関係ない別のスレッド.
                    //
                    Task.Factory.StartNew(() => { PrintMessageAndThreadId("Task.Factory"); });
                    
                    PrintMessageAndThreadId("Form.FormClosed");
                };
            }
            
            private void PrintMessageAndThreadId(string message)
            {
                Console.WriteLine("{0,-17}, スレッドID: {1}", message, Thread.CurrentThread.ManagedThreadId);
            }
        }
        
        [STAThread]
        public void Execute()
        {
            //
            // SynchronizationContextは、同期コンテキストを様々な同期モデルに反映させるための
            // 処理を提供するクラスである。
            //
            // 派生クラスとして以下のクラスが存在する。
            //     ・WindowsFormsSynchronizationContext   (WinForms用)
            //     ・DispatcherSynchronizationContext     (WPF用)
            //
            // 基本的に、WinFormsもしくはWPFを利用している状態で
            // UIスレッドとは別のスレッドから、UIを更新する際に裏で利用されているクラスである。
            // (BackgroundWorkerも、このクラスを利用してUIスレッドに更新をかけている。)
            //
            // 現在のスレッドのSynchronizationContextを取得するには、Current静的プロパティを利用する。
            // 特定のSynchronizationContextを強制的に設定するには、SetSynchronizationContextメソッドを利用する。
            //
            // デフォルトでは、独自に作成したスレッドの場合
            // SynchronizationContext.Currentの戻り値はnullとなる。
            //
            Console.WriteLine(
                "現在のスレッドでのSynchronizationContextの状態:{0}", 
                SynchronizationContext.Current == null
                    ? "NULL"
                    : SynchronizationContext.Current.ToString()
            );
            
            //
            // フォームを起動し、値を確認.
            //
            Application.EnableVisualStyles();
            
            SampleForm aForm = new SampleForm();
            Application.Run(aForm);
            
            Console.WriteLine("WinFormsでのSynchronizationContextの型名:{0}", aForm.ContextTypeName);
        }
    }
    #endregion


実行結果は以下のようになります。

  現在のスレッドでのSynchronizationContextの状態:NULL
  UI Thread        , スレッドID: 1
  Send             , スレッドID: 1
  Form.Load        , スレッドID: 1
  Post             , スレッドID: 1
  Send--2          , スレッドID: 1
  Form.FormClosing , スレッドID: 1
  Post--2          , スレッドID: 1
  Send--3          , スレッドID: 1
  Form.FormClosed  , スレッドID: 1
  Task.Factory     , スレッドID: 4
  Task.Factory     , スレッドID: 4
  Task.Factory     , スレッドID: 4
  Post--3          , スレッドID: 1
  WinFormsでのSynchronizationContextの型名:System.Windows.Forms.WindowsFormsSynchronizationContext


これを見ると、Sendはすぐに実行されており(同期)、Postは逆にイベントが終わった後に実行されているのが分かります。(非同期)


ちなみに、コンソールアプリの場合は、既定のSynchronizationContextしか所持していません。
あとWindowsサービスアプリの場合同様です。
ASP.NETの場合は、AspNetSynchronizationContextというクラスが同期コンテキストを処理しています。


また、タスク並列ライブラリ(TPL)には、同期コンテキストを利用するタスクを作成する機能が
元々備わっています。なので、今後のバージョンではSynchronizationContextをわざわざ利用することは
少なくなるのでしょうね。

// 現在のSynchronizationContextを利用するTaskSchedulerを取得
TaskScheduler scheduler = TaskScheduler.FromCurrentSynchronizationContext();

// タスク作成
Task t = Task.Factory.StartNew(() => 
            {
                //
                // この中の処理は、UIスレッドで処理される。
                //
            }, 
            CancellationToken.None, 
            TaskCreationOptions.None, 
            scheduler
        );


【余談】
BackgroundWorkerを利用されたことがある人は多いと思いますが
何故、DoWorkハンドラは別スレッドで動いて、RunWorkerCompletedハンドラは
UIスレッドで動くのか?って疑問を感じたことないでしょうか。


それは、BackgroundWorkerが内部でSynchronizationContextを利用して
UIスレッド上で処理が動作するようにしているからです。
(実際にはAsyncOperation周りを利用していると思われますが。)


これが分かると、何故BackgroundWorkerを入れ子にして利用すると
UIを更新する際におかしくなるかが分かります。
以下のようなコードがあったとします。

// 現在UIスレッド上とする.
var worker = new BackgroundWorker();
worker.DoWork += (s, e) =>
    {
        //
        // この中は、別スレッドで処理される
        //
        
        // 2つ目のBackgroundWorkerを作成.
        // このとき、2つ目のBackgroundWorkerは作成時に
        // 現在のスレッドに紐付いているSynchronizationContextをキャプチャして保持する.
        // その際、現在のスレッドはUIスレッドではないので
        // 取得されるSynchronizationContextは、既定のSynchronizationContextとなる。
        // なので、2つ目のBackgroundWorkerのRunWorkerCompletedにてUIを更新しようと
        // すると、最悪例外が発生する。
        var worker2 = new BackgroundWorker();
        worker2.DoWork += (...);
        worker2.RunWorkerCompleted += (ss, ee) => 
            {
                //
                // この中の処理が、UIスレッドで走らない.
                //
            };
    };
worker.RunWorkerCompleted += (s, e) =>
    {
        //
        // この中は、UIスレッドで処理される。
        //
    };

worker.RunWorkerAsync();

一つ目のBackgroundWorkerは、UIスレッド上で作成されているので
キャプチャしたSynchronizationContextは、WindowsFormsSynchronizationContextとなります。
なので、RunWorkerCompletedの時にちゃんとUIスレッドにて実行されます。


それに対して、2つ目のBackgroundWorkerは、別スレッド上で作成されているので
キャプチャしたSynchronizationContextは、既定のSynchronizationContextとなります。
なので、RunWorkerCompletedの時にちゃんとUIスレッドで実行されていません。
このような状態で、UIを更新すると最悪意味不明な例外が発生する可能性があります。




参考にしたリソースは以下です。

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

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