如何保证任务在当前线程上同步运行?

3

我知道在本网站和其他网站上有几个类似的问题,但标准的解决方法似乎在我的情况下不起作用。完成此要求的正常方法是在相关的Task.Factory.StartNew重载中,将TaskScheduler.FromCurrentSynchronizationContext()作为TaskScheduler输入参数使用:

// Set uiTaskScheduler whilst on the UI thread
TaskScheduler uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
...
Task.Factory.StartNew(() => SomeMethodToRunAsynchronously(), 
    CancellationToken.None, TaskCreationOptions.None, uiTaskScheduler);

看起来这就足以在UI线程上安排一个Task来运行,但在我的情况下似乎并不起作用。在我的情况下,我有一个UiThreadManager类,它除了其他事情之外还有一个RunAsynchronously方法:

public Task RunAsynchronously(Action method)
{
    return Task.Run(method);
}

这部分工作很好。我面临的问题是,当运行单元测试时,这个类被一个MockUiThreadManager类替换了(两者都实现了应用程序代码使用的IUiThreadManager接口),而我似乎无法强制让此方法在UI线程上运行:

public Task RunAsynchronously(Action method)
{
    return Task.Factory.StartNew(() => method(), 
        CancellationToken.None, TaskCreationOptions.None, UiTaskScheduler);
}

MockUiThreadManager类有一个static UiTaskScheduler属性,该属性在UI线程上设置(如下所示),因此我假设通过上述方法传递的所有代码都将按预期在该线程上运行:

SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
MockUiThreadManager.UiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();

然而,当运行单元测试时,我注意到它在测试的代码之前就已经完成了。因此,我添加了一些断点,并在 Visual Studio 立即窗口中调用了 System.Threading.Thread.CurrentThread.ManagedThreadId 来检查每个断点,果然,当代码通过上述方法时,线程 ID 发生了变化。
所以基本上,我正在寻找一种方法来“伪造”一个基于 Task 的异步调用,使其在运行单元测试期间确保该方法实际上在 UI 线程上同步运行。有人看出我做错了什么,或者有其他建议吗?
更新>>> 好的,关于 UI 线程,我在这里使用了错误的术语。单元测试不在 UI 线程上运行,因为没有 UI... 但是,情况仍然相同。为了澄清,我只需要我的测试在应用程序代码上同步地运行,并且在它开始的单个线程上运行。问题在于当应用程序正在运行时,会有很多基于 Task 的异步代码,这需要通过 MockUiThreadManager 类进行同步测试。

这对我来说似乎非常违反直觉。通常当你运行 Task.Factory.StartNew 时,你会在 UI 线程上。 - Aron
当您将UiSynchronizationContext传递给StartNew时,它的值是多少?请展示一下您在测试中如何调用Task.Factory.StartNew - Yuval Itzchakov
通常情况下,当您运行Task.Factory.StartNew时,您将在UI线程上运行...这完全取决于当前上下文,@YuvalItzchakov,您是指UiTaskScheduler吗?我没有将其传递给StartNew方法...正如我在问题中所说,我在UI线程上初始化它...在Intellisense中,它只显示Id=1,因此它绝对不是null。此外,我没有在我的测试中调用StartNew,我调用了(IMockUiThreadManagerRunAsynchronously方法,如上所示。 - Sheridan
@Sheridan:你尝试在MockUiThreadManagerStartNew()调用中传递TaskScheduler.FromCurrentSynchronizationContext()而不是UiTaskScheduler了吗? - toadflakz
@Sheridan:经过深入研究,我得出结论,Dirk是正确的,你需要编写一个线程亲和力同步上下文,因为UI同步上下文(无论是Dispatcher还是WinForms)使用某种形式的排队来实现你在UI线程上体验到的行为。我找到了一个,但我不确定使用它的后果会是什么(或者代码是否安全)-http://wcf-examples.googlecode.com/svn/trunk/CodeRunner/ServiceModel.Extensions/ThreadAffinity/AffinitySynchronizer.cs - toadflakz
显示剩余5条评论
2个回答

4

在继续搜索后,我现在找到了我需要的解决方案,并且可以用几行代码实现,所以肯定不需要实现自己的SynchronizationContext类。看起来这么简单,我很惊讶我之前没发现它。在我的MockUiThreadManager类中,该类用于运行单元测试时,我现在有以下代码:

public Task RunAsynchronously(Action method)
{
    Task task = new Task(method);
    task.RunSynchronously();
    return task;
}

我可以确认它确实可以做到其所描述的功能,并且在运行`method`函数时会在与测试运行相同的线程上同步运行。
为了完整起见,Task.RunSynchronously方法也有一个接受TaskScheduler参数的重载,但在我的情况下不需要这样做,因此我将不再需要我的MockUiThreadManager.UiTaskScheduler属性。

2
new SynchronizationContext()会返回一个新的默认同步对象,它将任务调度到线程池中执行(参考源代码)。 TaskScheduler.FromCurrentSynchronizationContext()会返回一个任务调度器,该调度器使用SynchronizationContext.Current,而你刚刚使用SynchronizationContext.SetSynchronizationContext将其设置为线程池同步上下文。
这意味着在该调度程序上安排任务将使用线程池来执行它们,而不是特定的线程。
通常情况下,除非该线程有某种消息队列,否则甚至无法在特定线程上安排工作,除非。这就是为什么您可以安排工作在UI线程上运行。
我不知道您使用哪个单元测试框架。它可能有一个UI来显示测试结果,但这并不意味着测试是在该线程上运行的。
我不确定如何解决这个问题,除非编写自己的SynchronizationContext类。如果您正在使用像ViewModelController之类的东西,那么当然可以对它们进行单元测试,但它们应该无论您使用哪种同步上下文都能正常工作,因为我认为UI本身不太适合进行单元测试。

抱歉@Dirk,我不应该使用UI线程这个术语...我是指主线程...我只需要测试以同步方式运行所有内容。 - Sheridan

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