redwarrior’s diary

C#, ASP.NET, WEB, G*などの雑多な情報

Dispatcher.InvokeとDispatcher.BeginInvoke、Dispatcher.InvokeAsyncの違い

WPFアプリケーションで、別スレッドから画面を更新したい場合は、DispatcherクラスのInvokeメソッドに、引数としてメソッドを渡して、UIスレッドで実行されるようにします。

ググってみると、DispatcherクラスにはInvokeメソッドの他に、BeginInvokeメソッド、InvokeAsyncメソッドがあるようです。

これらの違いが気になったので調べてみました。

Dispatcher.InvokeとDispatcher.BeginInvoke、Dispatcher.InvokeAsyncの違い

詳しく説明されているサイトがありました。

Learn more about how WPF Dispatcher works.(Invoke and InvokeAsync) | Neal's Blog

このサイトによると、Invokeが同期処理で、BeginInvokeとInvokeAsyncが非同期処理であり、InvokeAsyncが.NET Framework 4.5 で追加された新しいメソッドであるらしいです。

現在ではBeginInvokeとInvokeAsyncの実装は同じなので、InvokeAsyncを使うと良いそうです。

違いを確認する

文章で理解しただけだと「(* ̄- ̄)ふ~ん 」となって終わるので、コードを書いて確かめてみました。

MainWindow.xaml

<Window x:Class="DispatcherTestApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DispatcherTestApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="300">
    <StackPanel>
        <ListBox>
            <ListBoxItem>
                <StackPanel Orientation="Horizontal">
                    <Button Click="Button_Click" Width="140" Margin="10">Dispatcher.Invoke</Button>
                    <TextBlock Name="Text" VerticalAlignment="Center">123</TextBlock>
                    <TextBlock VerticalAlignment="Center">・・・①</TextBlock>
                </StackPanel>
            </ListBoxItem>
            <ListBoxItem>
                <StackPanel Orientation="Horizontal">
                    <Button Click="Button_Click_1" Width="140" Margin="10">Dispatcher.BeginInvoke</Button>
                    <TextBlock Name="Text1" VerticalAlignment="Center">123</TextBlock>
                    <TextBlock VerticalAlignment="Center">・・・②</TextBlock>
                </StackPanel>
            </ListBoxItem>
            <ListBoxItem>
                <StackPanel Orientation="Horizontal">
                    <Button Click="Button_Click_2" Width="140" Margin="10">Dispatcher.InvokeAsync</Button>
                    <TextBlock Name="Text2" VerticalAlignment="Center">123</TextBlock>
                    <TextBlock VerticalAlignment="Center">・・・③</TextBlock>
                </StackPanel>
            </ListBoxItem>
            <ListBoxItem>
                <StackPanel Orientation="Horizontal">
                    <Button Click="Button_Click_3" Width="140" Margin="10">NoneDispatcher</Button>
                    <TextBlock Name="Text3" VerticalAlignment="Center">123</TextBlock>
                    <TextBlock VerticalAlignment="Center">・・・④</TextBlock>
                </StackPanel>
            </ListBoxItem>
            <ListBoxItem>
                <StackPanel Orientation="Horizontal">
                    <Button Click="Button_Click_4" Width="140" Margin="10">await InvokeAsync</Button>
                    <TextBlock Name="Text4" VerticalAlignment="Center">123</TextBlock>
                    <TextBlock VerticalAlignment="Center">・・・⑤</TextBlock>
                </StackPanel>
            </ListBoxItem>
            <ListBoxItem>
                <StackPanel Orientation="Horizontal">
                    <Button Click="Button_Click_5" Width="140" Margin="10">return value</Button>
                    <TextBlock Name="Text5" VerticalAlignment="Center">123</TextBlock>
                    <TextBlock VerticalAlignment="Center">・・・⑥</TextBlock>
                </StackPanel>
            </ListBoxItem>
        </ListBox>
    </StackPanel>
