【问题标题】:Why does the Task.ContinueWith fail to execute in this Unit Test?为什么 Task.ContinueWith 在此单元测试中执行失败?
【发布时间】:2015-01-27 06:45:50
【问题描述】:

我遇到了一个单元测试失败的问题,因为 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(),测试就应该通过。

【问题讨论】:

  • 是否出现异常?
  • @SriramSakthivel 愚蠢的问题 - 您是否尝试过 var f = new Form(); 未评论?我是一个 MsTest,VS2013u4,Win8.1,Net 4.5.1
  • @OffHeGoes 抱歉,我忽略了这一点。现在写一个答案:)
  • @YuvalItzchakov 不,也不例外。如果我删除TaskScheduler.FromCurrentSynchronizationContext(),它也会通过。

标签: c# winforms unit-testing task-parallel-library


【解决方案1】:

我忽略了您代码中的 cmets 部分,在取消注释 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.InstallIfNeededhere的来源。

如果您想覆盖 SynchronizationContext,您需要自定义实现 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,这将防止自动覆盖上述同步上下文。(感谢 OP @OffHeGoes 在 cmets 中提到这一点)

【讨论】:

  • 啊,我想我知道我做错了什么——我在尝试调试问题时检查了TaskScheduler.Current——我应该检查SynchronizationContext.Current
  • 知道这一点真的很有用。我刚刚查看了WindowsFormsSynchronizationContext.InstallIfNeeded(),可以看到它检查了typeof(SynchronizationContext)
  • 还注意到您也可以这样做来阻止它:WindowsFormsSynchronizationContext.AutoInstall = false;
  • @Sriram 很好的答案! +1
【解决方案2】:

注释掉该行后,您的SynchronizationContext 是您创建的默认值。这将导致TaskScheduler.FromCurrentSynchrozisationContext() 使用默认调度程序,该调度程序将在线程池上运行延续。

一旦您创建了像您的 Form 一样的 Winforms 对象,当前的 SynchronizationContext 将变为 WindowsFormsSynchronizationContext,这反过来将返回一个调度程序,该调度程序依赖于 WinForms 消息泵来安排继续。

由于单元测试中没有 WinForms 泵,因此永远不会运行延续。

【讨论】:

  • 谢谢 - 我在尝试调试问题时检查了TaskScheduler.Current - 我应该检查SynchronizationContext.Current
猜你喜欢
  • 1970-01-01
  • 2022-06-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-17
相关资源
最近更新 更多