2016-10-19 34 views
4

私は、いくつかのタスクを並行して実行し、それらの中のいくつかが終了してから処理を続けるという少し複雑な要件があります。さて、私は、私が並行して実行したいが、ContinueWithハンドラの中にいくつかのタスクがあるとき、予期せぬ動作に遭遇しています。私はこの問題を説明するために少量のサンプルを泡立てた:ContinueWithブロック内で予期せぬ動作が発生する

var task1 = Task.Factory.StartNew(() => 
{ 
    Console.WriteLine("11"); 
    Thread.Sleep(1000); 
    Console.WriteLine("12"); 
}).ContinueWith(async t => 
{ 
    Console.WriteLine("13"); 
    var innerTasks = new List<Task>(); 
    for (var i = 0; i < 10; i++) 
    { 
     var j = i; 
     innerTasks.Add(Task.Factory.StartNew(() => 
     { 
      Console.WriteLine("1_" + j + "_1"); 
      Thread.Sleep(500); 
      Console.WriteLine("1_" + j + "_2"); 
     })); 
    } 
    await Task.WhenAll(innerTasks.ToArray()); 
    //Task.WaitAll(innerTasks.ToArray()); 
    Thread.Sleep(1000); 
    Console.WriteLine("14"); 
}); 
var task2 = Task.Factory.StartNew(() => 
{ 
    Console.WriteLine("21"); 
    Thread.Sleep(1000); 
    Console.WriteLine("22"); 
}).ContinueWith(t => 
{ 
    Console.WriteLine("23"); 
    Thread.Sleep(1000); 
    Console.WriteLine("24"); 
}); 
Console.WriteLine("1"); 
await Task.WhenAll(task1, task2); 
Console.WriteLine("2"); 

基本的なパターンである。 - タスク1、タスク2 と並行して実行されるべきである - パート1の最初の部分が完了したら、それは並行していくつかのことを行う必要があります。私はすべてが完了したら、完了したい。

1 <- Start 
11/21 <- The initial task start 
12/22 <- The initial task end 
13/23 <- The continuation task start 
Some combinations of "1_[0..9]_[1..2]" and 24 <- the "inner" tasks of task 1 + the continuation of task 2 end 
14 <- The end of the task 1 continuation 
2 <- The end 

代わりに、何が起こるか、await Task.WhenAll(innerTasks.ToArray());はない「ブロック」完成から継続タスクないことである:

は、私は次のような結果を期待しています。したがって、外側のタスクは、外側のawait Task.WhenAll(task1, task2);が完了した後に実行されます。結果のようなものです:

1 <- Start 
11/21 <- The initial task start 
12/22 <- The initial task end 
13/23 <- The continuation task start 
Some combinations of "1_[0..9]_[1..2]" and 24 <- the "inner" tasks of task 1 + the continuation of task 2 end 
2 <- The end 
Some more combinations of "1_[0..9]_[1..2]" <- the "inner" tasks of task 1 
14 <- The end of the task 1 continuation 

場合は、代わりに、私はTask.WaitAll(innerTasks.ToArray())を使用し、すべてが期待通りに動作するようです。もちろん、WaitAllを使用したくないので、スレッドをブロックしません。

私の質問は以下のとおりです。

  1. は、なぜ、この予期しない動作が発生していますか?
  2. どのようにスレッドをブロックすることなく状況を改善できますか?

ありがとうございました!

+0

質問1への回答は、ContinueWithがその内容を非同期で実行し、何も待っていないためです。あなたはtask1を待っていますが、continueWithはその一部ではありません。 – user1676075

+0

希望の結果を得るために、「continueWith」パーツをtask1のインラインで(continueWithではなく)インライン化したいと思うようです。それは本当にあなたが言っていることです。タスク1でこれらのステップを実行し、次にこれらのタスクを実行します。それはcontinueWithを必要としません。 – user1676075

+0

