Skip to content

Instantly share code, notes, and snippets.

@AlexVallat
Last active January 18, 2021 17:57
Show Gist options
  • Save AlexVallat/689544cc5671fd1f343d21d0c197bf43 to your computer and use it in GitHub Desktop.
Save AlexVallat/689544cc5671fd1f343d21d0c197bf43 to your computer and use it in GitHub Desktop.
Async patterns

Useful patterns I have found and used when writing Async code

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.

Initialisation

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("...");
}

Obtain several independent parameters

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);

Do independent operations simultanously

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)
);

Operations on which nothing further depends

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);
  }
}

Process a list of values simultaneously

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)
};

Get a combined list of values from multiple sources

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);

Awaiting the same thing twice is harmless

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;
@ModernRonin
Copy link

ModernRonin commented Jan 18, 2021

the following might be useful:

public static class TaskEx
{
    public static async Task<(U, V)> GetSimultaneouslyAsync<U, V>(Task<U> uTask, Task<V> vTask)
    {
        await Task.WhenAll(uTask, vTask);
        return (await uTask, await vTask);
    }
    public static async Task<(U, V, W)> GetSimultaneouslyAsync<U, V, W>(Task<U> uTask, Task<V> vTask, Task<W> wTask)
    {
        await Task.WhenAll(uTask, vTask, wTask);
        return (await uTask, await vTask, await wTask);
    }
    public static async Task<(U, V, W, X)> GetSimultaneouslyAsync<U, V, W, X>(Task<U> uTask, Task<V> vTask, Task<W> wTask, Task<X> xTask)
    {
        await Task.WhenAll(uTask, vTask, wTask, xTask);
        return (await uTask, await vTask, await wTask, await xTask);
    }
}

allow you to rewrite the example from Obtain several independent parameters as

var (item, context, config) = await TaskEx.GetSimultaneouslyAsync(GetItemAsync(), GetContextAsync(), GetConfigAsync());
var result = Process(item, context, config);

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 import TaskEx statically to call the method without qualification.

@AlexVallat
Copy link
Author

I like it! For the naming, I think it should just be TaskEx.WhenAll() - it really just acts as another overload of Task.WhenAll(), Task.WhenAll<T>(), and ought to be included by Microsoft in Task. I really don't like the repetition of having to copy it from <T1> all the way to <T1, T2, T3, T4, T5, T6, T7, T8>, but this seems to be the only way to do it (compare with ValueTuple.Create). A limitation of the generics system, I guess. Still, if it's swept under the rug in a library never to be seen again it probably would stop bothering me!

I don't think I've bothered with tests proving simultaneity anywhere... one place tests for a task not completing synchronously, but otherwise all my mocks just return synchronous values to Async methods. Possibly not ideal, but if you've got any good patterns for testing simultaneity then do share!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment