Waiting For More Than One Concurrent Await Operation

Waiting for more than one concurrent await operation

TL;DR

Don't use the pattern in the question where you get the promises, and then separately wait on them; instead, use Promise.all (at least for now):

const [value1, value2] = await Promise.all([getValue1Async(), getValue2Async()]);

While your solution does run the two operations in parallel, it doesn't handle rejection properly if both promises reject.

Details:

Your solution runs them in parallel, but always waits for the first to finish before waiting for the second. If you just want to start them, run them in parallel, and get both results, it's just fine. (No, it isn't, keep reading...) Note that if the first takes (say) five seconds to complete and the second fails in one second, your code will wait the full five seconds before then failing.

Sadly, there isn't currently await syntax to do a parallel wait, so you have the awkwardness you listed, or Promise.all. (There's been discussion of await.all or similar, though; maybe someday.)

The Promise.all version is:

const [value1, value2] = await Promise.all([getValue1Async(), getValue2Async()]);

...which is more concise, and also doesn't wait for the first operation to complete if the second fails quickly (e.g., in my five seconds / one second example above, the above will reject in one second rather than waiting five). Also note that with your original code, if the second promise rejects before the first promise resolves, you may well get a "unhandled rejection" error in the console (you do currently with Chrome v61; update: more recent versions have more interesting behavior), although that error is arguably spurious (because you do, eventually, handle the rejection, in that this code is clearly in an async function¹ and so that function will hook rejection and make its promise reject with it) (update: again, changed). But if both promises reject, you'll get a genuine unhandled rejection error because the flow of control never reaches const value2 = await p2; and thus the p2 rejection is never handled.

Unhandled rejections are a Bad Thing™ (so much so that soon, Node.js will abort the process on truly unhandled rejections, just like unhandled exceptions — because that's what they are), so best to avoid the "get the promise then await it" pattern in your question.

Here's an example of the difference in timing in the failure case (using 500ms and 100ms rather than 5 seconds and 1 second), and possibly also the arguably-spurious unhandled rejection error (open the real browser console to see it):

const getValue1Async = () => {

return new Promise(resolve => {

setTimeout(resolve, 500, "value1");

});

};

const getValue2Async = () => {

return new Promise((resolve, reject) => {

setTimeout(reject, 100, "error");

});

};

// This waits the full 500ms before failing, because it waits

// on p1, then on p2

(async () => {

try {

console.time("separate");

const p1 = getValue1Async();

const p2 = getValue2Async();

const value1 = await p1;

const value2 = await p2;

} catch (e) {

console.error(e);

}

console.timeEnd("separate");

})();

// This fails after just 100ms, because it doesn't wait for p1

// to finish first, it rejects as soon as p2 rejects

setTimeout(async () => {

try {

console.time("Promise.all");

const [value1, value2] = await Promise.all([getValue1Async(), getValue2Async()]);

} catch (e) {

console.timeEnd("Promise.all", e);

}

}, 1000);
Open the real browser console to see the unhandled rejection error.

Call async/await functions in parallel

You can await on Promise.all():

await Promise.all([someCall(), anotherCall()]);

To store the results:

let [someResult, anotherResult] = await Promise.all([someCall(), anotherCall()]);

Note that Promise.all fails fast, which means that as soon as one of the promises supplied to it rejects, then the entire thing rejects.

const happy = (v, ms) => new Promise((resolve) => setTimeout(() => resolve(v), ms))
const sad = (v, ms) => new Promise((_, reject) => setTimeout(() => reject(v), ms))

Promise.all([happy('happy', 100), sad('sad', 50)])
.then(console.log).catch(console.log) // 'sad'

Using async/await for multiple tasks

int[] ids = new[] { 1, 2, 3, 4, 5 };
Parallel.ForEach(ids, i => DoSomething(1, i, blogClient).Wait());

Although you run the operations in parallel with the above code, this code blocks each thread that each operation runs on. For example, if the network call takes 2 seconds, each thread hangs for 2 seconds w/o doing anything but waiting.

int[] ids = new[] { 1, 2, 3, 4, 5 };
Task.WaitAll(ids.Select(i => DoSomething(1, i, blogClient)).ToArray());

On the other hand, the above code with WaitAll also blocks the threads and your threads won't be free to process any other work till the operation ends.

Recommended Approach

I would prefer WhenAll which will perform your operations asynchronously in Parallel.

public async Task DoWork() {

int[] ids = new[] { 1, 2, 3, 4, 5 };
await Task.WhenAll(ids.Select(i => DoSomething(1, i, blogClient)));
}

In fact, in the above case, you don't even need to await, you can just directly return from the method as you don't have any continuations:

public Task DoWork() 
{
int[] ids = new[] { 1, 2, 3, 4, 5 };
return Task.WhenAll(ids.Select(i => DoSomething(1, i, blogClient)));
}

To back this up, here is a detailed blog post going through all the
alternatives and their advantages/disadvantages: How and Where Concurrent Asynchronous I/O with ASP.NET Web API

Running multiple async tasks and waiting for them all to complete

Both answers didn't mention the awaitable Task.WhenAll:

var task1 = DoWorkAsync();
var task2 = DoMoreWorkAsync();

await Task.WhenAll(task1, task2);

The main difference between Task.WaitAll and Task.WhenAll is that the former will block (similar to using Wait on a single task) while the latter will not and can be awaited, yielding control back to the caller until all tasks finish.

More so, exception handling differs:

Task.WaitAll:

At least one of the Task instances was canceled -or- an exception was thrown during the execution of at least one of the Task instances. If a task was canceled, the AggregateException contains an OperationCanceledException in its InnerExceptions collection.

Task.WhenAll:

If any of the supplied tasks completes in a faulted state, the returned task will also complete in a Faulted state, where its exceptions will contain the aggregation of the set of unwrapped exceptions from each of the supplied tasks.

If none of the supplied tasks faulted but at least one of them was canceled, the returned task will end in the Canceled state.

If none of the tasks faulted and none of the tasks were canceled, the resulting task will end in the RanToCompletion state.
If the supplied array/enumerable contains no tasks, the returned task will immediately transition to a RanToCompletion state before it's returned to the caller.

Why do we need more than one `await` statement in a C# method?

await will wait until the operation is not executed. So you has 2 async operations, that's why you need to use await.

One await for each async operation (method).

So, you have 3 async methods. You can call it without await, but it will crash. When you call it without await, it will start to execute in another thread, and thread where SeedAsync is executing, will not wait until InsertAsync is executed. It will start second InsertAsync at the same time

So, in your case, you can insert values without await. It will work. But in common case it's better to use await. Because often order of operations is important. await is allow to control the order of operations

Sometimes you need to run some tasks and then wait for all. Then you can use Task.WhenAll(t1,t2,t3)



Related Topics



Leave a reply



Submit