I have come across a problem with a unit test that failed because a TPL Task never executed its ContinueWith(x, TaskScheduler.FromCurrentSynchronizationContext())
.
The problem turned out to be because a Winforms UI Control was accidentally being created before the Task was started.
Here is an example that reproduces it. You will see that if you run the test as-is, it passes. If you run the test with the Form line uncommented, it fails.
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
// Create new sync context for unit test
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
var waitHandle = new ManualResetEvent(false);
var doer = new DoSomethinger();
//Uncommenting this line causes the ContinueWith part of the Task
//below never to execute.
//var f = new Form();
doer.DoSomethingAsync(() => waitHandle.Set());
Assert.IsTrue(waitHandle.WaitOne(10000), "Wait timeout exceeded.");
}
}
public class DoSomethinger
{
public void DoSomethingAsync(Action onCompleted)
{
var task = Task.Factory.StartNew(() => Thread.Sleep(1000));
task.ContinueWith(t =>
{
if (onCompleted != null)
onCompleted();
}, TaskScheduler.FromCurrentSynchronizationContext());
}
}
Can anyone explain why this is the case?
I thought it might have been because the wrong SynchronizationContext
is used, but actually, the ContinueWith
never executes at all! And besides, in this unit test, whether or not it is the correct SynchronizationContext
is irrelevant because as long as the waitHandle.set()
is called on any thread, the test should pass.
With the line commented out, your
SynchronizationContext
is the default one you created. This will causeTaskScheduler.FromCurrentSynchrozisationContext()
to use the default scheduler, which will run the continuation on the thread pool.Once you create a Winforms object like your Form, the current
SynchronizationContext
becomes a WindowsFormsSynchronizationContext, which in turn will return a scheduler that depends on the WinForms message pump to schedule the continuation.Since there is no WinForms pump in a unit test, the continuation never gets run.
I overlooked the comments section in your code, Indeed that fails when uncommenting the
var f = new Form();
Reason is subtle,
Control
class will automatically overwrite the synchronization context toWindowsFormsSynchronizationContext
if it sees thatSynchronizationContext.Current
isnull
or its is of typeSystem.Threading.SynchronizationContext
.As soon as Control class overwrite the
SynchronizationContext.Current
withWindowsFormsSynchronizationContext
, all the calls toSend
andPost
expects the windows message loop to be running in order to work. That's not going to happen till you created theHandle
and you run a message loop.Relevant part of the problematic code:
You can refer the source of
WindowsFormsSynchronizationContext.InstallIfNeeded
here.If you want to overwrite the
SynchronizationContext
, you need your custom implementation ofSynchronizationContext
to make it work.Workaround:
Above code works as expected :)
Alternatively you could set
WindowsFormsSynchronizationContext.AutoInstall
tofalse
, that will prevent automatic overwriting of the synchronization context mentioned above.(Thanks for OP @OffHeGoes for mentioning this in comments)