Note: .ConfigureAwait(false)
is omitted from all examples here, but due to poor design decisions in C# should be added to every await when writing library code.
Stephen Cleary, whose blog is an excellent resource for Async, describes The Asynchronous Initialization Pattern which is useful when your class has to do something async on construction. Contruction itself can't be async, but after construction (either by explicit new
or being passed an potentially newly constructed instance through DI), you await
the initialisation before using it. Note that the initialisation code starts executing on construction.
interface IAsyncInitialization
{
Task Initialization { get; }
}
interface IStorage : IAsyncInitialization
{
async Task WriteData<T>(T data);
...
}
class DatabaseStorage : IStorage
{
public DatabaseStorage()
{
async Task Initialize()
{
OpenConnection();
_table = await GetOrCreateTable();
}
Initialization = Initialize();
}
public Task Initialization { get; }
...
}
class ConfigurationManager : IAsyncInitialization
{
public ConfigurationManager(IStorage storage)
{
_storage = storage;
async Task Initialize()
{
await storage.Initialization;
}
Initialization = Initialize();
}
public Task Initialization { get; }
...
}
...
async Task StartProcessing()
{
await _configurationManager.Initialization;
var config = await _configurationManager.GetData("...");
}
Don't inline independent awaits:
var result = Process(await GetItemAsync(), await GetContextAsync(), await GetConfigAsync());
instead execute them simultaneously
var getItem = GetItemAsync();
var getContext = GetContextAsync();
var getConfig = GetConfigAsync();
var result = Process(await getItem, await getContext, await getConfig);
Task.WhenAll can be used to execute multiple async calls that don't depend on each other, but must all be completed before further processing can occur
await Task.WhenAll(
SetCurrentUserAsync(user),
UpdateStatusAsync(Status.Processing)
);
When there are operations that is fire-and-forget, but should still complete eventually, add them to a list of background tasks and await them all in a finally. Local functions can be used to group operations that depend on each other, but nothing further.
async Task DoWork()
{
var backgroundTasks = new List<Task>();
try
{
backgroundTasks.Add(AddAuditLogAsync("Started"));
...
async Task UpdateConfiguration()
{
var prefs = await _service.GetUserPrefsAsync();
prefs.Counter++;
await _configManager.SetAsync(prefs);
}
backgroundTasks.Add(UpdateConfiguration());
...
}
finally
{
await Task.WhenAll(backgroundTasks);
}
}
Task.WhenAll
and Select
can be used with a local function to perform an operation on all members of a list in parallel, and get the result once they are all done.
var users = await Task.WhenAll(ids.Select(GetUser));
async Task<User> GetUser(string id) => new User
{
Id = id,
Name = await _service.GetUserNameAsync(id)
};
Task.WhenAll
and SelectMany
can be used to fetch values from multiple sources simultaneously and combine them into a single list of results.
var allFiles = (await Task.WhenAll(
_service.GetPrivateFilesAsync(user),
_service.GetPublicFilesAsync(user)
)).SelectMany(x => x);
In general, start an operation as soon as possible once everything it depends on is avaiable, and await it as late as possible when something that depends on it needs it. If the thing that may need it is conditional, that's no problem at all. Awaiting something twice is harmless (performance is not impacted in any measurable way unless this is on an extremely hot path and tight loop, and even then, profile first to confirm!)
var userTask = _service.GetUserAsync(id);
...
if (sendNotification)
{
SendMessage(await userTask);
}
...
var userName = (await userTask).Name;
the following might be useful:
allow you to rewrite the example from Obtain several independent parameters as
Makes the intention a bit clearer and thus more unlikely that someone else will carelessly refactor away the simultaneity. Although of course that could never happen as you got tests proving simultaneity ;P
The method name
GetSimultaneouslyAsync
is debatable, couldn't come up with anything more satisfactory quickly. Some users probably will prefer to importTaskEx
statically to call the method without qualification.