I have really strange feeling here that I am missing something, but am struggling over the problem for a few hours now and can't get it. I have a kind of task scheduling class that mostly receives normal synchronous operations that it launches than asynchronously via Task.Run(). But when it gets an asynchronous Action passed it returns back without waiting for task to be awaited. I have simplified it to the following example:
private async void Button_OnClick(object sender, RoutedEventArgs e)
{
Logger("Click event received");
await OperationProcessor(async () =>
{
Logger(">>> Starting ACTION, waiting 5 seconds");
await Task.Delay(5000);
Logger(">>> ACTION finished");
});
Logger("Click event finished");
}
private static async Task OperationProcessor(Action operation)
{
Logger("Processing started, waiting a second");
await Task.Delay(1000);
Logger("Launching action");
await Task.Run(operation);
Logger("Returned from action, waiting another second");
await Task.Delay(1000);
Logger("Processing finished");
}
private static void Logger(string message)
{
Console.WriteLine($"{DateTime.Now:dd.MM.yy HH:mm:ss} [{Thread.CurrentThread.ManagedThreadId}] {message}");
}
This produces following output:
1: 16.07.19 10:58:06 [1] Click event received
2: 16.07.19 10:58:06 [1] Processing started, waiting a second
3: 16.07.19 10:58:07 [1] Launching action
4: 16.07.19 10:58:07 [7] >>> Starting ACTION, waiting 5 seconds
5: 16.07.19 10:58:07 [1] Returned from action, waiting another second
6: 16.07.19 10:58:08 [1] Processing finished
7: 16.07.19 10:58:08 [1] Click event finished
8: 16.07.19 10:58:12 [7] >>> ACTION finished
How to make 5 wait for action started at 4?
Why an async Action I passed to Task.Run() is not awaited?
Because it's not possible to await a void.
It's common to talk about "awaiting a method" or "awaiting a delegate", but that can be misleading. What actually happens is that the method is called first, and then whatever it returns is awaited. And it's not possible to await the result of an Action because that delegate returns void.
This is the primary reason why it's good to avoid async void. In this case, the async void was sneaky because it was a lambda expression.
The async equivalent of void Method() is async Task MethodAsync(), so the asynchronous delegate equivalent of Action is Func<Task>. This is why adding the Func<Task> overload worked. You can keep both the Action and Func<Task> overloads, and the compiler will intelligently prefer the Func<Task> overload for async lambdas.
I have a kind of task scheduling class
If your Func<Task> solution is good enough, then keep that as-is. But if you want to go a bit deeper, it is possible to detect (and wait for) async void methods. async void methods interact with the current SynchronizationContext. Since you have your own "scheduling class", you might want to consider providing a SynchronizationContext for code that is scheduled by that class. If you're interested, feel free to borrow from my AsyncContext class which is essentially just a single-threaded work queue with a SynchronizationContext.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With