はじめに
前回、TPL Dataflowを使って複数のスレッドからの結果を一つのファイルに書き込む で、ファイルIOでTPL Dataflowが使えるんだよということを書きました。
ファイルだけでなく、UIの操作にもTPL Dataflowは使えます。
アクターモデルは、一つしかないリソースに対する安全な非同期処理のための仕組みだからです。
今回は、進行状態を表示するものを作ることにします。
処理の進行状況を表示する
コンピュータが処理をしている時、何かしら動作していることを示すための仕組みが必要であることは、コンピュータのUIデザインの基本です。ホントは処理をしているのに、何も表示しなかったら、ユーザは、本当に動いているかどうかがわかりません。処理の途中で、ソフトを落としてしまうかもしれません。このようなことを防ぐためにも、ちゃんと動いていますということを示すことは重要です。
長い処理は、UIスレッドで行うと、UIが固まってしまいます。そのため、UIとは別のスレッドで行わせます。しかし、UIスレッドとは別のスレッドで処理を行うため、処理実行中にUIに何か変更を加えようとすると、エラーがでます。UIの変更はUIのスレッドで実行しないといけないためです。
C#では、古は、「BackgroundWorker」を使うことで、比較的簡単に、進行表示を作ることができましたが、Taskがなかった時の時代のものでしかなく、今時つかいたくありません。
TPL Dataflowでは、ブロックごとに実行するスレッドを指定することができます。
これを使って、描画に関するところのスレッドをUIのスレッドにすれば、スレッドを気にせず、処理の進行を表示するプログラムを作ることがことができるというわけです。
どこのクラスからでも使えるようにする。
普通、進行を表示するところは、ソフトの一部の場所であることが定番です。そして、一つしかないことが定番です。そのため、Staticクラスに、TPL DataFlow を配置します。このようにすることで、どこのクラスからでも、スレッドを気にせず、進行表示を操作することが可能になります。
MVVMの作り方をしていても、進行表示を行うことができるということですね。Postさえすれば、なんとかしてくれます。
準備
nugetで「TPL DataFlow 」を入れる。
using に、「using System.Threading.Tasks.Dataflow;」を加える。
WPFで、テキトウに、progressBarとtextBlockとbuttonの配置して名前をつける。
ソース
/// <summary>
/// 状態表示用の静的クラス
/// </summary>
public static class Status
{
public static BufferBlock<string> TextBlock { get; private set; } = new BufferBlock<string>();
public static BufferBlock<int> ProgressBlock { get; private set; } = new BufferBlock<int>();
}
/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
//UIスレッドの取得
TaskScheduler uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
//progressBar用のActionBlock作成
ActionBlock<int> progressBlock = new ActionBlock<int>((n) =>
{
progressBar.Value = Math.Min( Math.Max(0, n),100);
}, new ExecutionDataflowBlockOptions() { TaskScheduler = uiTaskScheduler });
//静的クラスのBufferBlockに、進行表示の処理をつなげる。
Status.ProgressBlock.LinkTo(progressBlock, new DataflowLinkOptions() { PropagateCompletion = true });
//一気にも書ける。わかりにくいけど。
Status.TextBlock.LinkTo(
new ActionBlock<string>((n) =>
{ textBlock.Text = n; },
new ExecutionDataflowBlockOptions() { TaskScheduler = uiTaskScheduler })
, new DataflowLinkOptions() { PropagateCompletion = true });
}
private async void button_Click(object sender, RoutedEventArgs e)
{
await Task.Factory.StartNew(() => {
Status.TextBlock.Post("普通のFor文");
for (int i = 0; i < 100; i++)
{
System.Threading.Thread.Sleep(20);
Status.ProgressBlock.Post(i);
}
});
await Task.Factory.StartNew(() => {
Status.TextBlock.Post("Parallel.For版");
int size = 1200;
int c1 = 0;
//Progress用の数値を作成関数 並列処理なので、Interlockedで加算。
Func<int> func = () => { c1 = System.Threading.Interlocked.Add(ref c1, 1); return c1 * 100 / size; };
Parallel.For(0, size, (n) =>
{
System.Threading.Thread.Sleep(80);
Status.ProgressBlock.SendAsync(func()).Wait();
});
});
}
}
まとめ
このように静的クラスにTPL DataFlowを使うと、どこのクラスからも現在の状態をUIに伝えることができます。非同期にできるのでとても便利です。静的クラスで使えるようにするための、初めの登録を忘れずに。
PostとSendAsync(n).Wait(); の違いは、私自身も正確には理解していないのですが、postの方が非同期的、SendAsync(n).Wait()の方は同期的な感じみたいです。そのため、postは、依頼はしたが、実行の確認はしていないという感じですかね。大体はPostで十分だと思いますが、困ったらSendAsync(n).Wait()の方を使ってみるといいと思います。