简体   繁体   English

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

[英]C# Firing events within the thread they are added

Consider two classes; 考虑两个班级; Producer and Consumer (the same as classical pattern, each with their own threads). ProducerConsumer (与经典模式相同,每个都有自己的线程)。 Is it possible for Producer to have an Event which Consumer can register to and when the producer triggers the event, the consumer's event handler is run in its own thread? 是否有可能对Producer有一个Event ,其Consumer可以注册和当制片触发事件,消费者的事件处理程序在它自己的线程中运行? Here are my assumptions: 以下是我的假设:

  • Consumer does not know if the Producer 's event is triggered within his own thread or another. Consumer不知道Producer的事件是否在他自己的线程或其他线程中被触发。

  • Neither Producer nor Consumer are descendants of Control so they don't have BeginInvoke method inherited. ProducerConsumer都不是Control后代,因此它们没有继承BeginInvoke方法。

PS. PS。 I'm not trying to implement Producer - Consumer pattern. 我不打算实现Producer - Consumer模式。 These are two simple classes which I'm trying to refactor the producer so it incorporates threads. 这是两个简单的类,我正在尝试重构生成器,因此它包含线程。

[UPDATE] [UPDATE]

To further expand my problem, I'm trying to wrap a hardware driver to be worked with in the simplest way possible. 为了进一步扩展我的问题,我正在尝试以最简单的方式包装硬件驱动程序。 For instance my wrapper will have a StateChanged event which the main application will register to so it will be notified when hardware is disconnected. 例如,我的包装器将有一个StateChanged事件,主应用程序将注册该事件,以便在硬件断开连接时通知它。 As the actual driver has no means other than polling to check its presence , I will need to start a thread to check it periodically. 由于实际的驱动程序除了轮询以外无法检查其存在,我将需要启动一个线程来定期检查它。 Once it is not available anymore I will trigger the event which needs to be executed in the same thread as it was added. 一旦它不再可用,我将触发需要在添加的同一线程中执行的事件。 I know this is a classical Producer-Consumer pattern but since I'm trying to simplify using my driver-wrapper, I don't want the user code to implement consumer. 我知道这是一个经典的Producer-Consumer模式,但由于我正在尝试简化使用我的驱动程序包装器,我不希望用户代码实现使用者。

[UPDATE] [UPDATE]

Due to some comments suggesting that there's no solution to this problem, I would like to add few lines which might change their minds. 由于一些评论表明没有解决这个问题的方法,我想添加一些可能会改变他们想法的行。 Considering the BeginInvoke can do what I want, so it shouldn't be impossible (at least in theory). 考虑到BeginInvoke可以做我想做的事情,所以这不应该是不可能的(至少在理论上)。 Implementing my own BeginInvoke and calling it within the Producer is one way to look at it. 实现我自己的BeginInvoke并在Producer调用它是一种查看它的方法。 It's just that I don't know how BeginInvoke does it! 只是我不知道BeginInvoke是如何做到的!

You want to do inter thread communication. 你想做线程间的沟通。 Yes it is possible. 对的,这是可能的。 Use System.Windows.Threading.Dispatcher http://msdn.microsoft.com/en-us/library/system.windows.threading.dispatcher.aspx 使用System.Windows.Threading.Dispatcher http://msdn.microsoft.com/en-us/library/system.windows.threading.dispatcher.aspx

The Dispatcher maintains a prioritized queue of work items for a specific thread. Dispatcher维护特定线程的优先级工作项队列。 When a Dispatcher is created on a thread, it becomes the only Dispatcher that can be associated with the thread, even if the Dispatcher is shut down. 在线程上创建Dispatcher时,即使Dispatcher关闭,它也会成为唯一可与该线程关联的Dispatcher。 If you attempt to get the CurrentDispatcher for the current thread and a Dispatcher is not associated with the thread, a Dispatcher will be created. 如果您尝试获取当前线程的CurrentDispatcher并且Dispatcher未与该线程关联,则将创建Dispatcher。 A Dispatcher is also created when you create a DispatcherObject. 创建DispatcherObject时也会创建Dispatcher。 If you create a Dispatcher on a background thread, be sure to shut down the dispatcher before exiting the thread. 如果在后台线程上创建Dispatcher,请确保在退出线程之前关闭调度程序。

