问题
I am testing the asynchronousity of C# async/await and came across a surprise where the subsequent code for ContinueWith does not wait for the previous task to complete:
public async Task<int> SampleAsyncMethodAsync(int number,string id)
{
Console.WriteLine($"Started work for {id}.{number}");
ConcurrentBag<int> abc = new ConcurrentBag<int>();
await Task.Run(() => { for (int count = 0; count < 30; count++) { Console.WriteLine($"[{id}] Run: {number}"); abc.Add(count); } });
Console.WriteLine($"Completed work for {id}.{number}");
return abc.Sum();
}
Which is executed with the below test method:
[Test]
public void TestAsyncWaitForPreviousTask()
{
for (int count = 0; count < 3; count++)
{
int scopeCount = count;
var c = SampleAsyncMethodAsync(0, scopeCount.ToString())
.ContinueWith((prevTask) =>
{
return SampleAsyncMethodAsync(1, scopeCount.ToString());
})
.ContinueWith((prevTask2) =>
{
return SampleAsyncMethodAsync(2, scopeCount.ToString());
});
}
}
The output shows execution for runs 0.0,1.0 and 2.0 executes asynchronously correctly but subsequent x.1 and x.2 get started almost immediately and x.2 actually completes before x.1. E.g. as logged below:
[2] Run: 0
[2] Run: 0
[2] Run: 0
Completed work for 2.0
Started work for 0.1
Started work for 0.2 <-- surprise!
[0] Run: 2
[0] Run: 2
[0] Run: 2
[0] Run: 2
[0] Run: 2
It seems the continueWith will only wait on the first task (0) regardless of subsequent chains. I can solve the problem by nesting the second ContinueWith within the first Continuewith block.
Is there something wrong with my code? I'm assuming Console.WriteLine respects FIFO.
回答1:
In short, you expect ContinueWith
to wait for a previously returned object. Returning an object (even a Task
) in ContinueWith
action does nothing with returned value, it does not wait for it to complete, it returns it and passes to the continuation if exists.
The following thing does happen:
- You run
SampleAsyncMethodAsync(0, scopeCount.ToString())
When it is completed, you execute the continuation 1:
return SampleAsyncMethodAsync(1, scopeCount.ToString());
and when it stumbles upon
await Task.Run
, it returns a task. I.e., it does not wait for SampleAsyncMethodAsync to complete.- Then, continuation 1 is considered to be completed, since it has returned a value (task)
- Continuation 2 is run.
If you wait for every asynchronous method manually, then it will run consequently:
for (int count = 0; count < 3; count++)
{
int scopeCount = count;
var c = SampleAsyncMethodAsync(0, scopeCount.ToString())
.ContinueWith((prevTask) =>
{
SampleAsyncMethodAsync(1, scopeCount.ToString()).Wait();
})
.ContinueWith((prevTask2) =>
{
SampleAsyncMethodAsync(2, scopeCount.ToString()).Wait();
});
}
Using ContinueWith(async t => await SampleAsyncMethodAsync...
doesn't work as well, since it results into wrapped Task<Task>
result (explained well here).
Also, you can do something like:
for (int count = 0; count < 3; count++)
{
int scopeCount = count;
var c = SampleAsyncMethodAsync(0, scopeCount.ToString())
.ContinueWith((prevTask) =>
{
SampleAsyncMethodAsync(1, scopeCount.ToString())
.ContinueWith((prevTask2) =>
{
SampleAsyncMethodAsync(2, scopeCount.ToString());
});
});
}
However, it creates some sort of callback hell and looks messy.
You can use await
to make this code a little cleaner:
for (int count = 0; count < 3; count++)
{
int scopeCount = count;
var d = Task.Run(async () => {
await SampleAsyncMethodAsync(0, scopeCount.ToString());
await SampleAsyncMethodAsync(1, scopeCount.ToString());
await SampleAsyncMethodAsync(2, scopeCount.ToString());
});
}
Now, it runs 3 tasks for 3 counts, and each task will consequently run asynchronous method with number
equal to 1, 2, and 3.
来源:https://stackoverflow.com/questions/46235216/c-sharp-chained-continuewith-not-waiting-for-previous-task-to-complete