はじめに
上記の記事で他スレッドにDispatcherを設けるとき、いくつかの闇があると書かれていた。
それを自分なりに見せないように設計し、実装してみたので公開する。
解決したい課題
C#で非同期で作業をする場合、Taskという非常に便利な機能がある。
これを使えば、簡単に非同期の処理を記述できる。
前回の本を、非同期で100冊追加する処理を試しに書いてみるとこうなる。
変更点のみ抜粋
public MainWindowViewModel()
{
Dispatcher uiThread = Dispatcher.CurrentDispatcher;
BookList = new List<BookInfo>();
AddItemToShelf();
uiThread.BeginInvoke(() => {
BookList = books;
Title = $"本の総数:{BookList.Count}冊";
});
}
public async void AddItemToShelf()
{
books = new List<BookInfo>();
ConcurrentQueue<BookInfo> createdBookQueue = new ConcurrentQueue<BookInfo>();
List<Task> tasks = new List<Task>();
for (int i = 0; i < 100; i++)
{
string bookName = $"TaskName{i}";
tasks.Add(Task.Run(() => createdBookQueue.Enqueue(new BookInfo(bookName))));
}
await Task.WhenAll(tasks);
while(createdBookQueue.TryDequeue(out BookInfo book))
{
books.Add(book);
}
}
全ソースコードはGitHubにアップロードしておいた。タグも一応つけておいた。
このソースは100個のタスクを雑に詰め込んでいるので、実際に実行してみるとこのように
タスクの生成順と実際の並び順が一致することは稀になっている。
もし、これを順番を保ったまま生成したいとなると、冒頭で示したページのコードを使えば良い。
だけどそこに書かれているコードは著者が言っているように闇がいくつか存在するし、
大規模なソフトとかだとログの機能などですでにawaitを挟んでいて事故が起きる可能性も高そうだ。
というわけで、今回はできるだけ闇を隠すようなDispatcherを作ってみた。
ソースコード
名前空間は各自で使用状況に合わせて修正してほしい。
今回変更しているのは次の二点。
- Task.Resultを使わない
- 抽象クラス化してカプセル化
Task.Resultの使用回避
Task.Resultでデッドロックを起こす可能性があるのだったら使わなければいいじゃない、ということでUIスレッドのDispatcherにbaseDispatcher.BeginInvoke((Action)OnInitiallized);
を投げるようにして回避する。
ただ、baseDispatcherがUIスレッドではなかった場合に不具合が起こるから、
初期化時にUIスレッドのDispatchetrを渡せるようにコンストラクタのオーバーロードをしておいた。
抽象クラス化してカプセル化
今回提示したソースコードは抽象クラス化して、外部から使えるメソッドは次の3つに絞った。
- OnInitiallized
- 初期化が完了されたときにベースのスレッドで実行されるメソッド
- PostOnIsolatedThread
- 作成したスレッドのメッセージキューに新しい処理を追加するメソッド
- PostOnBaseThread
- ベースのスレッドのメッセージキューに新しい処理を追加するメソッド
そうして絞ってしまえば、上のリンクのようなシンプルな記述方法で100個のインスタンスの作成を特定のスレッドで行い、
結果を返すことができる。
ちなみにOnInitiallizedの最後の処理でBookListCreateComplete→OnBookListCreateCompletedと一つメソッドを挟んでいるのは、
外部のスレッドのすべてのメッセージキューが終了されたことを確定させてからUIスレッドに作業が完了したと通知したいから。
PostOn~~の関数は作業の予約をしただけなため、いきなりPostOnBaseThreadを使ってしまうと、
100個のインスタンスの作成作業と更新完了通知を同時に走らせてしまうため、最終的な出力は不定のものになってしまう。
なお、今回は雑に作っているので初期化が終わったらすぐにインスタンスの作成をしているけど、
初期化に手間取り過ぎたりOnInitiallizedの処理が少なすぎたらBookListChangedを購読する前に
OnBookListCreateCompletedが呼ばれてしまう可能性が出てくるので注意してください。
(コンストラクタに作業完了時のActionを指定できるようにしてもいいかもしれない)