Yes there is a way to do this. 是的,有办法做到这一点。 It relies on using the SynchronizationContext class ( docs ). 它依赖于使用SynchronizationContext类( docs )。 The sync context abstracts the operations of sending messages from one thread to another via the methods Send (synchronous for the calling thread) and Post (async for the calling thread). 同步上下文通过方法Send (调用线程同步)和Post (调用线程的异步)抽象将消息从一个线程发送到另一个线程的操作。

Let's take a slightly simpler situation where you only want the capture one sync context, the context of the "creator" thread. 让我们稍微简单一点,你只需要捕获一个同步上下文,即“创建者”线程的上下文。 You would do something like this: 你会做这样的事情:

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

Now, the creating thread's sync context will be used when events are invoked. 现在,将在调用事件时使用创建线程的同步上下文。 If the creating thread was a UI thread it will have a sync context supplied by the framework. 如果创建线程是UI线程,则它将具有框架提供的同步上下文。 If there is no sync context, then the default implementation is used, which invokes on the thread pool. 如果没有同步上下文,则使用默认实现,该实现在线程池上调用。 SynchronizationContext is a class that you can subclass if you want to provide a custom way to send a message from the producer to the consumer thread. 如果要提供自定义方式将消息从生产者发送到使用者线程, SynchronizationContext是一个可以子类的类。 Just override Post and Send to send said message. 只需覆盖PostSend即可发送消息。

If you wanted every event subscriber to get called back on their own thread, you would have to capture the sync context in the add method. 如果您希望每个事件订阅者都可以在自己的线程上回调,则必须在add方法中捕获同步上下文。 You then hold on to pairs of sync contexts and delegates. 然后,您可以继续使用成对的同步上下文和委托。 Then when raising the event, you would loop through the sync context / delegate pairs and Post each one in turn. 然后,当提升事件时,您将遍历同步上下文/委托对并依次Post每个对。

There are several other ways you could improve this. 还有其他几种方法可以改善这一点。 For example, you may want to suspend polling the hardware if there no subscribers to the event. 例如,如果没有该事件的订阅者,您可能希望暂停轮询硬件。 Or you might want to back off your polling frequency if the hardware does not respond. 或者,如果硬件没有响应,您可能希望取消轮询频率。

First, please note that in .NET / the Base Class Library, it is usually the event subscriber's obligation to ensure that its callback code is executing on the correct thread. 首先,请注意在.NET /基类库中,事件订阅者通常有义务确保其回调代码在正确的线程上执行。 That makes it easy for the event producer: it may just trigger its event without having to care about any thread affinities of its various subscribers. 这使得事件生成者很容易:它可能只是触发它的事件而不必关心其各种订阅者的任何线程关联。

Here's a complete example step-by-step of a possible implementation. 这是一个可能实现的逐步完整示例。

Let's start with something simple: The Producer class and its event, Event . 让我们从简单的事情开始: Producer类及其事件Event My example won't include how and when this event gets triggered: 我的示例不包括触发此事件的方式和时间:

class Producer
{
    public event EventHandler Event;  // raised e.g. with `Event(this, EventArgs.Empty);`
}

Next, we want to be able to subscribe our Consumer instances to this event and be called back on a specific thread (I'll call this kind of thread a "worker thread"): 接下来,我们希望能够将Consumer实例订阅到此事件并在特定线程上回调(我将这种线程称为“工作线程”):

class Consumer
{
    public void SubscribeToEventOf(Producer producer, WorkerThread targetWorkerThread) {…}
}

How do we implement this? 我们如何实现这个?

First, we need the means to "send" code to a specific worker thread. 首先,我们需要将代码“发送”到特定工作线程的方法。 Since there is no way to force a thread to execute a particular method whenever you want it to, you must arrange for a worker thread to explicitly wait for work items. 由于无法在任何时候强制线程执行特定方法,因此必须安排工作线程明确等待工作项。 One way to do this is via a work item queue. 一种方法是通过工作项队列。 Here's a possible implementation for WorkerThread : 这是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();
        }
    }
}

