1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WPFのDispatcher、SynchronizationContextに関する備忘録

1
Last updated at Posted at 2026-11-02

はじめに

誤りについては、優しく指摘していただけますと幸いです。

WPFのUI部品は作ったスレッドしか触れない

TextBoxやButtonはUIスレッド以外からは触れず、
触ろうとすると例外になる。

UIを複数スレッドが同時に書き換えると、
半端な状態で描画されたり、内部の整合性が壊れてしまう。
そういった事態を防ぐため、WPFの部品はUIスレッド以外から触れないようにされている。

WPFの部品はUIスレッド以外から触れない。

Dispatcherとは何か

通常、重い処理はUIスレッドから逃がして別のスレッドで行う。
しかし、重い処理の結果をTextBoxに表示したいというようなケースはある。
その場合、別のスレッドからUIスレッドに処理を戻す仕組みが必要になるが、
この仕組みがDispatcherである。

UIスレッド上ではメッセージループというループが回っている。
このループの中では、Dispatcherのキューに積まれた仕事が1つずつ処理される。
つまり、DispatcherはUIスレッド上でやってほしい処理を積んでおく場所と言える。

UIスレッドから重い処理を逃がしたい、しかし結果の表示はUI上で行いたい。
この要求に応えるために存在するのがDispatcher。
Dispatcherには処理が積まれていく。

通常の非同期処理(Task, async/await)との関係性

UIスレッドから別スレッドへと処理を逃がすのはTaskの仕事。
別スレッドからUIスレッドへと処理を戻す際の受け皿がDispatcher。
async/awaitは、その2つを繋ぎ「逃がして、戻る」を表現できるようにしたもの。

UI→別スレッド→UI
private async void Button_Click(object sender, RoutedEventArgs e)
{
    ResultText.Text = "処理中...";

    int result = await Task.Run(() =>
    {
        Thread.Sleep(5000); 
        return 42;
    });

    ResultText.Text = $"結果: {result}";
}

このようなコードの場合、await以降の処理はUIスレッドのDispatcherキューに
一旦預けられている。
預けられた処理がUIスレッドのメッセージループで取り出されたとき、
ResultTextは更新されている。

SynchronizationContext

タスクが完了するのはラムダ式を実行しているスレッドプール上のスレッド。
つまりTask.Run()で処理を逃がした先のスレッドである。
ここで何もしなければ、以降の処理はそのまま逃がしたスレッド上で実行されようとする。
しかし、以降の処理はUIスレッドで処理しなければならない。

このような場合にはDispatcherに預けなければならないわけだが、
Dispatcherには勝手に処理を拾ってくる機構はない。
そのため、誰かがどうにかして処理をDispatcherに預ける必要がある。

そのDispatcherに預けるための窓口が、SynchronizationContext である。

awaitは呼び出された時点のSynchronizationContext(≒awaitが呼ばれた側のスレッド)を自動で記録しており、タスク完了時にそこへ続きを依頼する。
これにより、開発者が何もしなくてもawait以降はUIスレッドに戻る。

Dispatcherに処理を預けるために利用する窓口がSynchronizationContext。
await以降の処理は、SynchronizationContext経由で
Dispatcherに預けられている。


SynchronizationContextの実装イメージ
public class SynchronizationContext
{
    // 現在のスレッドで設定されているContext。設定がなければnull。
    public static SynchronizationContext Current { get; }

    // 処理を依頼する。
    public virtual void Post(SendOrPostCallback d, object state);

    // 処理を依頼し、終わるまで待つ。
    public virtual void Send(SendOrPostCallback d, object state);
}

※「処理を依頼する」は、先ほどの話で言うところの「キューに処理を積む」と結果的には同じ。ただ正確にはキューに処理を積むのはDispatcherであってコイツではない。こいつは「積んどいて」と依頼する人。

Currentはスレッドごとに保持される。
WPFのUIスレッド以外、スレッドプールのスレッドなどでは通常null。
WPFではDispatcherSynchronizationContext型として保存されている。

DispatcherSynchronizationContext
class DispatcherSynchronizationContext : SynchronizationContext
{
    Dispatcher _dispatcher;          // UIスレッドに処理を積むDispatcher
    DispatcherPriority _priority;    // 既定はNormal

    public override void Post(SendOrPostCallback d, object state)
        => _dispatcher.BeginInvoke(_priority, d, state);

    public override void Send(SendOrPostCallback d, object state)
        => _dispatcher.Invoke(_priority, d, state);
}

関係性

関係性としてはこんな感じ(AIに作らせた)。

Task / await ← WPFを知らない共通ライブラリ側
│ 使う
▼
SynchronizationContext ← 抽象(Post / Send / Current)
△
│ 継承して実装
DispatcherSynchronizationContext ← WPF側の実装
│ 呼ぶ
▼
Dispatcher(キュー)→ UIスレッドのループが実行

UIスレッドに処理が戻るまでの流れ

UIスレッドに処理が戻るまでの流れはこうなる。

await実行時にSynchronizationContext.Currentを保持
│
▼
Task.Runの処理が終わったら
コールバックに登録した続きの処理をPostに入れて呼ぶ。
│
▼
DispatcherSynchronizationContextが保持しているDispatcherの
BeginInvoke(処理)を呼び出す。
│
▼
Dipatcherはそれを受けて処理をキューに積む。

スレッドが切り替わる様子

4つのパターンでスレッドが切り替わる(切り替わらない)様子を観測してみる。
ボタンを押すと処理が行われているスレッドのIDと、それがUIスレッドか否か、
Contextがnullか否かを表示してくれる。

