Fire-and-forget with async vs "old async delegate"
C#AsynchronousC# 5.0C# Problem Overview
I am trying to replace my old fire-and-forget calls with a new syntax, hoping for more simplicity and it seems to be eluding me. Here's an example
class Program
{
static void DoIt(string entry)
{
Console.WriteLine("Message: " + entry);
}
static async void DoIt2(string entry)
{
await Task.Yield();
Console.WriteLine("Message2: " + entry);
}
static void Main(string[] args)
{
// old way
Action<string> async = DoIt;
async.BeginInvoke("Test", ar => { async.EndInvoke(ar); ar.AsyncWaitHandle.Close(); }, null);
Console.WriteLine("old-way main thread invoker finished");
// new way
DoIt2("Test2");
Console.WriteLine("new-way main thread invoker finished");
Console.ReadLine();
}
}
Both approaches do the same thing, however what I seem to have gained (no need to EndInvoke
and close handle, which is imho still a bit debatable) I am losing in the new way by having to await a Task.Yield()
, which actually poses a new problem of having to rewrite all existing async F&F methods just to add that one-liner. Are there some invisible gains in terms of performance/cleanup?
How would I go about applying async if I can't modify the background method? Seems to me that there is no direct way, I would have to create a wrapper async method that would await Task.Run()?
Edit: I now see I might be missing a real questions. The question is: Given a synchronous method A(), how can I call it asynchronously using async
/await
in a fire-and-forget manner without getting a solution that is more complicated than the "old way"
C# Solutions
Solution 1 - C#
Avoid async void
. It has tricky semantics around error handling; I know some people call it "fire and forget" but I usually use the phrase "fire and crash".
> The question is: Given a synchronous method A(), how can I call it asynchronously using async/await in a fire-and-forget manner without getting a solution that is more complicated than the "old way"
You don't need async
/ await
. Just call it like this:
Task.Run(A);
Solution 2 - C#
As noted in the other answers, and by this excellent blog post you want to avoid using async void
outside of UI event handlers. If you want a safe "fire and forget" async
method, consider using this pattern (credit to @ReedCopsey; this method is one he gave to me in a chat conversation):
-
Create an extension method for
Task
. It runs the passedTask
and catches/logs any exceptions:static async void FireAndForget(this Task task) { try { await task; } catch (Exception e) { // log errors } }
-
Always use
Task
styleasync
methods when creating them, neverasync void
. -
Invoke those methods this way:
MyTaskAsyncMethod().FireAndForget();
You don't need to await
it (nor will it generate the await
warning). It will also handle any errors correctly, and as this is the only place you ever put async void
, you don't have to remember to put try/catch
blocks everywhere.
This also gives you the option of not using the async
method as a "fire and forget" method if you actually want to await
it normally.
Solution 3 - C#
To me it seems that "awaiting" something and "fire and forget" are two orthogonal concepts. You either start a method asynchronously and don't care for the result, or you want to resume executing on the original context after the operation has finished (and possibly use a return value), which is exactly what await does. If you just want to execute a method on a ThreadPool thread (so that your UI doesn't get blocked), go for
Task.Factory.StartNew(() => DoIt2("Test2"))
and you'll be fine.
Solution 4 - C#
My sense is that these 'fire and forget' methods were largely artifacts of needing a clean way to interleave UI and background code so that you can still write your logic as a series of sequential instructions. Since async/await takes care of marshalling through the SynchronizationContext, this becomes less of an issue. The inline code in a longer sequence effectively becomes your 'fire and forget' blocks that would previously have been launched from a routine in a background thread. It's effectively an inversion of the pattern.
The main difference is that the blocks between awaits are more akin to Invoke than BeginInvoke. If you need behavior more like BeginInvoke, you can call the next asynchronous method (returning a Task), then don't actually await the returned Task until after the code that you wanted to 'BeginInvoke'.
public async void Method()
{
//Do UI stuff
await SomeTaskAsync();
//Do more UI stuff (as if called via Invoke from a thread)
var nextTask = NextTaskAsync();
//Do UI stuff while task is running (as if called via BeginInvoke from a thread)
await nextTask;
}
Solution 5 - C#
Here is a class I put together based on Ben Adams' tweet about constructing such a construct. HTH https://twitter.com/ben_a_adams/status/1045060828700037125
using Microsoft.Extensions.Logging;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
// ReSharper disable CheckNamespace
namespace System.Threading.Tasks
{
public static class TaskExtensions
{
[SuppressMessage("ReSharper", "VariableHidesOuterVariable", Justification = "Pass params explicitly to async local function or it will allocate to pass them")]
public static void Forget(this Task task, ILogger logger = null, [CallerMemberName] string callingMethodName = "")
{
if (task == null) throw new ArgumentNullException(nameof(task));
// Allocate the async/await state machine only when needed for performance reasons.
// More info about the state machine: https://blogs.msdn.microsoft.com/seteplia/2017/11/30/dissecting-the-async-methods-in-c/?WT.mc_id=DT-MVP-5003978
// Pass params explicitly to async local function or it will allocate to pass them
static async Task ForgetAwaited(Task task, ILogger logger = null, string callingMethodName = "")
{
try
{
await task;
}
catch (TaskCanceledException tce)
{
// log a message if we were given a logger to use
logger?.LogError(tce, $"Fire and forget task was canceled for calling method: {callingMethodName}");
}
catch (Exception e)
{
// log a message if we were given a logger to use
logger?.LogError(e, $"Fire and forget task failed for calling method: {callingMethodName}");
}
}
// note: this code is inspired by a tweet from Ben Adams: https://twitter.com/ben_a_adams/status/1045060828700037125
// Only care about tasks that may fault (not completed) or are faulted,
// so fast-path for SuccessfullyCompleted and Canceled tasks.
if (!task.IsCanceled && (!task.IsCompleted || task.IsFaulted))
{
// use "_" (Discard operation) to remove the warning IDE0058: Because this call is not awaited, execution of the
// current method continues before the call is completed - https://docs.microsoft.com/en-us/dotnet/csharp/discards#a-standalone-discard
_ = ForgetAwaited(task, logger, callingMethodName);
}
}
}
}