任务 - 如何确保ContinueWith操作是STA线程?

9

我正在尝试在一个小的.net 4.0应用程序中使用tasks(使用Visual Studio 2010编写),该应用程序需要在Windows 2003上工作并使用带调色板参数的WriteableBitmap

因此,使用该类的代码必须运行为STA线程,以避免它抛出无效转换异常(如果您有兴趣,请参见这里,但这不是我的问题的关键所在)。

因此,我在Stack overflow上查找,并发现了如何创建运行STA线程的任务(TPL)?当前的同步上下文可能不能用作任务调度程序 - 完美,所以现在我知道该怎么做了,但是...

这里有一个小型控制台应用程序:

using System;
using System.Threading;
using System.Threading.Tasks;
namespace TaskPlayingConsoleApplication
{
    class Program
    {
        [STAThread]
        static void Main()
        {
            Console.WriteLine("Before Anything: " 
                + Thread.CurrentThread.GetApartmentState());

            SynchronizationContext.SetSynchronizationContext(
                new SynchronizationContext());
            var cts = new CancellationTokenSource();
            var scheduler = TaskScheduler.FromCurrentSynchronizationContext();

            var task = Task.Factory.StartNew(
                () => Console.WriteLine(
                    "In task: " + Thread.CurrentThread.GetApartmentState()),
                cts.Token,
                TaskCreationOptions.None,
                scheduler);

            task.ContinueWith(t =>
                 Console.WriteLine(
                   "In continue: " + Thread.CurrentThread.GetApartmentState()),
                   scheduler);

            task.Wait();
        }
    }
}

以下是它的输出结果:

Before Anything: STA 
In task: STA 
In continue: MTA

天哪!?!?没错,这是关于传递给ContinueWith方法的Action<Task>的MTA线程。

我将相同的调度程序传递到任务和继续中,但在继续中似乎被忽略了。

我确信这是一些愚蠢的事情,那么如何确保我的回调传递给ContinueWith使用STA线程?


我本来想自动写入你可能忘记指定调度程序了,但是所有的都已经就位了..很奇怪。 - quetzalcoatl
1
这些结果很奇怪,我得到了不同的结果。我期望 'In task' 返回 MTA(对我来说确实如此),因为 SynchronizationContext 的默认实现使用 .NET 线程池。线程池线程默认为 MTA。 - Mike Zboray
@mikez:是的,那也让我感到惊讶。这就是为什么我认为第一个任务在发出StartNew命令时是同步运行的原因,但我想不出为什么会这样,以及为什么后来的ContinueWith没有以同样的方式处理。 - quetzalcoatl
1个回答

6

编辑:在您阅读以下内容之前,这里有一篇与此主题相关的优秀文章:http://blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259049.aspx; 您可以跳过我的帖子直接访问该网站!

描述根本原因的最重要部分:

SynchronizationContext.Post的默认实现只是通过QueueUserWorkItem将其传递给ThreadPool。但(...)可以从SynchronizationContext派生自己的上下文,并覆盖Post方法以更适合所表示的调度程序。例如,在Windows Forms中,WindowsFormsSynchronizationContext实现了Post以将委托传递给Control.BeginInvoke。对于WPF中的DispatcherSynchronizationContext,它调用Dispatcher.BeginInvoke等等。

因此,您需要使用除基本SynchronizationContext类之外的其他内容。尝试使用任何其他现有的内容,或者创建您自己的内容。示例包含在文章中。


现在,是我最初的回复:

经过一番思考,我认为问题在于控制台应用程序中没有“消息泵”的概念。默认的SynchronizationContext只是一些锁定部件。它可以防止线程在资源上相互干扰,但它不提供任何排队或线程选择。通常您需要派生SynchroContext以提供自己的适当同步方式。WPF和WinForms均提供了自己的子类型。

当您在任务上等待时,最有可能发生的情况是MainThread被阻塞,并且所有其他线程都在默认线程池中的某些随机线程上运行。

请尝试与STA / MTA标志一起将线程ID写入控制台。

您可能会看到:

STA: 1111
STA: 1111
MTA: 1234

如果您看到这个信息,那么很可能您的第一个任务是在调用线程上同步运行并立即完成的,然后您尝试“继续”它,它只是被“附加”到“队列”中,但不会立即启动(猜测,我不知道为什么;旧任务已经完成,所以ContinueWith也可以同步运行它)。然后主线程被锁定在等待状态,由于没有消息泵-它无法切换到另一个作业并休眠。然后线程池等待并清除滞留的连续任务。只是猜测。您可以尝试通过以下方式进行检查:
prepare synccontext
write "starting task1"
start task1 ( -> write "task1")
write "continuing task2"         <--- add this one 
continue: task2 ( -> write "task2")
wait

检查日志中消息的顺序。 "continuing" 是在任务1的 "hello" 之前还是之后?

您也可以尝试不使用StartNew创建Task1,而是将其创建为prepared/suspended,然后继续、启动、等待。如果我关于同步运行的想法是正确的,那么在这样的设置中,主任务和延续任务将在调用'1111' STA线程上同时运行,或者都在线程池的'2222'线程上运行。

同样地,如果所有这些都是正确的,提供一些消息泵和适当的SyncContext类型可能会解决您的问题。正如我所说,WPF和WinForms都提供了它们自己的子类型。虽然我现在不记得它们的名称,但您可以尝试使用它们。如果我记得正确,WPF会自动启动其调度程序,您不需要任何额外的设置。我不记得WinForms是怎样的。但是,对于WPF的自动启动,如果您的ConsoleApp实际上是某种单元测试,将运行许多单独的案例,则需要在案例之前关闭WPF的调度程序..但这远离了现在的话题。


也可以参考这个答案:https://dev59.com/k1HTa4cB1Zd3GeqPNwTb#14144101 虽然它的问题有所不同,但那里描述的问题是相似的。 - quetzalcoatl
我建议您将链接文章中的重要观点包含进来,以更清楚地回答问题。例如像这句引用所说:“但有一种常见的应用程序没有同步上下文:控制台应用程序。当调用控制台应用程序的Main方法时,SynchronizationContext.Current将返回null。这意味着,如果在控制台应用程序中调用异步方法,除非您做一些特殊的事情,否则您的异步方法将没有线程亲和性:这些异步方法内的继续可能会在“任何地方”运行。” - Martin Liversage
1
非常感谢 - 基于这个和链接的文章,我自己制作了一个小的同步上下文并将其连接起来,确保所有东西都在同一个线程上调用,这对我的控制台应用程序已经足够好了! - kmp
@MartinLiversage:这是重要的一部分,但请注意kmp已经SetSynchronizationContext,因此在创建和启动第一个任务时,Current肯定不为null。文章中充满了重要的细节。我不知道该引用什么...核心观点是当你第一次看到默认基础SynchroContext类时,它比任何人想象的都要不可用得多。但你是对的。我会在这里包含关于它的引用,以防目标文章消失。 - quetzalcoatl
1
@Simon_Weaver - 我曾在一段时间前上传到Github上的一个库中编写了一个单元测试,我在其中使用了这个。请查看nsane存储库中的内部类SingleThreadSynchronizationContext - kmp
显示剩余3条评论

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