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

时间:2013-12-20 07:22:08

标签: c# .net wpf multithreading

我正在尝试在一个小的.net 4.0应用程序(使用Visual Studio 2010编写,如果这很重要)中使用tasks,它需要在Windows 2003上运行并使用WriteableBitmap和palette参数。

因此,使用所述类必须的代码作为STA线程运行,以避免它抛出invalid cast exception(请参阅here以了解我需要STA线程的原因你有兴趣,但这不是我的问题的主旨。)

因此,我检查了堆栈溢出并遇到How to create a task (TPL) running a STA thread?The current SynchronizationContext may not be used as a TaskScheduler - 完美,所以现在我知道该怎么做,除了...

这是一个小控制台应用程序:

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

什么!?!是的,它回到Action<Task>传递给ContinueWith方法的 MTA 主题。

我将相同的调度程序传递给任务并继续但在某种程度上继续它似乎被忽略。

我确定这是愚蠢的,所以如何确保我的回调传递到ContinueWith使用STA线程?

1 个答案:

答案 0 :(得分:6)

编辑:在您阅读以下任何内容之前,这是一篇优秀的主题文章:http://blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259049.aspx;你可以跳过我的帖子直接去那里!

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

  

SynchronizationContext.Post的默认实现只是转身,通过QueueUserWorkItem将其传递给ThreadPool 。但是(...)可以从SynchronizationContext派生自己的上下文,并覆盖Post方法,使其更适合所表示的调度程序。

     

例如,对于Windows窗体,WindowsFormsSynchronizationContext实现Post以将委托传递给Control.BeginInvoke。对于WPF中的DispatcherSynchronizationContext,它调用Dispatcher.BeginInvoke。等等。

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


现在,我最初的回答是:

在思考了一下后,我认为问题是在你的控制台应用程序中没有“消息泵”这样的东西。默认的SynchronizationContext只是一块锁。它可以防止线程在资源上交叉,但它不提供任何排队或线程选择。通常,您意味着子类化SynchroContext以提供您自己的正确同步方式。 WPF和WinForms都提供了自己的子类型。

当您Wait完成任务时,很可能MainThread被阻止,而其他所有线程都在默认线程池中的一些随机线程上运行。

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

你可能会看到:

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

并检查日志中的消息顺序。从task1“hello”开始“继续”吗?

如果您不是通过StartNew创建Task1,而是将其创建为准备/暂停,然后继续,然后启动,然后等待,您也可以尝试看看会发生什么。如果我对同步运行是正确的,那么在这样的设置中,main和continuation任务将既可以在调用'1111'STA线程上运行,也可以在线程池'2222'线程上运行。

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