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のいたるところで見られる例に従い、TaskScheduler
を指定せずにTask.Factory.StartNew
を使用します(2番目のパラメーターで簡単にオーバーロードを指定することはできません)。 DoSomethingElse
メソッドは、単独で呼び出すと正常に機能しますが、イベントによって呼び出されるとすぐに、TaskFactory.Current
がライブラリの継続から同期コンテキストタスクスケジューラを再利用するため、UIがフリーズします。
特に2番目のタスクコールがいくつかの複雑なコールスタックに埋め込まれている場合は、これを見つけるのに時間がかかることがあります。もちろん、すべてがどのように機能するかがわかっていれば、ここでの修正は簡単です。スレッドプールで実行する予定の操作には常にTaskScheduler.Default
を指定してください。ただし、2番目のタスクは別の外部ライブラリによって開始され、この動作を認識せず、特定のスケジューラなしで単純にStartNew
を使用する場合があります。私はこのケースがかなり一般的であることを期待しています。
頭を回した後、デフォルトとしてTaskScheduler.Current
の代わりにTaskScheduler.Default
を使用するTPLを作成するチームの選択を理解できません。
Default
はデフォルトではありません。そして、ドキュメントは非常に不足しています。Current
が使用する実際のタスクスケジューラは、コールスタックによって異なります。この動作で不変条件を維持することは困難です。StartNew
でタスクスケジューラを指定するのは面倒です。これは、拡張メソッドを作成するか、TaskFactory
を使用するDefault
を作成することで軽減できます。この質問はかなり主観的に聞こえるかもしれませんが、なぜこの振る舞いがそうであるのかについて、良い客観的な議論を見つけることができません。私はここで何かを見逃していると確信しています。それが私があなたに目を向けている理由です。
現在の行動は理にかなっていると思います。独自のタスクスケジューラを作成し、他のタスクを開始するタスクを開始した場合、おそらくすべてのタスクで、作成したスケジューラを使用する必要があります。
UIスレッドからタスクを開始するときにデフォルトのスケジューラーを使用する場合と使用しない場合があるのは奇妙なことに同意します。しかし、私がそれを設計していた場合、どうすればこれをより良くすることができるのかわかりません。
特定の問題について:
new Task(lambda).Start(scheduler)
だと思います。これには、タスクが何かを返す場合に型引数を指定する必要があるという欠点があります。 _TaskFactory.Create
_が型を推測します。Dispatcher.Invoke()
の代わりにTaskScheduler.FromCurrentSynchronizationContext()
を使用できます。[編集]以下は、Task.Factory.StartNew
が使用するスケジューラの問題のみを扱います。
ただし、Task.ContinueWith
にはTaskScheduler.Current
がハードコードされています。 [/編集]
まず、簡単な解決策があります-この投稿の下部を参照してください。
この問題の背後にある理由は単純です。デフォルトのタスクスケジューラ(TaskScheduler.Default
)だけでなく、TaskFactory
(TaskFactory.Scheduler
)のデフォルトのタスクスケジューラもあります。このデフォルトのスケジューラーは、作成時にTaskFactory
のコンストラクターで指定できます。
ただし、TaskFactory
の背後にあるTask.Factory
は次のように作成されます。
s_factory = new TaskFactory();
ご覧のとおり、TaskFactory
は指定されていません。 null
はデフォルトのコンストラクターに使用されます-TaskScheduler.Default
のほうがよいでしょう(同じ結果をもたらす「現在」が使用されているとドキュメントに記載されています)。
これにより、TaskFactory.DefaultScheduler
(プライベートメンバー)が再び実装されます。
private TaskScheduler DefaultScheduler
{
get
{
if (m_defaultScheduler == null) return TaskScheduler.Current;
else return m_defaultScheduler;
}
}
ここで、この動作の理由を認識できるはずです。Task.Factoryにはデフォルトのタスクスケジューラがないため、現在のタスクスケジューラが使用されます。
では、NullReferenceExceptions
を実行して、現在実行中のタスクがない場合(つまり、現在のTaskSchedulerがない場合)はどうしてでしょうか。
理由は簡単です:
public static TaskScheduler Current
{
get
{
Task internalCurrent = Task.InternalCurrent;
if (internalCurrent != null)
{
return internalCurrent.ExecutingTaskScheduler;
}
return Default;
}
}
TaskScheduler.Current
のデフォルトはTaskScheduler.Default
です。
私はこれを非常に不幸な実装と呼んでいます。
ただし、簡単な修正があります。デフォルトのTaskScheduler
のTask.Factory
をTaskScheduler.Default
に設定するだけです。
TaskFactory factory = Task.Factory;
factory.GetType().InvokeMember("m_defaultScheduler", BindingFlags.SetField | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly, null, factory, new object[] { TaskScheduler.Default });
かなり遅いですが、私の応答を手伝ってくれるといいのですが:-)
Task.Factory.StartNew()
の代わりに
次の使用を検討してください:Task.Run()
これは常にスレッドプールスレッドで実行されます。私は質問で説明されている同じ問題を抱えていましたが、これはこれを処理する良い方法だと思います。
このブログエントリを参照してください: http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx
それはまったく明白ではありません、デフォルトはデフォルトではありません!そして、ドキュメントは非常に不足しています。
Default
がデフォルトですが、常にCurrent
とは限りません。
他のユーザーがすでに回答しているように、スレッドプールでタスクを実行する場合は、Current
スケジューラをDefault
に渡すことにより、TaskFactory
スケジューラを明示的に設定する必要がありますまたはStartNew
メソッド。
あなたの質問にはライブラリが関わっていたので、答えは、ライブラリの外部のコードから見えるCurrent
スケジューラを変更するようなことはすべきではないと思います。つまり、SomeOperationCompleted
イベントを発生させるときにTaskScheduler.FromCurrentSynchronizationContext()
を使用しないでください。代わりに、次のようにします。
public void DoSomeOperationAsync() {
var context = SynchronizationContext.Current;
Task.Factory
.StartNew(() => Thread.Sleep(1000) /* simulate a long operation */)
.ContinueWith(t => {
context.Post(_ => OnSomeOperationCompleted(), null);
});
}
Default
スケジューラで明示的にタスクを開始する必要があるとは思いません。必要に応じて、呼び出し元にCurrent
スケジューラを決定させます。
指定していなくても、UIスレッドでタスクがスケジュールされている奇妙な問題をデバッグするために何時間も費やしました。問題はサンプルコードが示したとおりであることがわかりました。タスクの継続がUIスレッドでスケジュールされ、その継続のどこかで新しいタスクが開始され、UIスレッドでスケジュールされました。現在実行中のタスクには特定のTaskScheduler
セット。
幸い、それは私が所有するすべてのコードなので、新しいタスクを開始するときにコードがTaskScheduler.Default
を指定していることを確認することで修正できますが、幸運でない場合は、代わりにDispatcher.BeginInvoke
を使用することをお勧めしますUIスケジューラを使用する方法。
したがって、代わりに:
var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
var task = Task.Factory.StartNew(() => Thread.Sleep(5000));
task.ContinueWith((t) => UpdateUI(), uiScheduler);
試してください:
var uiDispatcher = Dispatcher.CurrentDispatcher;
var task = Task.Factory.StartNew(() => Thread.Sleep(5000));
task.ContinueWith((t) => uiDispatcher.BeginInvoke(new Action(() => UpdateUI())));
ただし、少し読みにくくなります。