Unexpected behavior with wait inside ContinueWith block

I have a somewhat complex requirement for performing some tasks in parallel, and you need to wait for some of them to finish before continuing. Now I encounter unexpected behavior when I have a number of tasks that I want to perform in parallel, but inside the ContinueWith handler. I have illustrated a small example to illustrate the problem:

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");

The main template: - Task 1 should be performed in parallel with Task 2. - As soon as the first part of part 1 is completed, it should do several things in parallel. I want to complete as soon as everything is done.

I expect the following result:

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, - .

:

  • ?
  • , ?

!

+4
3

. 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.Run await , StartNew/ContinueWith. , ( Task.Run) .

StartNew ContinueWith.

+3

, , , . Task, ContinueWith(), , ContinueWith() . , await, Task, .

ContinueWith(), , , , .

, , . , , :

await Task.WhenAll(await task1, task2);

.. WhenAll() ContinueWith(), , . await , , , .

+3

async/lambdas StartNew :

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

Unwrap StartNew .

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

, Task.Factory.StartNew ContinueWith , , .

, Task.Factory.StartNew , , ( ) Task.Run , Task.Factory.StartNew, TaskCreationOptions TaskScheduler.

, Task.Factory. , StartNew , .

, , ContinueWith , , ( ) async/await , ContinueWith t TaskContinuationOptions TaskScheduler.

, # 5 await, catch finally, ContinueWith.

# 6:

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

# 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, (I factory):

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 await s.

-, , , (, MemoryStream ). , .

, Unwrap await s, , . , async/await, , , .

. , , async/await - , , , async, (ASP.NET, WCF, NServiceBus 6+ ..), - . Task.Yield. - , : .

If the asynchronous operation depends on the synchronization context, you can still use async/ awaitif you are in this context (in this case, think twice or more before using .ConfigureAwait(false)), otherwise start a new task using the task scheduler from the corresponding synchronization context.

+1
source

Source: https://habr.com/ru/post/1658234/


All Articles