</Window>

MainWindow.xaml.cs

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

namespace DispatcherTestApp1
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        private readonly Stopwatch _stopwatch;

        public MainWindow()
        {
            _stopwatch = new Stopwatch();
            InitializeComponent();
        }

        #region Dispatcher.Invoke
        //①
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            Text.Text = "start";
            _stopwatch.Start();
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event start");
            Task.Run(HeavyAction);
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event end");
        }

        private void HeavyAction()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action start");
            Thread.Sleep(3000);
            //Text.Text = "456"; <--エラーになる

            Dispatcher.Invoke(ChangeText);

            Thread.Sleep(100);

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action end");
        }

        private void ChangeText()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change start");
            Thread.Sleep(3000);
            Text.Text = "456";
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change end");
        }
        #endregion

        #region Dispatcher.BeginInvoke
        //②
        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            Text1.Text = "start";
            _stopwatch.Start();
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event start");
            Task.Run(HeavyAction1);
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event end");
        }

        private void HeavyAction1()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action start");
            Thread.Sleep(3000);

            Dispatcher.BeginInvoke(new Action(ChangeText1));

            Thread.Sleep(100);

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action end");
        }

        private void ChangeText1()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change start");
            Thread.Sleep(3000);
            Text1.Text = "456";
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change end");
        }
        #endregion

        #region Dispatcher.InvokeAsync
        //③
        private void Button_Click_2(object sender, RoutedEventArgs e)
        {
            Text2.Text = "start";
            _stopwatch.Start();
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event start");
            Task.Run(HeavyAction2);
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event end");
        }

        private void HeavyAction2()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action start");
            Thread.Sleep(3000);

            Dispatcher.InvokeAsync(ChangeText2);

            Thread.Sleep(100);

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action end");
        }

        private void ChangeText2()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change start");
            Thread.Sleep(3000);
            Text2.Text = "456";
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change end");
        }
        #endregion

        #region NoneDispatcher
        //④
        private async void Button_Click_3(object sender, RoutedEventArgs e)
        {
            Text3.Text = "start";
            _stopwatch.Start();
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event start");
            await Task.Run(HeavyAction3);

            ChangeText3();

            //Console.WriteLine($"{stopwatch.ElapsedMilliseconds} event end");
        }

        private void HeavyAction3()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action start");
            Thread.Sleep(3000);

            //Dispatcher.InvokeAsync(ChangeText3);

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action end");
        }

        private void ChangeText3()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change start");
            Thread.Sleep(3000);
            Text3.Text = "456";
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change end");
        }
        #endregion

        #region await Dispatcher.InvokeAsync
        //⑤
        private void Button_Click_4(object sender, RoutedEventArgs e)
        {
            Text4.Text = "start";
            _stopwatch.Start();
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event start");
            Task.Run(HeavyAction4);

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event end");
        }

        private async Task HeavyAction4()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action start");
            Thread.Sleep(3000);

            await Dispatcher.InvokeAsync(ChangeText4);

            Thread.Sleep(100);

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action end");
        }

        private void ChangeText4()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change start");
            Thread.Sleep(3000);
            Text4.Text = "456";
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change end");
        }
        #endregion

        #region return value
        //⑥
        private void Button_Click_5(object sender, RoutedEventArgs e)
        {
            Text5.Text = "start";
            _stopwatch.Start();
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event start");
            Task.Run(HeavyAction5);

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} event end");
        }

        private async Task HeavyAction5()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action start");
            Thread.Sleep(3000);

            var result = await Dispatcher.InvokeAsync(ChangeText5);

            Thread.Sleep(100);

            Console.WriteLine(result
                ? $"{_stopwatch.ElapsedMilliseconds} action result=true"
                : $"{_stopwatch.ElapsedMilliseconds} action result=false");

            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} action end");
        }

        private bool ChangeText5()
        {
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change start");
            Thread.Sleep(3000);
            Text5.Text = "456";
            Console.WriteLine($"{_stopwatch.ElapsedMilliseconds} ui change end");

            return _stopwatch.ElapsedMilliseconds % 2 == 0;
        }
        #endregion
    }
}

