昨日Twitterで次のようなつぶやきを見かけました。よくよく考えてみれば、自分でもこれまで一度も並列処理中の進捗通知についてはやったことなかったです。ということで、とりあえずでやってみました。
ProgressBarの進捗がカオスwwwと思ったけども、Parallel.For内でReportProgressしてるからかなー。
— にしざわ こういち (@koty) August 14, 2012
サンプル
WPFで作った簡単なサンプルのUI部分を以下に示します。進捗表示用プログレスバーとラベル、並列処理実行用のボタンが置いてあります。
<Window x:Class="WpfApplication.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Progress on Parallel" Width="300" SizeToContent="Height" ResizeMode="CanMinimize"> <StackPanel> <ProgressBar x:Name="progress" Height="10" Margin="10"/> <Grid Margin="10,0,10,10"> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <Label x:Name="display" Margin="0,0,10,0" Content="0%"/> <Button x:Name="button" Content="Run" Width="75" Click="Button_Click" Grid.Column="1"/> </Grid> </StackPanel> </Window>
Parallel.Forメソッドで100回、順序に依存しない時間のかかる何らかの処理を並列に行っているとします。並列処理の外部に処理数のカウンターを用意しておき、Interlocked.Incrementメソッドを使ってロックを掛けながら値をインクリメントしています。そして、その数値を同期コンテキスト経由でUIスレッドに戻しつつ、進捗状態を更新しています。
using System.Threading; using System.Threading.Tasks; using System.Windows; namespace WpfApplication { public partial class MainWindow : Window { public MainWindow() { this.InitializeComponent(); } private async void Button_Click(object sender, RoutedEventArgs e) { this.button.IsEnabled = false; var context = SynchronizationContext.Current; await Task.Run(() => { int counter = 0; //--- 処理数をカウント Parallel.For(0, 100, i => //--- とりあえず100個ベタうち { Thread.Sleep(10 * i); //--- 何か重い処理 context.Post(progress => //--- UIに同期させて通知 { this.progress.Value = (int)progress; this.display.Content = string.Format("{0}%", (int)progress); }, Interlocked.Increment(ref counter)); //--- ロックしてインクリメント }); }); this.display.Content = "完了"; this.button.IsEnabled = true; } } }
実行画面は以下のような感じです。
本当は「もっと良い方法があるんじゃないか」と頭をひねって考えてみたのですが、現在のところこれよりエレガントな方法は思い付いていません。もはや「やっぱり結局こうなるのか」状態。もし、何か代替案があったら是非教えてください。
ちょっと考察
TPL入門 (16) - おわりにでも軽く触れたのですが、並列処理を採用する場合は、並列処理のオーバーヘッドを差し引いても通常の逐次的な処理よりも良いパフォーマンスが期待できるかどうかを考慮しなければなりません。つまり、以下のようなケースで採用すべきです。
- 並列に処理すべき1つ1つのタスクに掛かる時間が比較的長い
- タスクの個数が非常に多い
特に前者の理由で採用する場合、処理数カウンターをロックしている時間はタスク1つ1つにかかる時間よりもずっと短いはずです。なので、今回のサンプルのように共通変数のカウンターを用意しても大きなパフォーマンスロスにはならないと考えられます。ちなみに、ネタ元の@kotyさんも同じような方法で解決したようです。やはり、行きつくところはここなのかもしれないですね。