69

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
  • コール スタックをキャプチャすると、追加のパフォーマンス コストが発生します。
  • タスクを実行中の別の親タスクに依存させたい場合は、コール スタック マジックに頼るのではなく、明示的に指定してコードを読みやすくすることを好みます。

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

4

5 に答える 5

20

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

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

あなたの特定の問題について:

  • 指定したスケジューラーで新しいタスクを開始する最も簡単な方法は、new Task(lambda).Start(scheduler). これには、タスクが何かを返す場合に型引数を指定する必要があるという欠点があります。TaskFactory.Createタイプを推測できます。
  • Dispatcher.Invoke()の代わりに使用できますTaskScheduler.FromCurrentSynchronizationContext()
于 2011-07-23T14:22:22.630 に答える
8

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

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

この問題の背後にある理由は単純です。デフォルトのタスクスケジューラ(TaskScheduler.Default)だけでなく、()のデフォルトのタスクスケジューラもありTaskFactoryますTaskFactory.Scheduler。このデフォルトのスケジューラーは、TaskFactory作成時にのコンストラクターで指定できます。

ただし、TaskFactory背後Task.Factoryは次のように作成されます。

s_factory = new TaskFactory();

ご覧のとおり、noTaskSchedulerは指定されています。nullデフォルトのコンストラクターに使用されます-より良いでしょうTaskScheduler.Default(ドキュメントには、同じ結果をもたらす「Current」が使用されると記載されています)。これもまた、 (プライベートメンバー)
の実装につながります。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.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 });

かなり遅いですが、私は私の応答を助けることができると思います:-)

于 2011-11-20T13:39:40.083 に答える
5

それ以外のTask.Factory.StartNew()

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

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

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

于 2013-08-28T07:40:08.010 に答える
3

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

Defaultはデフォルトですが、常にであるとは限りませんCurrent

他の人がすでに答えているように、タスクをスレッドプールで実行する場合は、スケジューラをまたはメソッドにCurrent渡すことによってスケジューラを明示的に設定する必要があります。DefaultTaskFactoryStartNew

Currentただし、あなたの質問にはライブラリが関係しているので、答えは、ライブラリ外のコードに表示されるスケジューラを変更するようなことは何もしてはいけないということだと思います。つまり、イベントを発生さTaskScheduler.FromCurrentSynchronizationContext()せるときに使用しないでください。SomeOperationCompleted代わりに、次のようにします。

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

于 2013-02-27T17:56:46.680 に答える
0

指定していないにもかかわらず、タスクが 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())));

少し読みにくいですが。

于 2013-05-31T09:30:11.553 に答える