Many awaits for Async method, or a single await fo

2020-07-28 10:27发布

问题:

Suppose we have to write down on database a list of 1000 elements, through an async flow. Is it better to await 1000 times an asynchronous insert statement, or to wrap all the 1000 inserts in one single synchronous method encapsulated into a Task.Run statement, awaiting one single time?

For example, SqlCommand has every method coupled with his async version. In this case, we have an insert statement, so we can call ExecuteNonQuery or ExecuteNonQueryAsync.

Often, on async/await guidelines, we read that if you have an asynchronous version available for some method, you should use it. So suppose we write:

async Task Save(IEnumerable<Savable> savables)
{
    foreach(var savable in savables)
    {
        //create SqlCommand somehow
        var sqlCmd = CreateSqlCommand(savable);

        //use asynchronous version
        await sqlCmd.ExecuteNonQueryAsync();
    }
}

This code is very clear. However, every time it goes out from the await part, it also returns on the UI thread, and then back in a background thread in the next await encountered, and so on (doesn't it?). This implies that the user can see some lag, since the UI Thread is continously interrupted by the continuation of the await to execute the next foreach cycle, and in that fraction of time the UI freezes a bit.

I want to know if I better write code like this:

async Task Save(IEnumerable<Savable> savables)
{
    await Task.Run(() =>
    {
        foreach(var savable in savables)
        {
            //create SqlCommand somehow
            var sqlCmd = CreateSqlCommand(savable);

            //use synchronous version
            sqlCmd.ExecuteNonQuery();
        }
    });
}

In this way, the whole foreach is executed on the secondary thread, without a continuous switch between the UI thread and the secondary one. This implies that the UI thread is free to update the View for the entire duration of the foreach (for example a spinner or a progress bar), that is, no lag is perceived by the user.

Am I right? or am I missing something about "async all the way down"?

I'm not looking for simple opinion-based answers, I'm looking for an explanation of async/await guidelines in case like that and for the best way to resolve it.

EDIT:

I've read this question but it is not the same. THAT question is about the choice of a SINGLE await on an async method versus a single Task.Run await. THIS question is about the consequences of calling 1000 await and the resources' overhead due to the continuous switching among threads.

回答1:

Your analysis is largely correct. You seem to overestimate the burden that this would place on the UI thread; the actual work that it would be asked to do is fairly small, so odds are that it would be able to keep up fine, but it's possible that you'd be doing enough that it couldn't, so you're right to be interested in not performing the continuations on the UI thread.

What you're missing of course is the preferred way of avoiding all of the call backs to the UI thread. When you await an operation, if you don't actually need the rest of the method to return back to the original context you can simply add ConfigureAwait(false) to the end of the task that you're awaiting. This will prevent the continuation from running in the current context (which is the UI thread) and instead let the continuation run in a thread pool thread.

Using ConfigureAwait(false) allows you to avoid the UI being responsible for non-UI work unnecessarily while also preventing you from needing to schedule thread pool threads to do more work than they need to do.

Of course, if the work that you end up doing after your continuation is actually going to do UI work, then that method shouldn't be using ConfigureAwait(false);, because it actually wants to schedule the continuation on the UI thread.



回答2:

This all depends on what the UI is expecting. If the UI depends on the operation being complete in order to retain a valid state then you have no choice but to wait for the async task or use a call on the main UI thread as you would normally.

Threading is great for tasks that take a long time but are NOT required by the calling thread immediately or the calling thread is dependant on the result of the operation.

Tasks are no there to speed things up, they are there for efficiency. So you could keep the task thread and raise an event with an 'operation completed' leaving the UI to operate normally while this operation is happening, but like I said if the UI thread depends on the result, you have no choice but to wait.

If you go via the event route, you can update your UI asynchronously with the results as they come in



回答3:

There is one important difference between the two solutions. The first one is single-threaded (unless you use ConfigureAwait(false) as suggested by @Servy), while the second one is multi-threaded.

Multi-threadedness always introduces extra complexity to your program. You should start by asking yourself if you're willing to trade that for the benefits you might gain.