This is the second 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?

I once found the following code in some WinForms application:

private async Task Broadcast(Func< object, Task> action, object sender)
{
	if (!Control.InvokeRequired)
	{
		await action(sender);
	}
	else
	{
		await (Task)Control.Invoke(action, new[] { sender});
	}
}

Here Control is any WinForms control, usually some kind of a Form. The code is supposed to run some lambda on UI thread, is this is possible and if it is required. This snipped worked quite good for some time, however, one day it started throwing ObjectDisposedException on Control.Invoke. The reason is quite obvious: we don’t check if the Control is still there, and since this code runs in non-ui thread, the control might get disposed in any arbitrary moment in time. The question is: how to solve it? We might of course try to catch exception and run action normally, however, let’s start experimenting with the code.

First attempt

We can simply check if control is disposed before running the lambda:

private async Task Broadcast(Func< object, Task> action, object sender)
{
	if (!Control.InvokeRequired || Control.IsDisposed)
	{
		await action(sender);
	}
	else
	{
		await (Task)Control.Invoke(action, new[] { sender});
	}
}

Looks good, unfortunately, it doesn’t work because of a race condition. Imagine that worker thread checks whether control was disposed and tries to execute second branch (with Control.Invoke). Before actually invoking the method, the thread might be suspended and other thread might dispose the control.

Second attempt

Let’s decompile some WinForms code. If we go to the Component.Dispose method, we will see the following:

protected virtual void Dispose(bool disposing) {
	if (disposing) {
		lock(this) {
			if (site != null && site.Container != null) {
				site.Container.Remove(this);
			}
			if (events != null) {
				EventHandler handler = (EventHandler)events[EventDisposed];
				if (handler != null) handler(this, EventArgs.Empty);
			}
		}
	}
}

It looks like Dispose locks itself before disposing the object. We can use this to block control from disposing until we finish our job:

private async Task Broadcast(Func< object, Task> action, object sender)
{
	lock(Control)
	{
		if (!Control.InvokeRequired || Control.IsDisposed)
		{
			await action(sender);
		}
		else
		{
			await (Task)Control.Invoke(action, new[] { sender});
		}
	}
}

Looks good and in theory this could work. However, this code has some fatal flaw: if action lambda awaits for a method which in turn call Broadcast from other thread, we will get a deadlock. We need to come up with fancier solution.

Third attempt

private async Task Broadcast(Func< object, Task> action, object sender)
{
	if (!Control.InvokeRequired
	{
		await action(sender);
		return;
	}
	
	IAsyncResult result = null;
	lock (Control)
	{
		if (!Control.IsDisposed)
		{
			result = Control.BeginInvoke(action, new[] { sender });
		}
	}
	if (result != null)
	{
		result.AsyncWaitHandle.WaitOne();
		Task toWait = null;
		lock (Control)
		{
			if (!Control.IsDisposed)
			{
				toWait = (Task)Control.EndInvoke(result);
			}
		}
		if (toWait != null)
		{
			await toWait;
		}
	}
}

We first check if invoke is required — if it is not, we simply execute our action.
Next, we try to invoke action in non-blocking manner. We lock a control, check if it is disposed, and if it is not, we post our lambda. We release the lock, so the lambda is able to call the method deeper in the execution stack without deadlocking. Next, we try to wait for our lambda, lock control once again, once again check if it is disposed, and finally obtain task. Now, we need to release the lock once again, and since our lambda represents asynchronous action, we have a Task to await for.

Summary

It looks like we found a solution for our problem. I leave it for your discretion if you prefer to call the code with multiple locks and tasks, or just simply catch the exception and rerun the action. And, what’s more important, I cannot guarantee that last solution works fine in all cases — it worked quite reliably in my situation, however, threads can be very surprising.