C#在添加的线程中触发事件
本文关键字:事件 线程 添加 | 更新日期: 2023-09-27 18:25:51
考虑两个类;Producer
和Consumer
(与经典模式相同,每个模式都有自己的线程)。Producer
是否可能有一个Event
,Consumer
可以向其注册,并且当生产者触发事件时,消费者的事件处理程序在其自己的线程中运行?以下是我的假设:
-
Consumer
不知道是否触发了Producer
的事件在他自己或另一条线内。 -
Producer
和Consumer
都不是Control
的后代,所以它们没有继承了BeginInvoke
方法。
PS。我不想实现Producer
-Consumer
模式。这是两个简单的类,我正试图重构生产者,使其包含线程。
[更新]
为了进一步扩展我的问题,我试图用最简单的方式包装一个硬件驱动程序。例如,我的包装器将有一个StateChanged
事件,主应用程序将向其注册,以便在硬件断开连接时通知它。由于实际的驱动程序除了轮询之外没有其他方法来检查它的存在,所以我需要启动一个线程来定期检查它。一旦它不再可用,我将触发需要在添加的同一线程中执行的事件。我知道这是一个经典的生产者-消费者模式,但由于我试图简化使用驱动程序包装器的过程,我不希望用户代码实现消费者。
[更新]
由于一些评论表明这个问题没有解决方案,我想补充几句可能会改变他们想法的话。考虑到BeginInvoke
可以做我想做的事,所以这应该不是不可能的(至少在理论上)。实现我自己的BeginInvoke
并在Producer
中调用它是看待它的一种方式。只是我不知道BeginInvoke
是如何做到的!
您想要进行线程间通信。是的,这是可能的。使用System.Windows.Threading.Dispatcherhttp://msdn.microsoft.com/en-us/library/system.windows.threading.dispatcher.aspx
Dispatcher为特定线程维护一个按优先级排列的工作项队列。在线程上创建Dispatcher时,它将成为唯一可以与该线程关联的Dispatcher,即使Dispatcher已关闭。如果尝试获取当前线程的CurrentDispatcher,但Dispatcher未与该线程关联,则会创建一个Dispatcher。创建DispatcherObject时也会创建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 e.g. 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<Action>();
this.workItemAvailable = new AutoResetEvent(initialState: false);
new Thread(ProcessWorkItems) { IsBackground = true }.Start();
}
readonly Queue<Action> 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
)。一旦发生这种情况,项目;CCD_ 33—已退出队列并被调用。然后线程再次进入睡眠状态(WaitOne
)),直到队列中有另一个项目可用。
通过CCD_ 36方法将CCD_。因此,本质上,我们现在可以通过调用该方法将要执行的代码发送到特定的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…`.
}
}
瞧!
p.S.(未详细讨论):作为一个附加组件,您可以使用一种称为
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
)。此计时器将向ui线程发布所有Tick
事件。
现在,如果您的设备轮询逻辑是非阻塞且快速的,那么您可以直接在计时器Tick
事件中实现它,并在那里引发您的自定义事件。
否则,您可以继续在线程内执行轮询逻辑,而不是在线程内触发事件,您只需翻转一些布尔变量,计时器每10毫秒读取一次,然后触发事件。
请注意,此解决方案仍然要求从GUI线程创建对象,但至少对象的用户不必担心Invoke
。
这是可能的。一种典型的方法是使用BlockingCollection
类。此数据结构的工作方式与普通队列类似,只是如果队列为空,则出列操作会阻塞调用线程。生产者将通过调用Add
来对项目进行排队,消费者将通过调用Take
来对它们进行出列。使用者通常运行自己的专用线程,旋转一个无限循环,等待项目出现在队列中。这或多或少是UI线程上消息循环的操作方式,也是获得Invoke
和BeginInvoke
操作以完成封送处理行为的基础。
public class Consumer
{
private BlockingCollection<Action> queue = new BlockingCollection<Action>();
public Consumer()
{
var thread = new Thread(
() =>
{
while (true)
{
Action method = queue.Take();
method();
}
});
thread.Start();
}
public void BeginInvoke(Action method)
{
queue.Add(item);
}
}