首先请注意,此问题未标记为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页面About Messages and Message Queues提供了有关消息循环如何在系统级工作的一些介绍性背景信息,即消息队列作为Windows API。)
我可以看到如何为具有消息队列的线程实现跨线程调用。使用Windows API,您可以通过PostThreadMessage
将消息放入特定线程的消息队列,其中包含调用某个委托的指令。消息循环 - 在该线程上运行 - 最终会到达该消息,并且将调用该委托。
From what I've read on MSDN,一个线程不会自动拥有自己的消息队列。消息队列将变得可用,例如当一个线程创建了一个窗口。没有消息队列,线程没有消息循环是没有意义的。
那么,当目标线程没有消息循环时,是否可以进行跨线程委托调用?比方说,在.NET控制台应用程序中? (从this question的答案来看,我认为使用控制台应用程序确实是不可能的。)
答案 0 :(得分:8)
很抱歉发布这么长的答案。但我认为值得解释究竟发生了什么。
A-公顷!我想我已经明白了。在特定线程上调用委托的最通用方式确实似乎是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
用于同步调用。我现在省略后者。)
第2步。现在让我们编写等待代表d
到达的 Z 的代码:
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都为其主/ UI线程提供并安装合适的SynchronizationContext
。
在特定线程上调用委托的一般过程如下:
您必须捕获目标线程( Z )SynchronizationContext
,以便Send
(同步)或Post
(异步) )该线程的代表。如何执行此操作的方法是在目标线程 Z 上存储由SynchronizationContext.Current
返回的同步上下文。 (此同步上下文必须先前已经/通过线程 Z 注册。)然后将该引用存储在线程 A 可访问的位置。
在线程 A 上,您可以使用捕获的同步上下文将任何委托发送或发布到线程 Z :zSyncContext.Post(_ => { ... }, null);
答案 1 :(得分:4)
如果你想支持在没有消息循环的线程上调用委托,你必须实现自己的,基本上。
消息循环没有什么特别的魔力:它就像普通生产者/消费者模式中的消费者一样。它保留了要做的事情的队列(通常是要响应的事件),并且它会相应地执行队列。当没有什么可做的时候,它会等待,直到队列中放置了一些东西。
换句话说:您可以将带有消息循环的线程视为单线程线程池。
您可以轻松地自己实现此功能,包括在控制台应用中。请记住,如果线程循环工作队列,它也不能做其他事情 - 而通常控制台应用程序中的主要执行线程意味着执行一系列任务然后完成。
如果您使用的是.NET 4,那么使用BlockingCollection
类实现生产者/消费者队列非常容易。
答案 2 :(得分:2)
我最近遇到过这篇文章,发现它是一个救星。正如上面的Jon Skeet所指出的那样,使用阻塞并发队列是秘诀。我在发现所有这些工作时发现的最好的“操作方法”是Mike Peretz在CodeProject上的this article。本文是SynchronizationContext的三部分系列文章的一部分,该系列提供了可以轻松转换为生产代码的代码示例。请注意,Peretz只会填写所有细节,但他也提醒我们,基本的SynchronizationContext基本上没有Post()和Send()的实现,因此实际上应该被视为一个抽象基类。基类的临时用户可能会惊讶地发现它无法解决实际问题。