web-dev-qa-db-ja.com

TaskScheduler.CurrentがデフォルトのTaskSchedulerである理由

Task Parallel Libraryは素晴らしく、ここ数か月間私はそれを頻繁に使用しました。ただし、本当に気になることがある: TaskScheduler.CurrentTaskScheduler.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を作成することで軽減できます。
  • 呼び出しスタックのキャプチャには、追加のパフォーマンスコストがあります。
  • タスクを実行中の別の親タスクに依存させたい場合、コールスタックマジックに依存するのではなく、コードを読みやすくするために明示的に指定することを好みます。

この質問はかなり主観的に聞こえるかもしれませんが、なぜこの振る舞いがそうであるのかについて、良い客観的な議論を見つけることができません。私はここで何かを見逃していると確信しています。それが私があなたに目を向けている理由です。

63

現在の行動は理にかなっていると思います。独自のタスクスケジューラを作成し、他のタスクを開始するタスクを開始した場合、おそらくすべてのタスクで、作成したスケジューラを使用する必要があります。

UIスレッドからタスクを開始するときにデフォルトのスケジューラーを使用する場合と使用しない場合があるのは奇妙なことに同意します。しかし、私がそれを設計していた場合、どうすればこれをより良くすることができるのかわかりません。

特定の問題について:

  • 指定したスケジューラで新しいタスクを開始する最も簡単な方法は、new Task(lambda).Start(scheduler)だと思います。これには、タスクが何かを返す場合に型引数を指定する必要があるという欠点があります。 _TaskFactory.Create_が型を推測します。
  • Dispatcher.Invoke()の代わりにTaskScheduler.FromCurrentSynchronizationContext()を使用できます。
18
svick

[編集]以下は、Task.Factory.StartNewが使用するスケジューラの問題のみを扱います。
ただし、Task.ContinueWithにはTaskScheduler.Currentがハードコードされています。 [/編集]

まず、簡単な解決策があります-この投稿の下部を参照してください。

この問題の背後にある理由は単純です。デフォルトのタスクスケジューラ(TaskScheduler.Default)だけでなく、TaskFactoryTaskFactory.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です。

私はこれを非常に不幸な実装と呼んでいます。

ただし、簡単な修正があります。デフォルトのTaskSchedulerTask.FactoryTaskScheduler.Defaultに設定するだけです。

TaskFactory factory = Task.Factory;
factory.GetType().InvokeMember("m_defaultScheduler", BindingFlags.SetField | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly, null, factory, new object[] { TaskScheduler.Default });

かなり遅いですが、私の応答を手伝ってくれるといいのですが:-)

5
Matthias

Task.Factory.StartNew()の代わりに

次の使用を検討してください:Task.Run()

これは常にスレッドプールスレッドで実行されます。私は質問で説明されている同じ問題を抱えていましたが、これはこれを処理する良い方法だと思います。

このブログエントリを参照してください: http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx

4
testalino

それはまったく明白ではありません、デフォルトはデフォルトではありません!そして、ドキュメントは非常に不足しています。

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スケジューラを決定させます。

2
Rory MacLeod

指定していなくても、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())));

ただし、少し読みにくくなります。

0
René