【问题标题】:C# Firing events within the thread they are addedC# 在添加它们的线程中触发事件
【发布时间】:2013-08-02 06:22:30
【问题描述】:

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

  • Consumer不知道Producer的事件是否被触发 在他自己的线程或其他线程中。

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

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

[更新]

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

[更新]

由于一些 cmets 暗示这个问题没有解决方案,我想添加几行可能会改变他们的想法。考虑到BeginInvoke 可以做我想做的事,所以这应该不是不可能的(至少在理论上)。实现我自己的BeginInvoke 并在Producer 中调用它是一种看待它的方式。只是不知道BeginInvoke是怎么做到的!

【问题讨论】:

  • 你可以,但可能有更好的方法来解决你的real problem。你能解释一下你想用这个事件系统解决什么问题吗?
  • 谢谢,如果需要进一步扩展,请告诉我。
  • 好多了,我不知道怎么解决,但我现在明白你的问题是什么了。
  • 我去过那里,我认为除了驱动程序包装事件的文档中的一个重要说明之外没有一个很好的答案,说它将从不同的线程中触发,并且客户端有责任避免跨线程 gui 操作。
  • @Mehran,BeginInvoke 有效,因为 GUI 线程始终在运行消息循环。 BeginInvoke 只是向 GUI 线程发布一条消息“请为我执行此代码”。你可以自己做同样的事情,但是你必须确保调用线程运行一个消息循环。

标签: c# multithreading events


【解决方案1】:

您想要进行线程间通信。对的,这是可能的。 使用 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,请务必在退出线程之前关闭 Dispatcher。

【讨论】:

  • 谢谢,尽管它仅在 .Net 4.5 中可用,但如果您还可以包含示例代码,那就太好了。
  • 我的错,从 .Net 3 开始就可以使用了
  • 当然线程间通信是可能的。有许多可能的机制。这就是为什么@Andreas 的答案是原始问题的唯一正确答案。
【解决方案2】:

是的,有办法做到这一点。它依赖于使用 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

还有其他几种方法可以改善这一点。例如,如果事件没有订阅者,您可能希望暂停轮询硬件。或者,如果硬件没有响应,您可能希望降低轮询频率。

【讨论】:

  • Timer 的事件不是在同一个线程中吗?这是多线程的吗?
  • @Mehran 计时器的事件确实在同一个线程中执行,但是context.Post 函数将信息发送到要调用的正确线程。把它想象成Control.BeginInvoke 的行为。
  • @Mehran 那是System.Threading.Timer。回调在线程池线程上触发。您还可以创建另一个线程并定期检查该线程。例如,如果硬件 API 需要 STA 公寓模型,那将是必要的。线程池线程是 MTA。
  • 不是原始问题的答案,因为没有。
【解决方案3】:

首先,请注意,在 .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();
        }
    }
}

这个类基本上启动一个线程,并将它放入一个无限循环中,直到一个项目到达它的队列(workItems)。一旦发生这种情况,该项目(Action)就会出列并调用。然后线程再次进入睡眠状态 (WaitOne)) 直到队列中有另一个可用项。

Actions 通过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)); 

【讨论】:

    【解决方案4】:

    我之前发帖说我去过那里,但没有很好的解决方案。

    但是,我只是偶然发现了我之前在另一个上下文中做过的事情:您可以在创建包装对象时实例化一个计时器(即Windows.Forms.Timer)。这个计时器会将所有Tick 事件发布到ui 线程。

    现在,如果您的设备轮询逻辑是非阻塞且快速的,您可以直接在计时器 Tick 事件中实现它,并在那里引发您的自定义事件。

    否则,您可以继续在线程内执行轮询逻辑,而不是在线程内触发事件,您只需翻转一些布尔变量,计时器每 10 毫秒读取一次,然后触发事件。

    请注意,此解决方案仍然需要从 GUI 线程创建对象,但至少该对象的用户不必担心Invoke

    【讨论】:

    • 不是原始问题的答案,因为没有。
    【解决方案5】:

    这是可能的。一种典型的方法是使用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);
      }
    }
    

    【讨论】:

    • 不是原始问题的答案,因为没有。
    • @MartinJames:我必须同意,特别是如果 OP 不愿意从“我不希望用户代码实现消费者”让步。你是绝对正确的......你不能只是将方法的执行注入任何任意线程。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-11-01
    • 2021-03-23
    • 1970-01-01
    • 2023-03-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多