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

时间:2013-08-02 06:22:31

标签: c# multithreading events

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

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

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

PS。我不是要尝试实施Producer - Consumer模式。这是两个简单的类,我正在尝试重构生成器,因此它包含了线程。

[UPDATE]

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

[UPDATE]

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

5 个答案:

答案 0 :(得分:4)

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

答案 1 :(得分:3)

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

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

答案 2 :(得分:3)

首先,请注意在.NET / Base Class Library中,通常事件订阅者有义务确保其回调代码在正确的线程上执行。这使得事件生产者很容易:它可能只是触发它的事件,而不必关心其各种订阅者的任何线程关联。

以下是可能实施的完整示例。

让我们从简单的事情开始: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)。一旦发生这种情况,项目 - Action - 就会出列并调用。然后线程再次进入休眠状态(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 (未详细讨论):作为附加组件,您可以使用称为{{1}的标准.NET机制将代码发送到WorkerThread的方法打包}:

SynchronizationContext
     

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

答案 3 :(得分:1)

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

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

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

否则,您可以继续在线程内部执行轮询逻辑,而不是在线程内触发事件,只需翻转一个布尔变量,该变量每10 ms由定时器读取一次,然后触发该事件。 / p>

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

答案 4 :(得分:0)

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