问题 事件与收益率


我有一个多线程应用程序,它为几个硬件工具生成线程。每个线程基本上都是一个无限循环(在应用程序的生命周期内),它会轮询硬件以获取新数据,并在每次收集新内容时激活一个事件(传递数据)。有一个侦听器类可以合并所有这些乐器,执行一些计算,并通过此计算触发新事件。

但是,我想知道是否,因为有一个监听器,最好是暴露一个 IEnumerable<> 关闭这些仪器的方法,并使用一个 yield return 返回数据,而不是触发事件。

我想知道是否有人知道这两种方法的差异。特别是,我正在寻找最好的可靠性,最佳的暂停/取消操作能力,最好的线程用途,一般安全性等。

此外,使用第二种方法仍然可以运行 IEnumerable 循环在一个单独的线程?其中许多仪器都受CPU限制,因此确保每个仪器都在不同的线程上是至关重要的。


4595
2017-08-04 02:23


起源



答案:


这听起来像是Reactive Extensions的一个非常好的用例。它有一点学习曲线,但简而言之,IObservable是IEnumerable的双重性。在IEnumerable要求您从中拉出的地方,IObservable将其值推送到观察者。几乎任何时候你需要在你的枚举器中阻塞,这是一个好的迹象,你应该扭转模式并使用推模型。事件是一种可行的方法,但IObservable具有更大的灵活性,因为它是可组合的和线程感知的。

instrument.DataEvents
          .Where(x => x.SomeProperty == something)
          .BufferWithTime( TimeSpan.FromSeconds(1) )
          .Subscribe( x => DoSomethingWith(x) );

在上面的示例中,只要主题(工具)生成具有匹配SomeProperty的DataEvent并且将事件缓冲到1秒持续时间的批次中,就会调用DoSomethingWith(x)。

你可以做更多的事情,例如合并其他主题产生的事件或将通知指向UI线程等。不幸的是,文档目前相当薄弱,但有一些很好的信息 Matthew Podwysocki的博客。 (虽然他的帖子几乎完全提到了JavaScript的Reactive Extensions,但它几乎都适用于.NET的Reactive Extensions。)


6
2017-08-04 04:57



我在一个新项目中使用了Rx,这太棒了。在这种情况下的问题是,它将需要对遗留代码进行重大改进,因为它会改变甚至接近问题的整个方式。对于下一个主要版本,我已经在研究这个问题,但对于小型(重新)设计而言,这似乎是一项艰巨的任务。 - drharris


答案:


这听起来像是Reactive Extensions的一个非常好的用例。它有一点学习曲线,但简而言之,IObservable是IEnumerable的双重性。在IEnumerable要求您从中拉出的地方,IObservable将其值推送到观察者。几乎任何时候你需要在你的枚举器中阻塞,这是一个好的迹象,你应该扭转模式并使用推模型。事件是一种可行的方法,但IObservable具有更大的灵活性,因为它是可组合的和线程感知的。

instrument.DataEvents
          .Where(x => x.SomeProperty == something)
          .BufferWithTime( TimeSpan.FromSeconds(1) )
          .Subscribe( x => DoSomethingWith(x) );

在上面的示例中,只要主题(工具)生成具有匹配SomeProperty的DataEvent并且将事件缓冲到1秒持续时间的批次中,就会调用DoSomethingWith(x)。

你可以做更多的事情,例如合并其他主题产生的事件或将通知指向UI线程等。不幸的是,文档目前相当薄弱,但有一些很好的信息 Matthew Podwysocki的博客。 (虽然他的帖子几乎完全提到了JavaScript的Reactive Extensions,但它几乎都适用于.NET的Reactive Extensions。)


6
2017-08-04 04:57



我在一个新项目中使用了Rx,这太棒了。在这种情况下的问题是,它将需要对遗留代码进行重大改进,因为它会改变甚至接近问题的整个方式。对于下一个主要版本,我已经在研究这个问题,但对于小型(重新)设计而言,这似乎是一项艰巨的任务。 - drharris


这是一个非常接近的电话,但我认为在这种情况下我会坚持使用事件模型,主要决策者认为未来的维护程序员不太可能理解产量概念。此外,yield表示处理每个硬件请求的代码与生成处理请求的代码在同一个线程中。这很糟糕,因为它可能意味着您的硬件必须等待消费者代码。

说到消费者,另一种选择是生产者/消费者队列。你的乐器都可以进入同一个队列,然后你的单个听众可以从中弹出任何东西。


4
2017-08-04 02:27



我不太关心可维护性。由于这是应用程序的关键任务部分,因此非文档编程人员可以找到自己的方式。感谢您回答线程问题。并且,关于使用队列的好指针。我在看新的 ConcurrentQueue<T> 上课,可能会走这条路。 - drharris


有一个非常根本的区别,推动与拉动。拉模型(yield)是从仪器界面视图实现的更难的模型。因为您必须存储数据,直到客户端代码准备好提取。当您推送时,客户端可能会或可能不会在其认为必要时进行存储。

但是,多线程场景中的大多数实际实现需要处理呈现数据所需的不可避免的线程上下文切换的开销。而这通常是通过使用线程安全的有界队列来完成的。


2
2017-08-04 04:04





斯蒂芬图布 博客 关于实现的阻塞队列 IEnumerable 作为使用的无限循环 yield 关键词。您的工作线程可以在出现新数据点时将它们排队,计算线程可以使用a将它们出列 foreach 循环与阻塞语义。


1
2017-08-04 02:38





我不认为事件和事件之间在性能方面存在很大差异 yield 做法。 Yield 是懒惰的评估,所以它留下了一个机会,告知生产线程停止。如果您的代码经过精心记录,那么维护也应该是一种清洗。

我的偏好是第三种选择,使用回调方法而不是事件(即使两者都涉及委托)。您的生成器每次有数据时都会调用回调。回调可以返回值,因此您的消费者可以通过每次检入数据来指示生产者停止或继续。

如果您拥有大量数据,此方法可以为您提供优化性能的位置。在回调中,您锁定中性对象并将传入数据附加到集合中。运行时内部在锁对象上使用就绪队列,因此这可以作为您的排队点。

这允许您选择一个集合,例如 List<T> 预定义容量,即O(1)用于追加。您还可以对消费者进行双重缓冲,将回调附加到“左”缓冲区,同时从“正确”缓冲区进行合并,依此类推。这最大限度地减少了生产者阻塞和相关的遗漏数据,这对于突发数据来说非常方便。您还可以在改变螺纹数量时轻松测量高水位标记和加工速率。


0
2017-08-04 18:05