インライン化は実際にはオプションではありません。最初のタスクは既に非同期なので、継続が実行される前に完了する必要があるからです。私はもちろん、ラッピングタスクを使用することができ、その中で、最初の操作を待ってから、他の操作を待ちます。しかし、私は十分なはずだと思うところで2つの仕事をするでしょう。一方、 'ContinueWith'は' Task 'を返すことでそれをやっているようですので、余分なタスクがどこから来るかは問題ではないと思います。私が間違っているなら、私を修正してください。 –

答えて

2

不適切なツールを使用しています。 StartNewの代わりにTask.Runを使用してください。代わりにContinueWithの、awaitを使用します。

var task1 = Task1(); 
var task2 = Task2(); 
Console.WriteLine("1"); 
await Task.WhenAll(task1, task2); 
Console.WriteLine("2"); 

private async Task Task1() 
{ 
    await Task.Run(() => 
    { 
    Console.WriteLine("11"); 
    Thread.Sleep(1000); 
    Console.WriteLine("12"); 
    }); 
    Console.WriteLine("13"); 
    var innerTasks = new List<Task>(); 
    for (var i = 0; i < 10; i++) 
    { 
    innerTasks.Add(Task.Run(() => 
    { 
     Console.WriteLine("1_" + i + "_1"); 
     Thread.Sleep(500); 
     Console.WriteLine("1_" + i + "_2"); 
    })); 
    await Task.WhenAll(innerTasks); 
    } 
    Thread.Sleep(1000); 
    Console.WriteLine("14"); 
} 

private async Task Task2() 
{ 
    await Task.Run(() => 
    { 
    Console.WriteLine("21"); 
    Thread.Sleep(1000); 
    Console.WriteLine("22"); 
    }); 
    Console.WriteLine("23"); 
    Thread.Sleep(1000); 
    Console.WriteLine("24"); 
} 

Task.Runawait彼らはStartNew/ContinueWithで予期しない動作の多くを修正するためにここに優れています。特に、非同期デリゲートと(Task.Runの場合)常にスレッドプールを使用します。

why you shouldn't use StartNewwhy you shouldn't use ContinueWithについては、私のブログに詳しい情報があります。

var task = Task.Factory.StartNew(async() => { /* ... */ }); 
task.Wait(); 
task.Result.Wait(); 
// consume task.Result.Result 

それともStartNewの結果に拡張メソッドUnwrapを使用してを待つ:StartNewと非同期メソッド/ラムダを使用する場合

+0

ありがとうございます。私は、コードをはるかに簡潔にしていると思います。 'Task.Run(..)'や 'Task.Factory.StartNew'に関して、私は様々な意見を読んでおり、' Task.Run(..) 'を使って問題を経験し、 '.StartNew(.. ) 'と言っているが、今はその説明を見つけることができない。いくつかのコードの並べ替え( 'await Task.WhenAll(innerTasks);'は 'for'ループの外にあるべきです)と、' i'で修正されたクロージャの問題から、あなたのコードは魅力的に機能します。私は、非同期メソッド内で待機し、呼び出しメソッドに制御を返すことが不足していました。 –

+0

私はあなたのコードが誤った場所にある 'await Task.WhenAll(innerTasks);'と元のコードで同じミスを繰り返すことに気づき、変更されたクロージャーにアクセスしました。これを反映するように私のコードを更新しました。もう一度おねがいします。あなたの答えは大きな助けとなりました。 –

3

in the commentsのように、表示されている内容は正常です。 ContinueWith()によって返されたTaskは、デリゲートが渡され、ContinueWith()によって呼び出されたときに実行が完了すると完了します。これは、匿名メソッドがawaitステートメントを初めて使用するときに発生し、デリゲートは、匿名メソッド全体の最終的な完了を表すTaskオブジェクト自体を返します。

だけContinueWith()タスクで待機している、そしてこのタスクは唯一の匿名メソッドを表しタスクではなく、そのタスクの完了の可用性を表しているので、あなたのコードは待ちません。

あなたの例から、最良の修正が何であるかは明確ではありません。しかし、あなたがこの小さな変化をした場合、それはあなたが望むことをするでしょう:

です。 WhenAll()コールでは、ContinueWith()タスク自体を待たずに、というタスクでは、タスクが最終的に復帰することになります。そのタスクが利用可能になるのを待つ間にスレッドをブロックしないようにするには、ここでawaitを使用してください。

+0

ありがとう、私は気付きませんでした.ContinueWith()は、タスク>を返します。私はそれが単純にカスケードし、結果として得られるタスクはチェーンの「最後の」タスクになると考えました。しかし、私がそれほど得意ではないのは、 'await Task.WhenAll(...) 'の代わりに' Task.WaitAll(...) 'を使うと、期待される結果が得られるからです。元のタスクが既に終了しているときにも、この「待機」が起こっているので、私は考えてみたいと思います。あなたが私にこれを戸惑わせることができれば、私はあなたの答えを喜んで受け入れるでしょう。 –

+0

_ "なぜTask.WaitAll(...)を使用すると期待される結果が得られるのでしょうか?" - あなたの投稿は 'WaitAll(innerTasks.ToArray()) 'を使ってのみ記述されます。このようにしてバグが発生しないようにする理由は、匿名メソッドの 'await'ステートメントを削除すると、メソッドが完了する前に返されてしまうためです。 'WaitAll()'だけの場合、メソッドは、メソッド全体が完了するまで( 'WaitAll()'の呼び出しを含む)スレッドをブロックします。後で続けるために 'await'式で返します。 –

+0

うーん、私はまだ、ここに関わるコンセプトをまだ理解していないと思います。あなたが今までに書いたことから、私は、タスクの '.WhenAll()'が元のタスクが完了して完了するのを待つだろうと考えました。しかし、今では、外部タスクが完了したとマークされる前に、内部タスクが実行されると言っています。継続の中で 'await'を使用しないと、継続の全体が実行された後にのみ、完了したとして外部のタスクがマークされます。しかし、私が行うと、内部タスクが別の非同期の結果を待つようになると、完了した外部タスクがマークされます。 –

1

、あなたは返さタスクに含まれるタスクを待つのいずれかそれが返すタスク。

var task = Task.Factory.StartNew(async() => { /* ... */ }) 
    .Unwrap(); 
task.Wait(); 
// consume task.Result 

以下の議論はTask.Factory.StartNewContinueWithは、このようなタスクスケジューラを提供していないときは、作成または継続オプションを提供したりしていないときなど、具体的な例では避けるべきであることをラインに沿って行きます。

私はTask.Factory.StartNewを使用すべきではないことに同意していないが、私はあなたがTaskCreationOptionsTaskSchedulerをとらないTask.Factory.StartNewメソッドオーバーロードを使用してどこでも使用(または使用することを検討してください)Task.Runをすべきであることに同意します。

これはデフォルトのTask.Factoryにのみ適用されます。私は、自分のニーズに合った工場固有のデフォルトを設定していたので、オプションタスクスケジューラなしでStartNewオーバーロードを使用することを選択したカスタムタスクファクトリを使用しました。

同様に、私は使用すべきではないContinueWith、私はあなたがTaskContinuationOptionsTaskSchedulerをとらないContinueWithメソッドオーバーロードを使用してどこでも使用(または使用することを検討してください)async/awaitなければならないことに同意することを同意しません。例えば

は、C#5まで、catch及びfinallyブロックで支持されていないawaitの制限を回避するための最も実用的な方法は、ContinueWithを使用することです。

C#6:

try 
{ 
    return await something; 
} 
catch (SpecificException ex) 
{ 
    await somethingElse; 
    // throw; 
} 
finally 
{ 
    await cleanup; 
} 

同等の前にC#6:私は言いましたよう