Dispatcher.Invoke

Dispatcher.Invokeは、同期処理です。サンプルは①です。このメソッドを基準として他のメソッドを確認していきます。
実行結果は以下の通りです。呼び出し順に処理が行われています。

0 event start
4 event end
6 action start
3007 ui change start
6007 ui change end
6108 action end

Dispatcher.BeginInvoke

Dispatcher.BeginInvokeは、非同期処理です。サンプルは②です。Delegateしか渡せないので、呼び出すときにわざわざActionをnewしなければなりません。
実行結果は以下の通りです。UI更新処理を呼び出した後も処理は続いています。

0 event start
1 event end
2 action start
3003 ui change start
3104 action end
6004 ui change end

Dispatcher.InvokeAsync

Dispatcher.InvokeAsyncは、非同期処理です。サンプルは③です。メソッドを直接渡しています。
実行結果は以下の通りです。流れはDispatcher.BeginInvokeと同じです。

0 event start
1 event end
2 action start
3003 ui change start
3103 action end
6004 ui change end

await Dispatcher.InvokeAsync

Dispatcher.BeginInvoke、Dispatcher.InvokeAsyncの返り値のDispatcherOperationクラスは、GetAwaiterメソッドを持っているので、awaitをつけることが出来ます。サンプルは⑤です。
実行結果は以下の通りです。awaitをつけて呼び出すと、渡したメソッドの実行終了後に、戻って後続の処理を実行するため、流れはDispatcher.Invokeと同じになります。

0 event start
1 event end
7 action start
3008 ui change start
6009 ui change end
6111 action end

return value

↑でawaitをつけると、Dispatcher.Invokeと同じ処理順になることがわかりましたが、awaitが必要になる場面もあります。 Dispatcher.InvokeAsyncは、Func<TResult>を渡すことができるので、awaitで渡したメソッドの返り値を取得することが出来ます。サンプルは⑥です。
実行結果は以下の通りです。画面更新メソッドの返り値によって、後続の処理の分岐をしています。

0 event start
1 event end
8 action start
3017 ui change start
6019 ui change end
6121 action result=false
6121 action end

まとめと方針

今までの内容をまとめて考えた結果、以下の方針で使ったら良いかなと思いました。

  • 画面更新が同期処理で問題なければ、Dispatcher.Invokeを使用する
  • 画面更新を非同期にしたければ、Dispatcher.InvokeAsyncを使用する
  • 画面更新を行うメソッドの返り値を使用したい場合は、Dispatcher.InvokeAsyncを使用する

補足

以下のサイトによると、例外の扱い方が違うらしいですが、画面更新で例外が発生する事はあまりないと思うので、こちらは確かめていません。

WPF Dispatcher.BeginInvokeとDispatcher.InvokeAsyncの違い - reflux flow

WPF Dispatcher BeginInvoke vs. InvokeAsync · jbe2277/waf Wiki · GitHub

(番外編)Dispatcherを使用せずに、画面を更新する

Dispatcherを使用せずに、画面を更新する方法がいくつかあるので、書いておきます。

awaitを使用して、別スレッドからの画面の更新をしない

別スレッドの処理をする場合に、Task.Run等を使うと思いますが、Task.Runの前にawaitを記述して、画面の更新処理を後ろの行に記述します。サンプルの④の処理です。

これで別スレッドの処理が終わってから、メインスレッド(UIスレッド)に戻ってくるので、画面の更新をしても例外は発生しません。

Prism等を使用して、Data Bindingによって画面を更新する

ViewModelの更新→Data BindingによるViewの更新という仕組みを使えば、別スレッドでViewModelを変更したとしても、直接画面のコントロールを触らないため、例外は発生しません。サンプルはありません。

以上です。