WindowはUI部品で、元々Dispatcherプロパティを持っている。

XAML
<Window x:Class="DispatcherLab.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Dispatcher Lab" Height="560" Width="860">
    <Grid Margin="8">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <WrapPanel Grid.Row="0">
            <Button Content="1. 基本 (await Task.Run)"       Click="Exp1_Click" Margin="0,0,8,8" Padding="8,4"/>
            <Button Content="2. ConfigureAwait(false)"        Click="Exp2_Click" Margin="0,0,8,8" Padding="8,4"/>
            <Button Content="4. Task.Run内のawait"             Click="Exp4_Click" Margin="0,0,8,8" Padding="8,4"/>
            <Button Content="5. Post / InvokeAsync"           Click="Exp5_Click" Margin="0,0,8,8" Padding="8,4"/>
            <Button Content="9. DispatcherからContext生成" Click="Exp9_Click" Margin="0,0,8,8" Padding="8,4"/>
            <Button Content="ログ消去"                         Click="Clear_Click" Margin="0,0,8,8" Padding="8,4"/>
        </WrapPanel>

        <TextBox x:Name="LogBox" Grid.Row="1"
                 IsReadOnly="True" FontFamily="Consolas" FontSize="13"
                 VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"/>
    </Grid>
</Window>
Exmaple
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace DispatcherLab
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        /// <summary>
        /// ログ出力を行う
        /// 画面への記載だけUIスレッドに依頼
        /// </summary>
        /// <param name="label"></param>
        private void Log(string label)
        {
            string line =
                $"{DateTime.Now:HH:mm:ss.fff}  {label,-26}" +
                $" Thread={Environment.CurrentManagedThreadId,2}" +
                $"  UIスレッド={Dispatcher.CheckAccess(),-5}" +
                $"  Context={SynchronizationContext.Current?.GetType().Name ?? "null"}";
            Append(line);
        }

        private void Header(string title) => Append(Environment.NewLine + "=== " + title + " ===");

        private void Append(string line)
        {
            Debug.WriteLine(line);
            Dispatcher.InvokeAsync(() =>
            {
                LogBox.AppendText(line + Environment.NewLine);
                LogBox.ScrollToEnd();
            });
        }

        private void Clear_Click(object sender, RoutedEventArgs e) => LogBox.Clear();

        /// <summary>
        /// awaitの前後でスレッドとContextが変わる様子
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private async void Exp1_Click(object sender, RoutedEventArgs e)
        {
            Header("1. 基本 (await Task.Run)");
            Log("前");
            await Task.Run(() =>
            {
                Log("中");
                Thread.Sleep(1000);
            });
            Log("後");
        }

        /// <summary>
        /// UIスレッドに戻らなくなる
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private async void Exp2_Click(object sender, RoutedEventArgs e)
        {
            Header("2. ConfigureAwait(false)");
            Log("前");
            await Task.Run(() =>
            {
                Log("中");
                Thread.Sleep(1000);
            }).ConfigureAwait(false);

            Log("後");

            try
            {
                Title = "ここでUIを触る";
                Log("Title設定 成功");
            }
            catch (Exception ex)
            {
                Log("Title設定 失敗: " + ex.GetType().Name + " / " + ex.Message);
            }
        }

        /// <summary>
        /// Currentがnullの場所でawait
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private async void Exp4_Click(object sender, RoutedEventArgs e)
        {
            Header("4. Task.Run内のawait");
            Log("外 前");

            await Task.Run(async () =>
            {
                Log("Run内 前");
                await Task.Delay(500);
                Log("Run内 後");
            });

            Log("外 後");
        }

        /// <summary>
        /// Postを手動で書く
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Exp5_Click(object sender, RoutedEventArgs e)
        {
            Header("5. SynchronizationContext.Post / Dispatcher.InvokeAsync");

            SynchronizationContext context = SynchronizationContext.Current;
            Log("記録(UIスレッド)");

            Task.Run(() =>
            {
                Log("Task内(裏)");
                context.Post(_ => Log("Post経由"), null);
                Dispatcher.InvokeAsync(() => Log("InvokeAsync経由"));
            });
        }

        /// <summary>
        /// DispatcherからContextを明示的に生成
        /// ContextはDispatcherへの依頼窓口に過ぎない
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Exp9_Click(object sender, RoutedEventArgs e)
        {
            Header("9. new DispatcherSynchronizationContext(Dispatcher)");

            Log("記録(UIスレッド)");
            var ctx = new DispatcherSynchronizationContext(Dispatcher);

            Task.Run(() =>
            {
                Log("Task内(裏)");
                ctx.Post(_ => Log("手動生成Context.Post"), null);
            });
        }
    }
}

スクリーンショット 2026-11-02 143239.png

Ex1では単にTask.Runで別のスレッドに処理が逃がされて、
その後UIスレッドに戻される様子が観測できる。

Ex2で書かれているConfigureAwait(false)は
「awaitがCurrentを記録してそこに処理を戻す動作を辞めさせる。
こうすると、await以降はタスクが完了したスレッドで続行されるため、UI部品には触れなくなる。例外が記録されいているのはそのため。

Ex4ではTask.Run内でawaitすることで更に処理が逃がされる様子が観測できる。
最終的には元のUIスレッドに戻っている様子も分かる。

Ex5はPostを手で書いたもの。
UIスレッドから逃がされた処理が、Postを経由してUIスレッドに戻っているのが分かる。

Ex9は明示的にUIスレッドに処理を戻すことを記載している。
new DispatcherSynchronizationContext(Dispatcher)でDispatcherを生成したスレッドに紐づけている。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?