Fire and forget, using Task.Run or just calling an async method without await
Asked Answered
O

4

15

In general especially when it comes to libraries or console apps, in order to fire and forget an async method, is it better to just call the async method without awaiting it or use Task.Run?

Basically:

public static void Main()
{
    // Doing
    _ = DoSomethingAsync();

    // vs.
    _ = Task.Run(DoSomethingAsync);
}
 
private static async Task DoSomethingAsync()
{
    //...
}

Note: This question is outside of the realm of ASP.NET and web apps.

Olmstead answered 20/3, 2020 at 16:55 Comment(0)
T
13

In general especially when it comes to libraries or console apps, in order to fire and forget an async method, is it better to just call the async method without awaiting it or use Task.Run?

In general, it's best not to use fire-and-forget at all.

"Fire and forget" means:

  1. You don't care if the code has an exception. Any exceptions will cause it to fail silently; no logging, no notification, etc.
  2. You don't need to know when the code completes. I.e., the consuming application doesn't ever need to wait for the code to complete. Even during shutdown.
  3. You don't need the code to complete. As a corollary of (2), fire-and-forget code may not run to completion; and as a corollary of (1), you would have no notification that it failed to complete.

In short, "fire and forget" is only appropriate for an extremely small number of tasks. E.g., updating a cache. I'd say probably 85% or more of "fire and forget" code is wrong - it's using fire and forget for code that should not be fire and forget.

So I'd say the best solution is to not use fire and forget at all. At the very least, you should expose a Task somewhere that represents a "followup action". Consider adding the Task to your return type or exposing it as a property.

Adopting fire and forget - especially in a library - means you're forcing all consumers to never know when it's safe to shut down and exit. But if you really want to do fire and forget, there are a few options.

A. One option is calling an async void function without a context. The consuming application still has no way to determine if/when the code completes, but at least that way exceptions are not ignored.

B. Another option is to start the task without a context. This option has both disadvantages of fire and forget code: exceptions are ignored and the calling code cannot know when it completes.

Both of these recommendations start the task without a context. There are helpers for doing this, or you can wrap the call in Task.Run (slightly less efficient, but it works fine).

I wouldn't recommend starting the task directly. While this would work fine in a Console app, it's not appropriate for libraries which may be called in situations where a context is provided.

Tardy answered 20/3, 2020 at 20:18 Comment(0)
M
8

Leaving aside the "should I use fire-and-forget or not" debate and "which tasks are suitable for fire-and-forget" debate:

Personally, I use this great helper by Gerald Barre, recommended at Nick Chapsas video blog - original here: https://www.meziantou.net/fire-and-forget-a-task-in-dotnet.htm

Fun fact: a variation of this code is used by ASP.NET Core SignalR.

public static class TaskExtensions
{
    /// <summary>
    /// Observes the task to avoid the UnobservedTaskException event to be raised.
    /// </summary>
    public static void Forget(this Task task)
    {
        // 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.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://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/discards?WT.mc_id=DT-MVP-5003978#a-standalone-discard
            _ = ForgetAwaited(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
        async static Task ForgetAwaited(Task task)
        {
            try
            {
                // No need to resume on the original SynchronizationContext, so use ConfigureAwait(false)
                await task.ConfigureAwait(false);
            }
            catch
            {
                // Nothing to do here
            }
        }
    }
}

Usage

LogRunningTask().Forget()
Mufinella answered 26/4, 2023 at 10:58 Comment(3)
This answer would be more relevant here: How do I avoid an "Unobserved Task" exception?Perique
When I use this, it seems to work fine, until the LogRunningTask calls an await/async method inside LogRunningTask. Then it just dies and nothing happens.Onetoone
@Onetoone Probably the async method you called throws an exception and it it is silently swallowed. This is the definition of "fire and FORGET". Add exception handling and error logging to that method, try debuggin.Mufinella
O
2

In general, it depends but Task.Run is the safer choice and the reasoning behind it depends on two things:

  1. If we can guarantee that the async method is always going to be asynchronous. Guaranteeing a method to be always asynchronous can become especially tricky for library projects or if the async method is in a code that is not managed by the caller. For example once I had implemented method similar to this example in a library:
 public async Task HandleMessages(Func<Task> onMessageNotification)
 {
   while(true)
   {
      var msg = MessagingClient.ReceiveMessage();
      ...
     if(msg.MessageType == "MakeAPizza")
     { 
       _ = onMessageNotification();
       _ = MakePizzaAsync();
     }
   }
 }

 private async Task MakePizzaAsync() {...}

I wanted to fire and forget onMessageNotification and MakePizzaAsync since I didn't want it to hold up the message handling. What I didn't catch was when the caller implementedonMessageNotification like below:

Func<Task> onMsgNotification = () => {
  DoSlowOperation();
  return Task.CompletedTask;
}

Which would practically made the onMessageNotification synchronous operation, and had to wait on the DoSlowOperation() to finish. So in order to avoid to fix issue I just had to change it to:

  _ = Task.Run(onMessageNotification);
  _ = MakePizzaAsync();
  1. We won't have long running operations in the async method before the first await operation. The other way that the caller can cause issues is if onMsgnotification Func were implemented in the following way:
Func<Task> onMsgNotification = async () => {
  DoSlowOperation();
  await AsyncOperation();
}

Which would have still slowed down the HandleMessages method because DoSlowOperation is still getting called synchronously.

Olmstead answered 20/3, 2020 at 16:55 Comment(0)
P
0

If you use the Task.Run, the DoSomethingAsync will be invoked in the ThreadPool instead of the current thread. That's the whole difference. In case the DoSomethingAsync contains synchronous blocking I/O, or heavy CPU-bound code before the first await, this code will run on ThreadPool, leaving the current thread free to continue doing other things. In other words the Task.Run introduces parallelism on top of the asynchronous concurrency. Whether this is good or bad depends on the scenario.

Considering that fire-and-forget is almost always a bad technique by itself, the choice between Task.Run or not is most likely a neutral choice. I mean, you've already decided to lose track of your tasks, and let them run rampant and unobserved, what worse could happen?

Perique answered 22/6, 2023 at 11:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.