.NET:如何在特定线程上调用委托?(ISynchronizeInvoke,Dispatcher,AsyncOperation,SynchronizationContext等。)

StackOverflow https://stackoverflow.com/questions/4843010

首先注意这个问题没有标记 或任何其他GUI特定的。这是故意的,你很快就会看到。

其次,对不起,如果这个问题有点长。我试图把这里和那里浮动的各种信息汇集在一起,以便也提供有价值的信息。然而,我的问题就在"我想知道的"下。

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


我想知道的:

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

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


我已经知道的:

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

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

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

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

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

    • Dispatcher &DispatcherSynchronizationContext (在 System.Windows.Threading)
      似乎后者是另一个子类 SynchronizationContext, ,以及前代表。

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

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

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

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

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

有帮助吗?

解决方案

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

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

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

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

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


例子::

步骤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 将用于同步调用。我现在省略了后者。)

第二步。 现在让我们写 线程的代码 Z 等待代表 d 到达目的地:

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);
        }
    }
}

第三步。 线程 Z 需要从某个地方开始:

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

第四步。 最后,回来 在其他线程上 A, ,我们想向thread发送委托 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都提供并安装合适的 SynchronizationContexts为他们的主/UI线程。

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

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

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

其他提示

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

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

换句话说:您可以将具有消息循环的线程视为单线程线程池。

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

如果您使用的是.NET4,那么使用 BlockingCollection 类。

我最近遇到了这篇文章,发现它是一个救星。使用阻塞,并发队列是秘密酱汁,正如上面的Jon Skeet所指出的那样。我在完成所有这些工作时发现的最好的"操作方法"是 本文 在迈克*佩雷茨的CodeProject上。本文是SynchronizationContext上由三部分组成的系列文章的一部分,其中提供了可以轻松转换为生产代码的代码示例。注意Peretz不仅填写了所有的细节,但他也提醒我们,base SynchronizationContext基本上没有post()和Send()的实现,因此真的应该被视为抽象基类。基类的临时用户可能会惊讶地发现它不能解决现实世界的问题。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top