Task Parallel Library は素晴らしく、私は過去数か月間よく使用してきました。ただし、本当に気になることがあります。それTaskScheduler.Current
は、デフォルトのタスク スケジューラであり、TaskScheduler.Default
. これは、ドキュメントやサンプルで一見しただけでは明らかではありません。
Current
別のタスク内にいるかどうかによって動作が変化するため、微妙なバグにつながる可能性があります。簡単には決められないもの。
.NET Framework で XxxAsync メソッドが行うのとまったく同じ方法で、イベントに基づく標準の非同期パターンを使用して、元の同期コンテキストで完了を通知する非同期メソッドのライブラリを作成しているとします (例: DownloadFileAsync
)。次のコードでこの動作を実装するのは非常に簡単なので、Task Parallel Library を実装に使用することにしました。
public class MyLibrary
{
public event EventHandler SomeOperationCompleted;
private void OnSomeOperationCompleted()
{
SomeOperationCompleted?.Invoke(this, EventArgs.Empty);
}
public void DoSomeOperationAsync()
{
Task.Factory.StartNew(() =>
{
Thread.Sleep(1000); // simulate a long operation
}, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default)
.ContinueWith(t =>
{
OnSomeOperationCompleted(); // trigger the event
}, TaskScheduler.FromCurrentSynchronizationContext());
}
}
これまでのところ、すべてがうまく機能しています。それでは、WPF または WinForms アプリケーションでボタン クリック時にこのライブラリを呼び出してみましょう。
private void Button_OnClick(object sender, EventArgs args)
{
var myLibrary = new MyLibrary();
myLibrary.SomeOperationCompleted += (s, e) => DoSomethingElse();
myLibrary.DoSomeOperationAsync(); // call that triggers the event asynchronously
}
private void DoSomethingElse() // the event handler
{
//...
Task.Factory.StartNew(() => Thread.Sleep(5000)); // simulate a long operation
//...
}
ここで、ライブラリ呼び出しを書いている人はTask
、操作が完了したときに新しい呼び出しを開始することを選択しました。珍しいことは何もありません。彼または彼女は、Web 上のどこにでもある例に従い、Task.Factory.StartNew
を指定せずに単純に使用TaskScheduler
します (2 番目のパラメーターで指定する簡単なオーバーロードはありません)。メソッドはDoSomethingElse
単独で呼び出されると正常に動作しますが、イベントによって呼び出されるとすぐにTaskFactory.Current
、ライブラリの継続から同期コンテキスト タスク スケジューラを再利用するため、UI がフリーズします。
特に 2 番目のタスク呼び出しが複雑な呼び出しスタックに埋もれている場合は、これを見つけるのに時間がかかることがあります。もちろん、すべてがどのように機能するかがわかれば、ここでの修正は簡単TaskScheduler.Default
です。スレッド プールで実行する予定のすべての操作を常に指定します。ただし、2 番目のタスクは別の外部ライブラリによって開始され、この動作を知らStartNew
ず、特定のスケジューラーなしで素朴に使用されている可能性があります。このケースはかなり一般的だと思います。
頭を悩ませた後、TPL を作成しているチームがデフォルトTaskScheduler.Current
の代わりに使用するという選択を理解できません。TaskScheduler.Default
- まったく明らかで
Default
はありません。デフォルトではありません! そして、ドキュメントは深刻に不足しています。 - によって使用される実際のタスク スケジューラ
Current
は、コール スタックに依存します。この動作で不変条件を維持するのは困難です。 - 最初にタスク作成オプションとキャンセル トークンを指定する必要があるため、タスク スケジューラを指定するのは面倒で
StartNew
、長い行が読みにくくなります。これは、拡張メソッドを作成するか、TaskFactory
を使用する を作成することで軽減できますDefault
。 - コール スタックをキャプチャすると、追加のパフォーマンス コストが発生します。
- タスクを実行中の別の親タスクに依存させたい場合は、コール スタック マジックに頼るのではなく、明示的に指定してコードを読みやすくすることを好みます。
この質問がかなり主観的に聞こえるかもしれませんが、なぜこのような振る舞いをするのかについて客観的な議論を見つけることができません。ここで何かが欠けていると確信しています。それが、私があなたに目を向けている理由です。