C#在添加的线程中触发事件
考虑两个class级; Producer
和Consumer
(与经典模式相同,每个都有自己的线程)。 Producer
是否可以拥有一个Consumer
可以注册的事件,当生产者触发事件时,消费者的事件处理程序是在自己的线程中运行的? 以下是我的假设:
-
Consumer
不知道Producer
的事件是否在他自己的线程或其他线程中被触发。 -
Producer
和Consumer
都不是Control
后代,因此它们没有inheritanceBeginInvoke
方法。
PS。 我不打算实现Producer
– Consumer
模式。 这是两个简单的类,我正在尝试重构生成器,因此它包含线程。
[UPDATE]
为了进一步扩展我的问题,我试图以最简单的方式包装硬件驱动程序。 例如,我的包装器将有一个StateChanged
事件,主应用程序将注册该事件,以便在硬件断开连接时通知它。 由于实际的驱动程序除了轮询以外没有任何其他方法来检查它的存在,我将需要启动一个线程来定期检查它。 一旦它不再可用,我将触发需要在添加的同一线程中执行的事件。 我知道这是一个经典的Producer-Consumer模式,但由于我正在尝试简化使用我的驱动程序包装器,我不希望用户代码实现使用者。
[UPDATE]
由于一些评论表明没有解决这个问题的方法,我想补充一些可能会改变他们想法的路线。 考虑到BeginInvoke
可以做我想要的,所以它不应该是不可能的(至少在理论上)。 实现我自己的BeginInvoke
并在Producer
调用它是一种查看它的方法。 只是我不知道BeginInvoke
是如何做到的!
你想做线程间的沟通。 对的,这是可能的。 使用System.Windows.Threading.Dispatcher http://msdn.microsoft.com/en-us/library/system.windows.threading.dispatcher.aspx
Dispatcher维护特定线程的优先级工作项队列。 在线程上创建Dispatcher时,即使Dispatcher关闭,它也会成为唯一可与该线程关联的Dispatcher。 如果您尝试获取当前线程的CurrentDispatcher并且Dispatcher未与该线程关联,则将创建Dispatcher。 创建DispatcherObject时也会创建Dispatcher。 如果在后台线程上创建Dispatcher,请确保在退出线程之前关闭调度程序。
是的,有办法做到这一点。 它依赖于使用SynchronizationContext
类( docs )。 同步上下文通过方法Send
(调用线程同步)和Post
(调用线程的异步)抽象消息从一个线程发送到另一个线程的操作。
让我们稍微简单一点,你只需要捕获一个同步上下文,即“创建者”线程的上下文。 你会做这样的事情:
using System.Threading; class HardwareEvents { private SynchronizationContext context; private Timer timer; public HardwareEvents() { context = SynchronizationContext.Current ?? new SynchronizationContext(); timer = new Timer(TimerMethod, null, 0, 1000); // start immediately, 1 sec interval. } private void TimerMethod(object state) { bool hardwareStateChanged = GetHardwareState(); if (hardwareStateChanged) context.Post(s => StateChanged(this, EventArgs.Empty), null); } public event EventHandler StateChanged; private bool GetHardwareState() { // do something to get the state here. return true; } }
现在,将在调用事件时使用创建线程的同步上下文。 如果创建线程是UI线程,则它将具有框架提供的同步上下文。 如果没有同步上下文,则使用默认实现,该实现在线程池上调用。 如果要提供自定义方式将消息从生产者发送到使用者线程, SynchronizationContext
是一个可以子类化的类。 只需覆盖Post
和Send
即可发送消息。
如果您希望每个事件订阅者都可以在自己的线程上回调,则必须在add
方法中捕获同步上下文。 然后,您可以保持成对的同步上下文和委托。 然后,当提升事件时,您将遍历同步上下文/委托对并依次Post
每个对。
还有其他几种方法可以改善这一点。 例如,如果没有该事件的订阅者,您可能希望暂停轮询硬件。 或者,如果硬件没有响应,您可能希望取消轮询频率。
首先,请注意在.NET /基类库中,事件订阅者通常有义务确保其回调代码在正确的线程上执行。 这使得事件生产者很容易:它可能只是触发其事件而无需关心其各种订阅者的任何线程关联。
这是一个可能实现的逐步完整示例。
让我们从简单的事情开始: Producer
类及其事件Event
。 我的示例不包括触发此事件的方式和时间:
class Producer { public event EventHandler Event; // raised eg with `Event(this, EventArgs.Empty);` }
接下来,我们希望能够将Consumer
实例订阅到此事件并在特定线程上回调(我将这种线程称为“工作线程”):
class Consumer { public void SubscribeToEventOf(Producer producer, WorkerThread targetWorkerThread) {…} }
我们如何实现这个?
首先,我们需要将代码“发送”到特定工作线程的方法。 由于无法在任何时候强制线程执行特定方法,因此必须安排工作线程明确等待工作项。 一种方法是通过工作项队列。 这是WorkerThread
的可能实现:
sealed class WorkerThread { public WorkerThread() { this.workItems = new Queue(); this.workItemAvailable = new AutoResetEvent(initialState: false); new Thread(ProcessWorkItems) { IsBackground = true }.Start(); } readonly Queue workItems; readonly AutoResetEvent workItemAvailable; public void QueueWorkItem(Action workItem) { lock (workItems) // this is not extensively tested btw. { workItems.Enqueue(workItem); } workItemAvailable.Set(); } void ProcessWorkItems() { for (;;) { workItemAvailable.WaitOne(); Action workItem; lock (workItems) // dito, not extensively tested. { workItem = workItems.Dequeue(); if (workItems.Count > 0) workItemAvailable.Set(); } workItem.Invoke(); } } }
这个类基本上启动一个线程,并将它置于一个无休止的循环中( WaitOne
),直到一个项到达其队列( workItems
)。 一旦发生这种情况,项目 – 一个Action
– 就会出列并调用。 然后线程再次进入hibernate状态( WaitOne
)),直到队列中有另一个项目可用。
Action
通过QueueWorkItem
方法放入队列。 所以基本上我们现在可以通过调用该方法将要执行的代码发送到特定的WorkerThread
实例。 我们现在准备实现Customer.SubscribeToEventOf
:
class Consumer { public void SubscribeToEventOf(Producer producer, WorkerThread targetWorkerThread) { producer.Event += delegate(object sender, EventArgs e) { targetWorkerThread.QueueWorkItem(() => OnEvent(sender, e)); }; } protected virtual void OnEvent(object sender, EventArgs e) { // this code is executed on the worker thread(s) passed to `Subscribe…`. } }
瞧!
PS (未详细讨论):作为附加组件,您可以使用称为
SynchronizationContext
的标准.NET机制将代码发送到WorkerThread
:sealed class WorkerThreadSynchronizationContext : SynchronizationContext { public WorkerThreadSynchronizationContext(WorkerThread workerThread) { this.workerThread = workerThread; } private readonly WorkerThread workerThread; public override void Post(SendOrPostCallback d, object state) { workerThread.QueueWorkItem(() => d(state)); } // other overrides for `Send` etc. omitted }
在
WorkerThread.ProcessWorkItems
的开头,您将为该特定线程设置同步上下文,如下所示:SynchronizationContext.SetSynchronizationContext( new WorkerThreadSynchronizationContext(this));
我之前发布过,我去过那里,并没有很好的解决方案。
但是,我之前偶然发现了我在另一个上下文中所做的事情:您可以在创建包装器对象时实例化一个计时器(即Windows.Forms.Timer
)。 此计时器将所有Tick
事件发布到ui线程。
现在,如果您的设备轮询逻辑是非阻塞且快速的,您可以直接在计时器Tick
事件中实现它,并在那里引发自定义事件。
否则,您可以继续在线程内部执行轮询逻辑,而不是在线程内触发事件,只需翻转一个布尔变量,该变量每10 ms由定时器读取一次,然后触发该事件。
请注意,此解决方案仍然要求从GUI线程创建对象,但至少对象的用户不必担心Invoke
。
有可能的。 一种典型的方法是使用BlockingCollection
类。 此数据结构的工作方式与普通队列类似,只是如果队列为空,则出队操作会阻塞调用线程。 产品将通过调用Add
来排队项目,消费者将通过调用Take
将它们出列。 消费者通常运行它自己的专用线程,旋转无限循环,等待项目出现在队列中。 这或多或少是UI线程上的消息循环如何操作,并且是获取Invoke
和BeginInvoke
操作以完成封送行为的基础。
public class Consumer { private BlockingCollection queue = new BlockingCollection (); public Consumer() { var thread = new Thread( () => { while (true) { Action method = queue.Take(); method(); } }); thread.Start(); } public void BeginInvoke(Action method) { queue.Add(item); } }