.NET:如何在特定线程上调用委托?(ISynchronizeInvoke,Dispatcher,AsyncOperation,SynchronizationContext等。)
-
27-10-2019 - |
题
首先注意这个问题没有标记 winforms的 或 wpf的 或任何其他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都提供并安装合适的 SynchronizationContext
s为他们的主/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()的实现,因此真的应该被视为抽象基类。基类的临时用户可能会惊讶地发现它不能解决现实世界的问题。