Fire-And-Forget with Async VS "Old Async Delegate"

Fire-and-forget with async vs old async delegate

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

Async/await with/without awaiting (fire and forget)


I'm asking this, because we're moving our app to service fabric where we no longer can use HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken => await LongMethodAsync()); and the advice is to simply replace it with Task.Run.

That's bad advice. You should use a separate background process separated from your web frontend by a queue.

What's the in-depth logic behind the calls?

  1. Starts the asynchronous method on the current thread. Ignores all results (including exceptions).
  2. Starts the asynchronous method on the current thread. Asynchronously waits for it to complete. This is the standard way of calling asynchronous code.
  3. Starts the asynchronous method on a thread pool thread. Ignores all results (including exceptions).
  4. Starts the asynchronous method on a thread pool thread. Asynchronously waits for it to complete.
  5. Exactly the same as #3.
  6. Exactly the same as #4.

Fire and Forget Async Await Call

It's just a warning, so that you know it's not been awaited on. You can use discard to make it clear you don't care whether or not it ends up running to completion:

_ = SendSMS();

So, question is, If the UserRegistration() method returns before completion of SendSMS() OR SendEmail(), Does it fire the exception?

Nope.

And Is it good practice to make Fire and Forget call in Async Await Methods?

Not in my opinion.

Proper way to start and async fire-and-forget call?

It depends on what you mean by proper :)

For instance: are you interested in the exceptions being thrown in your "fire and forget" calls? If not, than this is sort of fine. Though what you might need to think about is in what environment the task lives.

For instance, if this is a asp.net application and you do this inside the lifetime of a thread instantiated due to a call to a .aspx or .svc. The Task becomes a background thread of that (foreground)thread. The foreground thread might get cleaned up by the application pool before your "fire and forget" task is completed.

So also think about in which thread your tasks live.

I think this article gives you some useful information on that:
https://www.hanselman.com/blog/HowToRunBackgroundTasksInASPNET.aspx

Also note that if you do not return a value in your Tasks, a task will not return exception info. Source for that is the ref book for microsoft exam 70-483
There is probably a free version of that online somewhere ;P https://www.amazon.com/Exam-Ref-70-483-Programming-C/dp/0735676828

Maybe useful to know is that if your have an async method being called by a non-async and you wish to know its result. You can use .GetAwaiter().GetResult().

Also I think it is important to note the difference between async and multi-threading.

Async is only useful if there are operations that use other parts of a computer that is not the CPU. So things like networking or I/O operations. Using async then tells the system to go ahead and use CPU power somewhere else instead of "blocking" that thread in the CPU for just waiting for a response.

multi-threading is the allocation of operations on different threads in a CPU (for instance, creating a task which creates a background thread of the foreground thread... foreground threads being the threads that make up your application, they are primary, background threads exist linked to foreground threads. If you close the linked foreground thread, the background thread closes as well)
This allows the CPU to work on different tasks at the same time.

Combining these two makes sure the CPU does not get blocked up on just 4 threads if it is a 4 thread CPU. But can open more while it waits for async tasks that are waiting for I/O operations.

I hope this gives your the information needed to do, what ever it is you are doing :)

The correct way to Fire-and-Forget an asynchronous delegate

The "old-school" way in .NET 3.5 is to use the ThreadPool:

ThreadPool.QueueUserWorkItem(s => DoSomething());

If you prefer to use asynchronous delegates, then you should know that the call to EndInvoke is necessary, even if you don't have any additional code you wish to execute on callback.

Does skipping await automatically make async code synchronous?


Does the fact that I omitted await and did not declare SaveImage as async make it actually [synchronous]?

No. async void methods are dangerous for several reasons. They're asynchronous, but there's no way for your code to detect when the asynchronous operation completes. Also, since there's no way for your code to detect errors from the asynchronous operation, any exceptions are raised directly on the current context, which usually results in an application shutdown. This behavior seems very odd an undesirable, until you realize that async void methods were specifically designed to be used as event handlers, and should be avoided in other scenarios.

What I would expect to happen is that the loop would instantly complete because even though WritePngAsync takes a long time SaveImage is not awaited and would return instantly.

Yes, the loop completes quickly, and all of the PNG.WriteAsync operations may be incomplete when the loop completes.

I think this is causing issues with the file contents being overwritten when called like this

That sounds reasonable. Your code cannot know when the WritePngAsync methods are complete.

what's the best way to invoke Async Code from Non Async in C# (Fire and Forget)

The best way to achieve fire and forget methods are with the async void modifiers:

public async void PersistTask()
{
// no need to assign the task if you are just going to await for it.
await Task.Run(() =>
{
Thread.Sleep(3000);
Console.WriteLine("First");
return Task.FromResult(0);
});
}

And call it using:

//Blocking
public void OrderForm_Load()
{
var t1 = new ServiceTask();

Task.Run(() => t1.PersistTask()); //Approach 1
//[OR]
t1.PersistTask(); //Approach 2
Console.WriteLine("Second");
}

There's another difference, though: Task.Run is best suited for CPU-bound methods. See many more details in this answer.

Also, note that you should avoid using synchronous waits in asynchronous work. So, change this:

Thread.Sleep(3000);

For this:

Task.Delay(3000);

How to make a fire & forget async FIFO queue in c#?

A common pattern here is to have a callback method that executes upon a document state change. With a background task running, it will chew threw documents as fast as it can. Call Dispose to shutdown the processor.

If you need to process the callback on a gui thread, you'll need to synchornize the callback to your main thread some how. Windows forms has methods to do this if that's what you are using.

This example program implements all the necessary classes and interfaces, and you can fine tune and tweak things as you need.

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp2
{
class Program
{
private static Task Callback(IExtractedDocument doc, DocumentProcessor.DocState docState)
{
Console.WriteLine("Processing doc {0}, state: {1}", doc, docState);
return Task.CompletedTask;
}

public static void Main()
{
using DocumentProcessor docProcessor = new DocumentProcessor(Callback);
Console.WriteLine("Processor started, press any key to end processing");
for (int i = 0; i < 100; i++)
{
if (Console.KeyAvailable)
{
break;
}
else if (i == 5)
{
// make an error
docProcessor.Add(null);
}
else
{
docProcessor.Add(new Document { Text = "Test text " + Guid.NewGuid().ToString() });
}
Thread.Sleep(500);
}
Console.WriteLine("Doc processor shut down, press ENTER to quit");
Console.ReadLine();
}

public interface IDocument
{
public string Text { get; }
}

public class Document : IDocument
{
public string Text { get; set; }
}

public interface IExtractedDocument : IDocument
{
public IDocument OriginalDocument { get; }
public Exception Error { get; }
}

public class ExtractedDocument : IExtractedDocument
{
public override string ToString()
{
return $"Orig text: {OriginalDocument?.Text}, Extracted Text: {Text}, Error: {Error}";
}

public IDocument OriginalDocument { get; set; }

public string Text { get; set; }

public Exception Error { get; set; }
}

public class DocumentProcessor : IDisposable
{
public enum DocState { Processing, Completed, Error }

private readonly BlockingCollection<IDocument> queue = new BlockingCollection<IDocument>();
private readonly Func<IExtractedDocument, DocState, Task> callback;
private CancellationTokenSource cancelToken = new CancellationTokenSource();

public DocumentProcessor(Func<IExtractedDocument, DocState, Task> callback)
{
this.callback = callback;
Task.Run(() => StartQueueProcessor()).GetAwaiter();
}

public void Dispose()
{
if (!cancelToken.IsCancellationRequested)
{
cancelToken.Cancel();
}
}

public void Add(IDocument doc)
{
if (cancelToken.IsCancellationRequested)
{
throw new InvalidOperationException("Processor is disposed");
}
queue.Add(doc);
}

private void ProcessDocument(IDocument doc)
{
try
{
// do processing
DoCallback(new ExtractedDocument { OriginalDocument = doc }, DocState.Processing);
if (doc is null)
{
throw new ArgumentNullException("Document to process was null");
}
IExtractedDocument successExtractedDocument = DoSomeDocumentProcessing(doc);
DoCallback(successExtractedDocument, DocState.Completed);
}
catch (Exception ex)
{
DoCallback(new ExtractedDocument { OriginalDocument = doc, Error = ex }, DocState.Error);
}
}

private IExtractedDocument DoSomeDocumentProcessing(IDocument originalDocument)
{
return new ExtractedDocument { OriginalDocument = originalDocument, Text = "Extracted: " + originalDocument.Text };
}

private void DoCallback(IExtractedDocument result, DocState docState)
{
if (callback != null)
{
// send callbacks in background
callback(result, docState).GetAwaiter();
}
}

private void StartQueueProcessor()
{
try
{
while (!cancelToken.Token.IsCancellationRequested)
{
if (queue.TryTake(out IDocument doc, 1000, cancelToken.Token))
{
// can chance to Task.Run(() => ProcessDocument(doc)).GetAwaiter() for parallel execution
ProcessDocument(doc);
}
}
}
catch (OperationCanceledException)
{
// ignore, don't need to throw or worry about this
}
while (queue.TryTake(out IDocument doc))
{
DoCallback(new ExtractedDocument { Error = new ObjectDisposedException("Processor was disposed") }, DocState.Error);
}
}
}
}
}


Related Topics



Leave a reply



Submit