WPFの非同期復帰の実装を覗いてみた（Async/Await/Dispatch.BeginInvoke）

「未だにWPF使ってるのかよー」って感じですが許してください…UWPはSandbox環境の制約が面倒すぎるのです。
ここでは以下のように言葉を使います。

* UIスレッド,メインスレッド := Mainメソッドが叩かれたスレッド
* ワーカースレッド := メインスレッド以外のスレッド

# 非同期処理の要、`Dispatch.BeginInvoke`

`Dispatch.BeginInvoke`はUIスレッド以外でUIスレッドでしか呼べないメソッドを呼び出したいときに使います。

```csharp
protected override void OnInitialized(EventArgs e)
{
  // メインスレッド
  new Thread(() =>
  {
     // ワーカースレッド
     for(int i = 0; i < 100000000; i++) result += result; // ハチャメチャ重い処理
     Dispatch.BeginInvoke(() => // スレッドセーフでないUI操作をキュー
     {
       // メインスレッド
       DisplayToUI(); // UIスレッドでしか呼べないメソッドを呼び出し
     });
     // ワーカースレッド
  });
}
```

WPFをはじめGUIプラットフォームの多くは、UIをメインスレッドからしか実行できない仕組みになっています。（スレッドセーフでない）
しかし、どうしても次の画面再描画に間に合わない（1/60秒で終わらない）ほど長い処理というものは出てきます。メインスレッドがそういった処理に占有されている間、GUIを書き直すことができないので所謂「フリーズ」を起こしてしまいます。

このような問題を回避するために、GUIプラットフォームでは重たい処理は別スレッドに回すというアプローチが見られます。そうすることでUIスレッドを遊ばせることができ、フリーズを回避できます。

もっとも単純な実装のイメージは次のようになります。

```csharp
static void Main(string[] _)
{
  var uiThreadQueue = new Queue<Action>();
  // メインスレッド
  new Thread(() =>
  {
     // ワーカースレッド
     for(int i = 0; i < 100000000; i++) result += result; // ハチャメチャ重い処理
     uiThreadQueue.Enqueue(() => // スレッドセーフでないUI操作をキュー
     {
       // メインスレッド
       DisplayToUI();
     });
     // ワーカースレッド
  });
  // メインスレッド
  while(true)
  {
    while(uiThreadQueue.Count > 0) // ワーカースレッドの完了を監視
    {
      uiThreadQueue.Dequeue().Invoke();
    }
    Repaint(); // UIの更新（これが60回/秒以上実行されればフリーズはしない）
  }
}
```

力業のようにも見えるのですが、実際ほとんど同等の実装になっています。
ポイントは以下の点です。

1. UIスレッドに任せたい処理のキューがある
2. UIスレッドは初期化処理が終わったら半無限のループでキューを監視している
3. 重い処理をワーカースレッドで実行
4. ワーカースレッドからUIスレッドにキューを通して処理を任せる

# `Dispatcher`クラスを見てみる

コード全体はこちら
https://github.com/dotnet/wpf/blob/master/src/Microsoft.DotNet.Wpf/src/WindowsBase/System/Windows/Threading/Dispatcher.cs

`Dispatcher`クラスは先述のポイントのうち、1/2/4の役割を担っています。いろいろな例外処理が挟まっているものの、いかのメンバが機能を実装しています。

|機能|実装||
|:--|:--|:--|
|キュー|`Dispatch._queue`|
|無限ループ|`Dispatch.Run`|さらに奥の`PushFrameImpl`にwhileループがある|
|任せる|`Dispatch.BeginInvoke`|`Dispatch._queue`に積むだけ|

ちなみに`Dispatch.Run`については、`Application.Run`から呼び出され、`Application.Run`は`Main`から呼び出されます。WPFでは普通見ないのですが、`Main`メソッドは当然あります。

```csharp
public partial class App : System.Windows.Application
{
  public static void Main()
  {
    HogeTest.App app = new HogeTest.App();
    app.InitializeComponent();
    app.Run(); // このメソッドはアプリ終了まで抜けることはない
  }
}
```

これでWPFのDispatchの仕組みを完全に理解することができました。しかし、C#にはAsyncAwaitの構文があり`Dispatch.BeginInvoke`を使うことは非常に稀であります。ということでWPFのAsyncAwaitも見ていきましょう。

# AsyncAwaitの動作の仕組み

詳しくは[以前に書いたこちら（async/await で書いたコードと実行されるスレッド）](https://qiita.com/up-hash/items/c582e70d3f3a6760ecbe#%E4%BD%BF%E3%81%84%E9%81%93)を見ていただきたいのですが、ざっくりと説明すると。

`await HogeAsync()` 描いたとしましょう。すると、その直前のSynchronizationContext（≒スレッド）を記憶しておき、HogeAsyncの中で「よびだし元のSynchronizationContextで実行してくれな処理パート」が出てきた時に記憶しておいたSynchronizationContextで処理を行うというものです。コードにするとこんなイメージ。（実際にはコンパイラの仕事が挟まれるのであくまでイメージ）

```csharp
{
  ui.text = "aaaa";
  var x = await GetTextAsync();
  ui.text = text;
  await Task.Run(() => Thread.Sleep(100000));
  ui.text = "";
}
// ↓
{
  SynchronizationContext callerContext = SynchronizationContext.Current;
  ui.text = "aaaa";
  GetTextAsync(onComplete: text =>
  {
    callerContext.Post(() => // 呼び出し元のContext（≒スレッド）に戻す
    {
      ui.text = text;
      Task.Run(() =>
      {
        // Task.Runはワーカースレッドで実行するため、ここはメインスレッドではなくなっている
        Thread.Sleep(100000)
        // onComplete();
        callerContext.Post(() => // 呼び出し元のContext（≒スレッド）に戻す
        {
          ui.text = "";
        });
      });
    };
  });
}
```

ということで、awaitをうまく使うためには、`SynchronizationContext.Current`（Staticなのでプロセス内で共有の設定です）のインスタンスに最低でも`SynchronizationContext.Post`が実装されている必要があります。（実際にはもっといろいろ必要）
そして`Post`にコールバックを渡したときにメインスレッドで実行されるようになっていればOKです。
ちなみにConsoleアプリでは`SynchronizationContext.Current`はデフォルトでnullであり、awaitから戻ってきてもメインスレッドに復帰しません。

WPFでは、この`SynchronizationContext`が`DispatcherSynchronizationContext`にて実装されています。そして肝心の`Post`については、`Dispatcher.BeginInvoke`が呼ばれるようになっています。

これはちょっとよくわからなかったのですが、`SynchronizationContext.SetSynchronizationContext`で`DispatcherSynchronizationContext`を設定している個所が、`Dispatcher.Invoke`したタイミング以外見当たらないんですよね…
つまりそれまではawaitしても帰ってくることができなくなりそうです。一回でも`Dispatcher.Invoke`が呼ばれていればそれ以降はCurrentにContextが入り続けるので、WPFのコアが初期化している間に`Dispatcher.Invoke`が呼ばれていい感じになるのかな…という感じです。しかし、普通に考えたら明示的に初期化するでしょって思っちゃうのですが、私が見落としているだけなんでしょうかね🤔🤔🤔

# まとめ

ということで紐解いてみると結構あっけない作りだったりします。まぁ独自のGUIシステムを作ろうとでもしない限り、何も考えなくても書けますけどね。