SynchronizationContext の実験(+Dispatcher(WPF)の実験)

もくじ
https://tera1707.com/entry/2022/02/06/144447

やりたいこと

WInUI3/WPFで、ワーカースレッドの中に、メインスレッドでやってほしい処理(UIの更新など)を書きたいときはこうやる、というのを以前調べた。

tera1707.com

tera1707.com

それはそれでできてたのだが、最近別の書き方をしているのを見かけた。

それが、以前調べた「同期コンテキスト)」に関連するっぽい。 勉強がてら、実験してみる。

やりかた

SynchronizationContext.Send(SendOrPostCallback, Object)

と、

SynchronizationContext.Post(SendOrPostCallback, Object)

を使う。

実験コード

SynchronizationContext.Send()は同期的に実行する(その場で実行し、終わるのを待つ)

SynchronizationContext.Post()は、非同期的に実行する(その場では実行せず、対象のスレッドがキューを見たタイミングで実施する)

っぽい。

using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace SyncroTest;

public partial class MainWindow : Window
{
    private SynchronizationContext? Ctx { get => SynchronizationContext.Current; }
    private int ThId { get => Thread.CurrentThread.ManagedThreadId; }

    public MainWindow() => InitializeComponent();

    private async void Button_Click(object sender, RoutedEventArgs e)
    {
        //現在の(UIスレッドの)コンテキストを取得
        var context1 = Ctx;

        Debug.WriteLine($"Task.Run前:{ThId}");

        await Task.Run(() =>
        {
            Debug.WriteLine($"Task.Runの中:{ThId} 開始");
            context1?.Send(callback, "Send");
            context1?.Post(callback, "Post");
            Debug.WriteLine($"Task.Runの中:{ThId} 終了");
        });

        Debug.WriteLine($"Task.Run後:{ThId}");
        Debug.WriteLine($"");

        // ---------------------

        Debug.WriteLine($"context1 = Current? : {context1 == Ctx}");
        Debug.WriteLine($"");
    }

    private void callback(object? param)
    {
        Debug.WriteLine($"{param}{ThId}");
    }
}

実行結果

Task.Run前:1
Task.Runの中:8 開始
Send:1
Task.Runの中:8 終了
Post:1
Task.Run後:1

context1 = Current? : False

上に書いた通り、Sendはその場で、Postはワーカースレッドが終わってから、メインスレッドのタイミングで実行された。

実験コード②

上の実験コードの中のTask.Runに、.ConfigureAwait(false);を付けてみた。

変化点はココ。

        await Task.Run(() =>
        {
            Debug.WriteLine($"Task.Runの中:{ThId} 開始");
            context1?.Send(callback, "Send");
            context1?.Post(callback, "Post");
            Debug.WriteLine($"Task.Runの中:{ThId} 終了");
        }).ConfigureAwait(false);// ★ココが変化点

出力結果

Task.Run前:1
Task.Runの中:8 開始
Send:1
Task.Runの中:8 終了
Post:1
Task.Run後:8

context1 = Current? : False

Send、Postの処理の実行順は特に変化なし。

通常の.ConfigureAwait(false);を付けたときの動作と同じで、 await Task.Run()が終わった後のスレッドが、ワーカースレッドになったのみ。

実験コード③:Dispatcher(WPF)の実験

以前、ワーカースレッドのコードに、UI部品を更新するコードを書く(WPF)というのをやったのだが、Dispatcher.Invoke() ほぼイコール SynchronizationContext.Send()じゃないか?

また、Dispatcher.BeginInvoke() ほぼイコール SynchronizationContext.Post()じゃないか?と気づいた。

ほんとにそうか実験してみた。

using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;

namespace SyncroTest;

public partial class MainWindow : Window
{
    Dispatcher dispatcher = Dispatcher.CurrentDispatcher;
    private SynchronizationContext? Ctx { get => SynchronizationContext.Current; }
    private int ThId { get => Thread.CurrentThread.ManagedThreadId; }

    public MainWindow() => InitializeComponent();

    private async void Button_Click(object sender, RoutedEventArgs e)
    {
        //現在の(UIスレッドの)コンテキストを取得
        var context1 = Ctx;

        Debug.WriteLine($"Task.Run前:{ThId}");

        await Task.Run(() =>
        {
            Debug.WriteLine($"Task.Runの中:{ThId} 開始");
            
            context1?.Send(callback, "Send");
            dispatcher.Invoke(() => Debug.WriteLine($"dispatcher.Invoke : {ThId}"));

            context1?.Post(callback, "Post");
            dispatcher.BeginInvoke(() => Debug.WriteLine($"dispatcher.BeginInvoke : {ThId}"));
            
            Debug.WriteLine($"Task.Runの中:{ThId} 終了");
        });

        Debug.WriteLine($"Task.Run後:{ThId}");
        Debug.WriteLine($"");

        // ---------------------

        Debug.WriteLine($"context1 = Current? : {context1 == Ctx}");
        Debug.WriteLine($"");
    }

    private void callback(object? param)
    {
        Debug.WriteLine($"{param}{ThId}");
    }
}

結果出力

Task.Run前:1
Task.Runの中:8 開始
Send:1
dispatcher.Invoke : 1
Task.Runの中:8 終了
Post:1
dispatcher.BeginInvoke : 1
Task.Run後:1

context1 = Current? : False

sendとinvoke、postとbegininvokeで同じ動きをしているように見える。やはり同じっポイ。

参考

非同期処理とディスパッチャー
ufcpp.wordpress.com

備考

ワーカースレッドにはSynchronizationContextが無いので、ワーカースレッドの中ではSynchronizationContext.Currentはnullになっている。

なので、.ConfigureAwait(false);していたTaskの後でSynchronizationContext.Currentを取ってもnullなので注意。

参考

(C#) 同期コンテキストと Task, async/await

https://qiita.com/ikorin24/items/92ea46374642a0b59011

Unityでメインスレッドに処理を戻して実行する

https://qiita.com/odayushin/items/fd729c2752121c84e0e6