return await something 
    .ContinueWith(async somethingTask => 
    { 
     var ex = somethingTask.Exception.InnerException as SpecificException; 
     if (ex != null) 
     { 
      await somethingElse; 
      // await somethingTask; 
     } 
    }, 
     CancellationToken.None, 
     TaskContinuationOptions.DenyChildAttach | TaskContinuationOptions.NotOnRanToCompletion, 
     TaskScheduler.Default) 
    .Unwrap() 
    .ContinueWith(async catchTask => 
    { 
     await cleanup; 
     await catchTask; 
    }, 
     CancellationToken.None, 
     TaskContinuationOptions.DenyChildAttach, 
     TaskScheduler.Default) 
    .Unwrap(); 

があるので、いくつかのケースでは、私は特定のデフォルト値を持つTaskFactoryを持って、私はいくつかの拡張機能を定義しましたTaskFactoryを受け取るメソッドは、引数の1つを渡さないエラーの可能性を減らします(私はいつも工場を渡すことを忘れてしまいます)。

public static Task ContinueWhen(this TaskFactory taskFactory, Task task, Action<Task> continuationAction) 
{ 
    return task.ContinueWith(continuationAction, taskFactory.CancellationToken, taskFactory.ContinuationOptions, taskFactory.Scheduler); 
} 

public static Task<TResult> ContinueWhen<TResult>(this TaskFactory taskFactory, Task task, Func<Task, TResult> continuationFunction) 
{ 
    return task.ContinueWith(continuationFunction, taskFactory.CancellationToken, taskFactory.ContinuationOptions, taskFactory.Scheduler); 
} 

// Repeat with argument combinations: 
// - Task<TResult> task (instead of non-generic Task task) 
// - object state 
// - bool notOnRanToCompletion (useful in C# before 6) 

使用法:

// using namespace that contains static task extensions class 
var task = taskFactory.ContinueWhen(existsingTask, t => Continue(a, b, c)); 
var asyncTask = taskFactory.ContinueWhen(existingTask, async t => await ContinueAsync(a, b, c)) 
    .Unwrap(); 

私は、タスクを返すデリゲートをアンラップに同じメソッド名をオーバーロードしないことによってTask.Runを模倣しないことに決めたが、それはあなたが望むものをいつも本当にありません。実際には、ContinueWhenAsync拡張メソッドを実装していないので、Unwrapまたは2つのawaitを使用する必要があります。

多くの場合、これらの継続はI/O非同期操作であり、処理前および処理後のオーバーヘッドは、最初の降伏点まで同期して実行されるかどうか気にしないでください。 (例えば、基礎となるMemoryStreamまたは擬似DBアクセスを使用して)同期を完了します。また、それらのほとんどは同期コンテキストに依存しません。

Unwrap拡張メソッドまたは2つのawaitを適用するたびに、タスクがこのカテゴリに該当するかどうかを確認する必要があります。その場合は、仕事を開始するよりも、おそらくasync/awaitが良い選択です。

同期オーバーヘッドが無視できない非同期操作では、新しいタスクを開始することが望ましい場合があります。それでも、async/awaitがさらに良い選択肢であるという顕著な例外は、フレームワークまたはホスト(ASP.NET、WCF、NServiceBus 6+など)によって呼び出されるasyncメソッドなど、コードが最初から非同期である場合です。 オーバーヘッドはあなたの実際のビジネスです。長い処理の場合は、Task.Yieldの使用を検討することがあります。非同期コードの原則の1つはあまり細かい粒度ではありませんが、あまりに粗い粒度も悪いです。大量のタスクがキューに入れられた軽量タスクの処理を妨げる可能性があります。非同期操作が同期コンテキストに依存する場合、あなたはまだ、そうでない場合は、使用して新しいタスクを開始しますが、そのコンテキスト内なら(以上.ConfigureAwait(false)を使用する前に、この場合には、よく考えて)async/awaitを使用することができます

タスクスケジューラをそれぞれの同期コンテキストから削除する。

+0

ありがとうございます。詳しい説明はありません。これは本当に関係するすべての概念を理解するのに役立ちます。しかし、@ stephen-clearyの答えを正しい答えとして守ります。問題を解決し、コードをすばらしく簡潔にします。 –

関連する問題