This is the fifth part of the Async Wandering series. For your convenience you can find other parts in the table of contents in Part 1 – Why creating Form from WinForms in unit tests breaks async?
If you are interested in the topic see the talk page
Last time we saw how to wait for async void
methods. Today we are going to catch exceptions. Normally exceptions thrown by those methods crash application because we cannot wait for them. Exceptions from async Task
on the other hand don’t do that generally (they can when the GC cleans up and unobserved exception is being thrown) but silently wait for us to be handled. We would like to have exactly the same behavior, so let’s see, how we can do that.
Last time we implemented custom synchronization context. Today we need something more — task scheduler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public class MyTaskScheduler : TaskScheduler { private readonly MyContext context; public BlockingCollection< Task> tasks = new BlockingCollection< Task>(); public MyTaskScheduler(MyContext context) { this.context = context; } protected override IEnumerable<Task> GetScheduledTasks() { return tasks; } protected override void QueueTask(Task task) { Queue(task); } protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { return TryExecuteTask(task); } public new bool TryExecuteTask(Task task) { return base.TryExecuteTask(task); } public void Queue(Task task) { tasks.Add(task); } } |
We have collection of tasks to execute. Whenever we queue new task, we just store it in the array instead of trying to execute it right away. Effectively those tasks are not run at all until we say so.
Now let’s see the refined context:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
public class MyContext : SynchronizationContext { public MyTaskScheduler scheduler; public TaskFactory factory; private int operations; public MyContext() { scheduler = new MyTaskScheduler(this); factory = new TaskFactory(CancellationToken.None, TaskCreationOptions.HideScheduler, TaskContinuationOptions.HideScheduler, scheduler); } public override void Post(SendOrPostCallback d, object state) { var task = factory.StartNew(() => d(state)); scheduler.Queue(task); } public override void Send(SendOrPostCallback d, object state) { d(state); } public override void OperationCompleted() { operations--; if (operations == 0) { scheduler.tasks.CompleteAdding(); } } public override void OperationStarted() { operations++; } public static Task Run(Action action) { return Task.Run(() => { var oldContext = SynchronizationContext.Current; var newContext = new MyContext(); try { SynchronizationContext.SetSynchronizationContext(newContext); var spanningTask = newContext.factory.StartNew(action); foreach (var task in newContext.scheduler.tasks.GetConsumingEnumerable()) { newContext.scheduler.TryExecuteTask(task); task.Wait(); } spanningTask.Wait(); } finally { SynchronizationContext.SetSynchronizationContext(oldContext); } }); } } |
We create new TaskFactory
with our scheduler so whatever we run on this factory will go through our logic. Next, we count operations so we know when we are done.
Finally, we have a helper method for running the tasks. We replace synchronization context with ours, submit a task with lambda provided by the caller, and then execute all tasks from the scheduler one by one. We wait for all the tasks so whatever exception is thrown we just “catch” it with ordinary Task
mechanisms.
Finally, let’s try it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
static void Main(string[] args) { Console.WriteLine("Preparing job to run"); var task = MyContext.Run(() => Throw()); Console.WriteLine("Job is scheduled, will run any second. Sleeping main thread"); Thread.Sleep(5000); Console.WriteLine("Catching exception"); try { task.Wait(); } catch (Exception e) { Console.WriteLine("Swallowing exception " + e.GetType() + "\n" + e); } Thread.Sleep(1000); Console.WriteLine("Done"); } public static async void Throw() { Console.WriteLine("\tWaiting in async void"); await Task.Delay(1000); Console.WriteLine("\tThrowing in async void"); throw new Exception("Hahaha from async void"); } |
We schedule a lambda to run, add some sleeping and finally wait for the task with ordinary try
–catch
logic. The exception is propagated and handled correctly.
Bonus chatter: what is the difference between Wait()
and GetAwaiter().GetResult()
? Which one should we actually use in our library?