Replies: 9 comments
-
in async await you should use this works as expected static void Main(string[] args)
{
Console.WriteLine("Hello World!");
BadTask().SafeFireAndForget();
Console.WriteLine("Hello World!");
Console.ReadLine();
}
static async Task BadTask()
{
await Task.Delay(5000);
Console.WriteLine("Hello World!");
} |
Beta Was this translation helpful? Give feedback.
-
I agree that this is true, however, I'm not sure how to fix it. It may just be a limitation of .NET. Expanding on your example, even if we use
|
Beta Was this translation helpful? Give feedback.
-
If anyone has ideas on how to best resolve this Issue, please do share them! If it turns out that we cannot resolve this issue, I'll likely convert this to a Discussion so that we can continue the conversation. |
Beta Was this translation helpful? Give feedback.
-
@brminnick adding Task.Yield() at the start will push everything below to thread pool static async void HandleSafeFireAndForget<TException>(ValueTask valueTask, bool continueOnCapturedContext, Action<TException>? onException) where TException : Exception
{
try
{
await Task.Yield();
await valueTask.ConfigureAwait(continueOnCapturedContext);
}
catch (TException ex) when (_onException != null || onException != null)
{
HandleException(ex, onException);
if (_shouldAlwaysRethrowException)
throw;
}
} |
Beta Was this translation helpful? Give feedback.
-
I had the same thought, and tried adding This means that For example, in the following code, Task.Run(() =>
{
Thread.Sleep(5000);
await Task.Delay(1000); // This is when HandleSafeFireAndForget executes
}.SafeFireAndForget(); Here's another example, using a Unit Test:
[Test]
public async Task SafeFireAndForget_NonAsyncMethodThreadTest()
{
//Arrange
Thread initialThread, workingThread, finalThread;
var threadTCS = new TaskCompletionSource<Thread>();
//Act
initialThread = Thread.CurrentThread;
NonAsyncMethod().SafeFireAndForget();
finalThread = Thread.CurrentThread;
workingThread = await threadTCS.Task;
//Assert
Assert.IsNotNull(initialThread);
Assert.IsNotNull(workingThread);
Assert.IsNotNull(finalThread);
Assert.AreEqual(initialThread.ManagedThreadId, finalThread.ManagedThreadId);
Assert.AreNotEqual(initialThread.ManagedThreadId, workingThread.ManagedThreadId);
Assert.AreNotEqual(finalThread.ManagedThreadId, workingThread.ManagedThreadId);
async Task NonAsyncMethod()
{
threadTCS.SetResult(Thread.CurrentThread);
await Task.Delay(1000);
}
} |
Beta Was this translation helpful? Give feedback.
-
In current design it's not possible without API break, because with any call like this one: The only way to fix eagerness of C# Tasks is to wrap them in thunk like
This will introduce breaking change in API (
That way it will definitely execute Task.Yield or Task.Run first, jump to thread pool and then proceed safely. It's not my call to make, but maybe just add "even more safer" API? :) |
Beta Was this translation helpful? Give feedback.
-
I thought that the purpose of this API is to provide an exception-safe and explicit way to forget about async (not background/parallel) task execution and nothing more. Fire-and-forget is an alternative to Modifying existing logic with thread switching under the hood will break the compatibility for applications that fire-and-forget tasks that interact with the UI thread. Adding Task.Run(BadTask).SafeFireAndForget(); |
Beta Was this translation helpful? Give feedback.
-
That's why I've mentioned what's written in README.md:
It's just not true :) |
Beta Was this translation helpful? Give feedback.
-
This just got me big time today. Read a blog post a few years ago that recommended SafeFireAndForget and I've often used it. Did not realise that the Task only returns once the first await is called. So obvious, but yea I missed that thinking it was some secret sauce to firing a task off in a background thread. So maybe there could be a little sentence for the less experienced so they don't run into this gotcha, even though a proper understanding of async / await would not necessitate it. |
Beta Was this translation helpful? Give feedback.
-
https://github.com/brminnick/AsyncAwaitBestPractices/blame/main/README.md#L140
This line says that:
That couldn't be true, because implementation doesn't switch threads at all
AsyncAwaitBestPractices/Src/AsyncAwaitBestPractices/SafeFireAndForgetExtensions.shared.cs
Lines 77 to 90 in d7ae190
Everything prior first await will be executed on caller thread because that's how C# asyncs work (e.g. F# asyncs are different)
Code below will block caller thread for 5 sec before printing, which demonstrate that SafeFireAndForget isn't actually safe.
Beta Was this translation helpful? Give feedback.
All reactions