I have a method (called via an AJAX request) that runs at the end of a sequence. In this method I save to the database, send emails, look up a bunch of info in other APIs/databases, correlate things together, etc.. I just refactored the original method into a second revision and used Tasks to make it asynchronous, and shaved off up to two seconds in wall time. I used Tasks mainly because it seemed easier (I'm not that experienced in async/await yet) and some tasks depend on other tasks (like task C D and E all depend on results from B, which itself depends on A). Basicalll all of the tasks are started at the same time (processing just zips down the to the Wait() call on the email task, which in one way or another requires all the others to complete. I generally do something like this except with something like eight tasks:
public thing() {
var FooTask<x> = Task<x>.Factory.StartNew(() => {
// ...
return x;
});
var BarTask<y> = Task<y>.Factory.StartNew(() => {
// ...
var y = FooTask.Result;
// ...
return y + n;
}
var BazTask<z> = Task<z>.Factory.StartNew(() => {
var y = FooTask.Result;
return y - n;
}
var BagTask<z> = Task<z>.Factory.StartNew(() => {
var g = BarTask.Result;
var h = BazTask.Result;
return 1;
}
// Lots of try/catch aggregate shenanigans.
BagTask.Wait();
return "yay";
}
Oh I also need to roll back previous things if something breaks, like remove a database row if the email fails to send, so there are a few levels of try/catches in there. Anyway, all of this works (amazingly, it all worked on the first try). My question is whether this sort of method would benefit from being rewritten to use async/await rather than Tasks. If so, how would the multiple-dependency scenario play out without re-running an async method that was already ran or awaited by another method? I guess some shared variable?
Update:
The // ... lines were supposed to indicate that the task was doing something, like looking up DB records. Sorry if that wasn't clear. About half of the tasks (there are 8 total) can take up to maybe five seconds to run, if the contexts aren't warmed up, and the other half of the tasks just collect/assemble/process/use that data.
You'll find that
async/await(paired withTask.Runinstead ofStartNew) will make your code much cleaner:You also have the option of using
Task.WhenAllif you want toawaitmultiple tasks completing. Error handling in particular is cleaner withawaitsince it doesn't wrap exception inAggregateException.However
This is a bit of a problem. Both
StartNewandTask.Runshould be avoided on ASP.NET.Yes, parallel processing on ASP.NET (which is what the code is currently doing) will make individual requests execute faster, but at the expense of scalability. The server will be unable to handle as many requests if it is doing parallel processing on each one.
These are all I/O-bound operations, not CPU-bound. So the ideal solution is to create truly-async I/O methods and then just call them using
await(andTask.WhenAllif necessary). By "truly-async", I mean calling the underlying asynchronous APIs (e.g.,HttpClient.GetStringAsyncinstead ofWebClient.DownloadString; or Entity Framework'sToListAsyncinstead ofToList, etc). UsingStartNeworTask.Runis what I call "fake asynchrony".Once you have asynchronous APIs, your top-level method really becomes simple: