My team and I support several background worker console applications that synchronously process messages from a queue.
Currently, we spin up new instances of the app using docker to handle multiple messages when the number of messages in the queue exceeds a certain threshold.
We were discussing alternative ways we could process these messages in parallel and one teammate proposed using a top level, async void
method in a while
loop to process the messages.
It looks like it will work, since it would be similar to a UI-based application using async void as an event handler.
However, I've personally never written a background worker to handle multiple messages in parallel this way, and wanted to know if anyone had experience doing so, and if so, if there are any "gotchas" that we aren't thinking of.
Here is a simplified version of the proposed solution:
static async void Main(string[] args)
{
while (true)
{
TopLevelHandler(await ReceiveMessage());
}
}
static async void TopLevelHandler(object message)
{
await DoAsyncWork(message);
}
static async Task<object> ReceiveMessage()
{
//fetch message from queue
return new object();
}
static async Task DoAsyncWork(object message)
{
//processing here
}
CodePudding user response:
With newest NET we have by default async Main
method.
Moreover, if you don't have that feature, you could mark Main
as async.
The main risk here that you could miss exceptions and have to be very carefull with created tasks. If you'll be creating tasks in while (true)
loop you most probably could end up with thread pool starvation at some point (when someone mistakenly would use some blocking call).
Here is below sample code that shows what shuold be thought about, but i am most certain that there will be more intricacies:
using System.Collections.Concurrent;
using System.Security.Cryptography.X509Certificates;
namespace ConsoleApp2;
public static class Program
{
/// <summary>
/// Property to track all running tasks, should be dealt with carefully.
/// </summary>
private static ConcurrentBag<Task> _runningTasks = new();
static Program()
{
// Subscribe to an event.
TaskScheduler.UnobservedTaskException = TaskScheduler_UnobservedTaskException;
}
static async Task Main(string[] args)
{
var messageNo = 1;
while (true)
{
// Just schedule the work
// Here you should most probably limit number of
// tasks created.
var task = ReceiveMessage(messageNo)
.ContinueWith(t => TopLevelHandler(t.Result));
_runningTasks.Add(task);
messageNo ;
}
}
static async Task TopLevelHandler(object message)
{
await DoAsyncWork(message);
}
static async Task<object> ReceiveMessage(int messageNumber)
{
//fetch message from queue
await Task.Delay(5000);
return await Task.FromResult(messageNumber);
}
static async Task DoAsyncWork(object message)
{
//processing here
Console.WriteLine($"start processing message {message}");
await Task.Delay(5000);
// Only when you want to test catching exception
// throw new Exception("some malicious code");
Console.WriteLine($"end processing message {message}");
}
/// <summary>
/// Here you handle any unosberved exception thrown in a task.
/// Preferably you should handle somehow all running work in other tasks.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
static async void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
// Potentially this method could be enter by multiple tasks having exception.
Console.WriteLine($"Exception caught: {e.Exception.InnerException.Message}");
await Task.WhenAll(_runningTasks.Where(x => !x.IsCompleted));
}
}
CodePudding user response:
Can I safely use
async void
...
It depends on what you mean be "safely". The async void
methods have specific behaviors, so if you like these behaviors then the async void
is OK.
- The
async void
operations cannot be awaited, so you don't know when they have completed. If you have a requirement to wait for the completion of all pending asynchronous operations before terminating the program, then theasync void
is not good for you. - Unhandled exceptions thrown in
async void
operations are rethrown on theSynchronizationContext
that was captured when theasync void
method started. Since you have a Console application there is noSynchronizationContext
, so the error will be thrown on theThreadPool
. This means that your application will raise theAppDomain.UnhandledException
event, and then will crash. If you have a requirement to not have your application crashing at random moments, then theasync void
is not good for you.
The async void
methods were originally invented to make async
event handlers possible. In modern practice this is still their main usage.
One niche scenario that makes async void
an interesting choice is the case that you have a workflow that when it completes it must kick off another workflow. If the kicking is critical for the continuous flow of your application, and a failure to do the kick will leave your app in a hanging state, then it makes sense to escalate this failure to a process-crashing event. Arguably a program that crashes is comparatively better than a program that hangs. You can see two examples of using async void
for this scenario here and here. In the second example the async void
is the lambda passed to the ThreadPool.QueueUserWorkItem
method, ensuring that the async void
will not capture any unknown ambient SynchronizationContext
.
CodePudding user response:
Try using IHostedService with Timer to invoke background work. Please ever this link https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-6.0&tabs=visual-studio for more details.