This class basically starts a thread, and puts it in an infinite loop that falls asleep ( WaitOne ) until an item arrives in its queue ( workItems ). 这个类基本上启动一个线程,并将它置于一个无休止的循环中( WaitOne ),直到一个项到达其队列( workItems )。 Once that happens, the item — an Action — is dequeued and invoked. 一旦发生这种情况,项目 - 一个Action - 就会出列并调用。 Then the thread goes to sleep again ( WaitOne )) until another item is available in the queue. 然后线程再次进入休眠状态( WaitOne )),直到队列中有另一个项目可用。

Action s are put in the queue via the QueueWorkItem method. Action通过QueueWorkItem方法放入队列。 So essentially we can now send code to be executed to a specific WorkerThread instance by calling that method. 所以基本上我们现在可以通过调用该方法将要执行的代码发送到特定的WorkerThread实例。 We're now ready to implement Customer.SubscribeToEventOf : 我们现在准备实现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…`. 
    }
}

Voilà! 瞧!


PS (not discussed in detail): As an add-on, you could package the method of sending code to WorkerThread using a standard .NET mechanism called a SynchronizationContext : 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 } 

And at the beginning of WorkerThread.ProcessWorkItems , you'd set the synchronization context for that particular thread as follows: WorkerThread.ProcessWorkItems的开头,您将为该特定线程设置同步上下文,如下所示:

 SynchronizationContext.SetSynchronizationContext( new WorkerThreadSynchronizationContext(this)); 

I posted earlier that I've been there, and that there is no nice solution. 我之前发布过我已经去过那里,并没有很好的解决方案。

However, I just stumbled upon something I have done in another context before: you could instantiate a timer (that is, Windows.Forms.Timer ) when you create your wrapper object. 但是,我之前偶然发现了我在另一个上下文中所做的事情:您可以在创建包装器对象时实例化一个计时器(即Windows.Forms.Timer )。 This timer will post all Tick events to the ui thread. 此计时器将所有Tick事件发布到ui线程。

Now if you're device polling logic is non-blocking and fast, you could implement it directly inside the timer Tick event, and raise your custom event there. 现在,如果您的设备轮询逻辑是非阻塞且快速的,您可以直接在计时器Tick事件中实现它,并在那里引发自定义事件。

Otherwise, you could continue to do the polling logic inside a thread, and instead of firing the event inside the thread, you just flip some boolean variable which gets read by the timer every 10 ms, who then fires the event. 否则,您可以继续在线程内部执行轮询逻辑,而不是在线程内触发事件,只需翻转一个布尔变量,该变量每10 ms由定时器读取一次,然后触发该事件。

Note that this solution still requires that the object is created from the GUI thread, but at least the user of the object will not have to worry about Invoke . 请注意,此解决方案仍然要求从GUI线程创建对象,但至少对象的用户不必担心Invoke

It is possible. 有可能的。 One typical approach is to use the BlockingCollection class. 一种典型的方法是使用BlockingCollection类。 This data structure works like a normal queue except that the dequeue operation blocks the calling thread if the queue is empty. 此数据结构的工作方式与普通队列类似,只是如果队列为空,则出队操作会阻塞调用线程。 The produce will queue items by calling Add and the consumer will dequeue them by calling Take . 产品将通过调用Add对项目进行排队,并且消费者将通过调用Take来使它们出列。 The consumer typically runs it's own dedicated thread spinning an infinite loop waiting for items to appear in the queue. 消费者通常运行它自己的专用线程,旋转无限循环,等待项目出现在队列中。 This is, more or less, how the message loop on the UI thread operates and is the basis for getting the Invoke and BeginInvoke operations to accomplish the marshaling behavior. 这或多或少是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);
  }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM