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を変更したとしても、直接画面のコントロールを触らないため、例外は発生しません。サンプルはありません。
以上です。