Windows Formsで UIスレッドかどうか判定する。Control.Invokeは何をし、InvokeRequiredは何をどう調べているのか？

#はじめに
Windows Forms アプリケーションなどでマルチスレッドアプリケーションを作成するときの注意として、フォームやコントロールへは、それらを作成したスレッドからアクセスする必要があります。

#お題のアプリケーション
こんな感じのフォームで、1秒ごとに現在時刻を更新表示するアプリケーションを作成してみます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100206/b44e4341-fbf1-313c-1c4a-6815607590fa.png)

フォームを作成したスレッドからタスクを生成（＝別のスレッド）して更新表示をさせるしくみの、こんな感じのコードを実行させると、タスク内でラベルコントロール (label1) のテキストを更新しようとしたとき「[InvalidOperationException](https://docs.microsoft.com/ja-jp/dotnet/api/system.invalidoperationexception) : 有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール 'label1' がアクセスされました。」例外が発生してしまいます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100206/eb6f3814-c41a-66e5-3d2f-2405d1adb0da.png)

```C#:Form1.cs
using System;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp2
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);

            Task.Run(() => 
            {
                for(; ; )
                {
                    label1.Text = DateTime.Now.ToString("HH:mm:ss"); // <- 例外発生
                    System.Threading.Thread.Sleep(1000);
                }
            });
        }
    }
}

```

専ら作業用に作成したスレッド（ワーカースレッド）から、フォームやコントロールなどが属するスレッド（UIスレッド）に処理をさせるには、[Control.Invoke](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.forms.control.invoke) メソッドを使用する必要があり、ラベルコントロールのTextプロパティへ書き込む処理のコードを次のように変更するとうまく動きます。

```C#
            Task.Run(() =>
            {
                for (; ; )
                {
                    Invoke((MethodInvoker)delegate
                    {
                        label1.Text = DateTime.Now.ToString("HH:mm:ss");
                    });
                    System.Threading.Thread.Sleep(1000);
                }
            });
```

果たして、この Invoke メソッドは、どんなことをしているのでしょうか？

##Invokeメソッドの処理
.NET Framework (4.7.2) のソースコードを読んでみました。かなりおおまかに書くと次のことをしています。

1. Win32API の [GetWindowsThreadProcessId](https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getwindowthreadprocessid) 関数を使用して、アクセスしようとしているスレッドと、対象のコントロール（ハンドルが作成されていなければその親コントロール）の属するスレッドが一致しているか調べる。
2. 処理するメソッド情報を溜めるキューにメソッド情報（メソッド、引数、実行コンテキスト、戻り値や発生した例外の受け取り場所、[System.IAsyncResult](https://docs.microsoft.com/ja-jp/dotnet/api/system.iasyncresult) をインプリメント）を追加し、1.でスレッドが異なればウインドウメッセージ（スレッドコールバック用：4.7.2では"WindowsForms12_ThreadCallbackMessage"）を POST する。1.で同じスレッドであれば、自分自身でキューの中身を取り出してメソッドを呼び出す（キューが空になるまで繰り返す）。
3. たったいまキューに追加した対象のメソッドが終了するまで待機（[IAsyncResult.AsyncWaitHandle](https://docs.microsoft.com/ja-jp/dotnet/api/system.iasyncresult.asyncwaithandle)）し、その戻り値を返す。

POSTされた先のUIスレッドのメッセージポンプ（WndProc）では、スレッドコールバック用のウインドウメッセージを受け取ったら、キューを取り出してメソッドを（キューが空になるまで繰り返し）呼び出します。

ちなみに [Control.InvokeRequired](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.forms.control.invokerequired) プロパティは、上記のうち 1. の処理を行い、同じスレッドであれば false、異なれば true を返します。

#おわりに
ちょっと興味がありましたので、調べてみました。.NET Framework のソースコードを簡単に調べられる、Visual Studio の拡張機能「[Ref12](https://marketplace.visualstudio.com/items?itemName=SchabseSLaks.Ref12)」にはいつもお世話になっています。