为什么在这个单元测试中,Task.ContinueWith无法执行?

10

我遇到了一个单元测试问题,因为TPL任务从未执行其ContinueWith(x, TaskScheduler.FromCurrentSynchronizationContext())而失败。

问题的原因是在任务启动之前意外创建了Winforms UI控件。

以下是可重现该问题的示例。如果按原样运行测试,则测试通过。如果取消注释Form行并运行测试,则测试失败。

[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());
    }
}

有人能解释为什么会这样吗?

我原以为是因为使用了错误的SynchronizationContext,但实际上,ContinueWith根本没有执行!而且,在这个单元测试中,是否使用了正确的SynchronizationContext都无关紧要,因为只要在任何线程上调用了waitHandle.set(),测试就应该通过。


是否发生异常? - Yuval Itzchakov
@SriramSakthivel 问题有点傻 - 你试过把 var f = new Form(); 的注释去掉了吗?我在 MsTest、VS2013u4、Win8.1、Net 4.5.1 上测试过。 - oatsoda
@OffHeGoes 抱歉我忽略了那个。现在正在写答案 :) - Sriram Sakthivel
@YuvalItzchakov 没有异常。如果我删除 TaskScheduler.FromCurrentSynchronizationContext(),它也会通过。 - oatsoda
2个回答

8

我在你的代码中忽略了评论部分,事实上,当取消注释var f = new Form();时会失败。

原因很微妙,如果Control类发现SynchronizationContext.Currentnull或其类型为System.Threading.SynchronizationContext,它将自动覆盖同步上下文为WindowsFormsSynchronizationContext

一旦Control类使用WindowsFormsSynchronizationContext覆盖SynchronizationContext.Current,所有对SendPost的调用都需要运行Windows消息循环才能正常工作。这只有在创建Handle并运行消息循环后才会发生。

有问题的代码的相关部分:

internal Control(bool autoInstallSyncContext)
{
    ...
    if (autoInstallSyncContext)
    {
       //This overwrites your SynchronizationContext
        WindowsFormsSynchronizationContext.InstallIfNeeded();
    }
}

您可以参考WindowsFormsSynchronizationContext.InstallIfNeeded的源码,网址在这里
如果您希望覆盖同步上下文,您需要自行实现SynchronizationContext来使其正常工作。
解决方法:
internal class MyContext : SynchronizationContext
{

}

[TestMethod]
public void TestMethod1()
{
    // Create new sync context for unit test
    SynchronizationContext.SetSynchronizationContext(new MyContext());

    var waitHandle = new ManualResetEvent(false);

    var doer = new DoSomethinger();
    var f = new Form();

    doer.DoSomethingAsync(() => waitHandle.Set());

    Assert.IsTrue(waitHandle.WaitOne(10000), "Wait timeout exceeded.");
}

上面的代码按预期工作 :)
另外,您可以将WindowsFormsSynchronizationContext.AutoInstall设置为false,这将防止自动覆盖上述同步上下文。(感谢@OffHeGoes在评论中提到这一点)

啊,我想我知道我做错了什么——在尝试调试问题时,我检查了TaskScheduler.Current,而我应该检查的是SynchronizationContext.Current - oatsoda
这真的很有用。我刚刚查看了 WindowsFormsSynchronizationContext.InstallIfNeeded(),发现它检查了 typeof(SynchronizationContext) - oatsoda
1
还有一点需要注意,你也可以使用以下代码停止它:WindowsFormsSynchronizationContext.AutoInstall = false; - oatsoda
1
@Sriram 很棒的回答!+1 - Yuval Itzchakov

4
注释掉这行代码后,您的SynchronizationContext将成为您创建的默认上下文。这会导致 TaskScheduler.FromCurrentSynchrozisationContext() 使用默认调度程序,在线程池中运行延续。
一旦您创建了像Form这样的Winforms对象,当前的SynchronizationContext就变成了一个WindowsFormsSynchronizationContext,它将返回一个取决于WinForms消息泵的调度程序以安排延续。
由于单元测试中没有WinForms消息泵,因此延续永远不会被运行。

谢谢 - 我在尝试调试问题时检查了 TaskScheduler.Current,但我应该检查的是 SynchronizationContext.Current - oatsoda

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接