Home > front end >  How to write async methods with await but make them as short as with Task.Run
How to write async methods with await but make them as short as with Task.Run

Time:01-03

After browsing different await vs Task.Run questions on SO my takeaway is that await is better for I/O operations and Task.Run for CPU-intensive operations. However the code with await seems to always be longer than Task.Run. Below is an example method of how I am creating a context object with await in my app:

public async AppContext CreateAppContext()
{
    var context = new AppContext();

    var customersTask = _dataAccess.GetCustomers();
    var usersTask = _dataAccess.GetUsers();
    var reportsTask = _dataAccess.GetReports();

    context.Customers = await customersTask;
    context.Users = await usersTask;
    context.Reports = await reportsTask;

    return context;     
}

If I was to rewrite this with Task.Run I could do

public async AppContext CreateAppContext()
{
    var context = new new AppContext();
    await Task.WhenAll(new[]
    {
        Task.Run(async () => { context.Customers  = await _dataAccess.GetCustomers(); }),
        Task.Run(async () => { context.Users = await _dataAccess.GetUsers(); }),
        Task.Run(async () => { context.Reports = await _dataAccess.GetReports(); });
    })
            
    return context; 
}

The difference is not major when I create an object with 3 properties but I have objects where I need to initialize 20 properties in this manner which makes the await code a lot longer (nearly double) than Task.Run. Is there a way for me to initialize the object using await with code that is not a lot longer than what I can do with Task.Run?

CodePudding user response:

If you really want to keep your general pattern (which I'd avoid - it would be much better to do all the work and then assign all the results at the same time; look into the return value of Task.WhenAll), all you need is a simple helper method:

static async Task Assign<T>(Action<T> assignment, Func<Task<T>> getValue)
  => assignment(await getValue());

Then you can use it like this:

await Task.WhenAll
  (
    Assign(i => context.Customers = i, _dataAccess.GetCustomers),
    Assign(i => context.Users = i, _dataAccess.GetUsers),
    Assign(i => context.Reports = i, _dataAccess.GetReports)
  );

There's many other ways to make this even simpler, but this is the most equivalent to your Task.Run code without having to involve another thread indirection just to do an assignment. It also avoids the very common mistake when you happen to use the wrong Task.Run overload and get a race condition (as Task.Run returns immediately instead of waiting for the result).

Also, you misunderstood the "await vs. Task.Run" thing. There's actually not that much difference between await and Task.Run in your code - mainly, it forces a thread switch (and a few other subtle things). The argument is against using Task.Run to run synchronous code; that wastes a thread waiting for a thing to complete, your code doesn't.

Do keep in mind that WhenAll comes with its own complications, though. While it does mean you don't have to worry about some of the tasks ending up unobserved (and not waited on!), it also means you have to completely rethink your exception handling, since you're going to get an AggregateException rather than anything more specific. If you're relying on error handling based on identifying exceptions, you need to be very careful. Usually, you don't want AggregateException to leak out of methods - it's very difficult to handle in a global manner; the only method that knows the possibilities of what can happen is the method that calls the WhenAll. Hopefully.

It's definitely a good idea to run parallel operations like this in a way that cannot produce dangerous and confusing side-effects. In your code, you either get a consistent object returned, or you get nothing - that's exactly the right approach. Be wary of this approach leaking into other contexts - it can get really hard to debug issues where randomly half of the operations succeed and other half fails :)

CodePudding user response:

Is there a way for me to initialize the object using await with code that is not a lot longer than what I can do with Task.Run?

If you want to run all tasks in parallel - in short no, you cant shorten number of lines. Also note that those two snippets are not fully functionally equivalent - see Why should I prefer single await Task.WhenAll over multiple awaits.

You can simplify (and maybe even improve performance a bit) your Task.WhenAll approach by introducing a method which will await and assign. Something along these lines:

public async AppContext CreateAppContext()
{
    var context = new  AppContext();
    await Task.WhenAll(
        AwaitAndAssign(val => context.Customers = val, _dataAccess.GetCustomers()),
        AwaitAndAssign(val => context.Users = val, _dataAccess.Users()),
        AwaitAndAssign(val => context.Reports = val, _dataAccess.GetReports())
        );
    return context;

    async Task AwaitAndAssign<T>(Action<T> assign, Task<T> valueTask) =>
        assign(await valueTask);
}

CodePudding user response:

I think it's the shortest and pretty straight code that can be:

public async AppContext CreateAppContext()
{
    // Wrap with Task.Run to be 100% sure
    // that all task will be run in parallel
    var customersTask = Task.Run(_dataAccess.GetCustomers);
    var usersTask = Task.Run(_dataAccess.GetUsers);
    var reportsTask = Task.Run(_dataAccess.GetReports);

    var context = new AppContext
    {
        Customers = await customersTask,
        Users = await usersTask,
        Reports = await reportsTask
    };        

    return context;     
}
  • Related