C#在添加的线程中触发事件

本文关键字:事件 线程 添加 | 更新日期: 2023-09-27 18:25:51

考虑两个类;ProducerConsumer(与经典模式相同,每个模式都有自己的线程)。Producer是否可能有一个EventConsumer可以向其注册,并且当生产者触发事件时,消费者的事件处理程序在其自己的线程中运行?以下是我的假设:

  • Consumer不知道是否触发了Producer的事件在他自己或另一条线内。

  • ProducerConsumer都不是Control的后代,所以它们没有继承了BeginInvoke方法。

PS。我不想实现Producer-Consumer模式。这是两个简单的类,我正试图重构生产者,使其包含线程。

[更新]

为了进一步扩展我的问题,我试图用最简单的方式包装一个硬件驱动程序。例如,我的包装器将有一个StateChanged事件,主应用程序将向其注册,以便在硬件断开连接时通知它。由于实际的驱动程序除了轮询之外没有其他方法来检查它的存在,所以我需要启动一个线程来定期检查它。一旦它不再可用,我将触发需要在添加的同一线程中执行的事件。我知道这是一个经典的生产者-消费者模式,但由于我试图简化使用驱动程序包装器的过程,我不希望用户代码实现消费者。

[更新]

由于一些评论表明这个问题没有解决方案,我想补充几句可能会改变他们想法的话。考虑到BeginInvoke可以做我想做的事,所以这应该不是不可能的(至少在理论上)。实现我自己的BeginInvoke并在Producer中调用它是看待它的一种方式。只是我不知道BeginInvoke是如何做到的!

C#在添加的线程中触发事件

您想要进行线程间通信。是的,这是可能的。使用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是一个类,如果您想提供一种从生产者向消费者线程发送消息的自定义方式,则可以对其进行子类化。只需覆盖PostSend即可发送所述消息。

如果您希望每个事件订阅者都能在自己的线程上被回调,那么您必须在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线程上消息循环的操作方式,也是获得InvokeBeginInvoke操作以完成封送处理行为的基础。

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