Home > Software design >  Can I safely use async void to asynchronously process multiple items from a queue, in a single threa
Can I safely use async void to asynchronously process multiple items from a queue, in a single threa

Time:09-18

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.

  1. 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 the async void is not good for you.
  2. Unhandled exceptions thrown in async void operations are rethrown on the SynchronizationContext that was captured when the async void method started. Since you have a Console application there is no SynchronizationContext, so the error will be thrown on the ThreadPool. This means that your application will raise the AppDomain.UnhandledException event, and then will crash. If you have a requirement to not have your application crashing at random moments, then the async 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.

  • Related