12

首先请注意,此问题未标记为或任何其他特定于 GUI 的内容。这是有意的,你很快就会看到。

其次,对不起,如果这个问题有点长。我试图将到处飘荡的各种信息汇集在一起​​,以提供有价值的信息。然而,我的问题就在“我想知道的”下面。

我的任务是最终了解 .NET 提供的在特定线程上调用委托的各种方法。


我想知道的:

  • 我正在寻找最通用的方式(不是 Winforms 或 WPF 特定的)来调用特定线程上的委托。

  • 或者,换种说法:我会感兴趣,如果以及如何,不同的方式来做到这一点(例如通过 WPF's Dispatcher)相互利用;也就是说,如果所有其他人都使用一种通用的跨线程委托调用机制。


我已经知道的:

  • 有很多与这个主题相关的课程;其中:

    • SynchronizationContext (in System.Threading)
      如果我不得不猜测,那将是最基本的一个;虽然我不明白它到底是做什么的,也不知道它是如何使用的。

    • AsyncOperation& (in ) 这些似乎是. 不知道如何使用它们。AsyncOperationManager System.ComponentModel
      SynchronizationContext

    • WindowsFormsSynchronizationContext (in System.Windows.Forms)
      的子类SynchronizationContext

    • ISynchronizeInvoke (in System.ComponentModel)
      由 Windows 窗体使用。(Control该类实现了这一点。如果我不得不猜测,我会说这个实现使用了WindowsFormsSynchronizationContext.)

    • Dispatcher& (in ) 似乎后者是 的另一个子类,而前者代表它。DispatcherSynchronizationContext System.Windows.Threading
      SynchronizationContext

  • 一些线程有自己的消息循环以及消息队列。

    (关于消息和消息队列的 MSDN 页面有一些关于消息循环如何在系统级别工作的介绍性背景信息,即作为 Windows API 的消息队列。)

    我可以看到如何为带有消息队列的线程实现跨线程调用。PostThreadMessage使用 Windows API,您可以通过包含调用某个委托的指令将消息放入特定线程的消息队列中。在该线程上运行的消息循环最终将获得该消息,并且将调用委托。

    根据我在 MSDN 上阅读的内容,线程不会自动拥有自己的消息队列。例如,当一个线程创建了一个窗口时,一个消息队列将变得可用。没有消息队列,线程有消息循环是没有意义的。

    那么,当目标线程没有消息循环时,是否可以进行跨线程委托调用?比方说,在 .NET 控制台应用程序中?(从这个问题的答案来看,我想控制台应用程序确实不可能。)

4

2 回答 2

9

很抱歉发布这么长的答案。但我认为值得解释到底发生了什么。

啊哈!我想我已经弄清楚了。在特定线程上调用委托的最通用方式确实似乎是SynchronizationContext类。

首先,.NET 框架没有提供一种默认方式来简单地将委托“发送”到任何线程,这样它就会立即在那里执行。显然,这是行不通的,因为这意味着“中断”该线程当时正在执行的任何工作。因此,目标线程自己决定如何以及何时“接收”委托;也就是说,这个功能必须由程序员提供。

因此,目标线程需要某种“接收”委托的方式。这可以通过许多不同的方式来完成。一种简单的机制是线程总是返回到某个循环(我们称之为“消息循环”),它会在其中查看队列。它会解决队列中的任何问题。当涉及到与 UI 相关的内容时,Windows 本身就是这样工作的。

下面,我将演示如何实现一个消息队列和一个 SynchronizationContextfor it,以及一个带有消息循环的线程。最后,我将演示如何在该线程上调用委托。


例子:

第 1 步。让我们首先创建一个SynchronizationContext将与目标线程的消息队列一起使用的类:

class QueueSyncContext : SynchronizationContext
{
    private readonly ConcurrentQueue<SendOrPostCallback> queue;

    public QueueSyncContext(ConcurrentQueue<SendOrPostCallback> queue)
    {
        this.queue = queue;
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        queue.Enqueue(d);
    }

    // implementation for Send() omitted in this example for simplicity's sake.
}

基本上,这只是将通过的所有委托添加Post到用户提供的队列中。(Post是异步调用的方法。Send将用于同步调用。我现在省略后者。)

第 2 步。现在让我们为等待委托到达的线程Zd编写代码:

SynchronizationContext syncContextForThreadZ = null;

void MainMethodOfThreadZ()
{
    // this will be used as the thread's message queue:
    var queue = new ConcurrentQueue<PostOrCallDelegate>();

    // set up a synchronization context for our message processing:
    syncContextForThreadZ = new QueueSyncContext(queue);
    SynchronizationContext.SetSynchronizationContext(syncContextForThreadZ);

    // here's the message loop (not efficient, this is for demo purposes only:)
    while (true)
    {
        PostOrCallDelegate d = null;
        if (queue.TryDequeue(out d))
        {
            d.Invoke(null);
        }
    }
}

步骤 3.线程Z需要在某处启动:

new Thread(new ThreadStart(MainMethodOfThreadZ)).Start();

第 4 步。最后,回到其他线程A,我们想向线程Z发送一个委托:

void SomeMethodOnThreadA()
{
    // thread Z must be up and running before we can send delegates to it:
    while (syncContextForThreadZ == null) ;

    syncContextForThreadZ.Post(_ =>
        {
            Console.WriteLine("This will run on thread Z!");
        },
        null);
}

这样做的好处是,SynchronizationContext无论您是在 Windows 窗体应用程序、WPF 应用程序还是您自己设计的多线程控制台应用程序中,它都能正常工作。Winforms 和 WPF 都SynchronizationContext为其主/UI 线程提供和安装合适的 s。

在特定线程上调用委托的一般过程如下:

  • 您必须捕获目标线程的 ( Z ) SynchronizationContext,以便您可以Send(同步)或Post(异步)委托给该线程。如何做到这一点的方法是存储SynchronizationContext.Current当你在目标线程Z时返回的同步上下文。(此同步上下文必须先前已在线程 Z 上/由线程Z注册。)然后将该引用存储在线程A可访问的某个位置。

  • 在线程A上,您可以使用捕获的同步上下文将任何委托发送或发布到线程ZzSyncContext.Post(_ => { ... }, null);

于 2011-01-30T15:28:03.617 回答
4

如果您想支持在没有消息循环的线程上调用委托,基本上您必须实现自己的。

消息循环没有什么特别神奇的地方:它就像普通生产者/消费者模式中的消费者。它保留一系列要做的事情(通常是要做出反应的事件),并通过队列进行相应的操作。当无事可做时,它会一直等到有东西被放入队列中。

换句话说:你可以把一个带有消息循环的线程想象成一个单线程线程池。

您可以自己轻松地实现这一点,包括在控制台应用程序中。请记住,如果线程在工作队列中循环,它也不能做其他事情 - 而控制台应用程序中的执行主线程通常是为了执行一系列任务然后完成。

如果您使用的是 .NET 4,那么使用BlockingCollection该类实现生产者/消费者队列非常容易。

于 2011-01-30T13:06:59.143 回答