我有一个多线程应用程序,可以为多个硬件工具生成线程。每个线程基本上都是一个无限循环(在应用程序的生命周期内),它会轮询硬件以获取新数据,并在每次收集新内容时激活一个事件(传递数据)。有一个侦听器类可以合并所有这些乐器,执行一些计算,并通过此计算触发一个新事件。
但是,我想知道,由于只有一个监听器,最好从这些工具中公开IEnumerable<>
方法,并使用yield return
来返回数据,而不是射击事件。
我想知道是否有人知道这两种方法的差异。特别是,我正在寻找最佳的可靠性,最佳的暂停/取消操作能力,最好的线程用途,一般安全性等。
另外,使用第二种方法是否仍然可以在单独的线程上运行IEnumerable
循环?其中许多工具都受CPU限制,所以确保每个工具都在不同的线程上是至关重要的。
答案 0 :(得分:7)
这听起来像是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's blog上有一些很好的信息。 (虽然他的帖子几乎完全提到JavaScript的Reactive Extensions,但它几乎都适用于.NET的Reactive Extensions。)
答案 1 :(得分:4)
这是一个近距离的调用,但我认为在这种情况下我会坚持使用事件模型,主要决策者认为未来的维护程序员不太可能理解产量概念。此外,yield表示处理每个硬件请求的代码与生成处理请求的代码在同一个线程中。这很糟糕,因为这可能意味着您的硬件必须等待消费者代码。
说到消费者,另一种选择是生产者/消费者队列。你的乐器都可以进入同一个队列,然后你的单个听众可以从中弹出任何东西。
答案 2 :(得分:2)
有一个非常根本的区别,推动与拉动。拉模型(yield)是从仪器界面视图实现的更难的模型。因为在客户端代码准备就绪之前,您必须存储数据。当您推送时,客户可能会或可能不会在其认为必要时进行存储。
但是,多线程场景中的大多数实际实现都需要处理呈现数据所需的不可避免的线程上下文切换的开销。而这通常是使用线程安全的有界队列来完成的。
答案 3 :(得分:1)
Stephen Toub blogs关于使用IEnumerable
关键字将yield
实现为无限循环的阻塞队列。您的工作线程可以在出现新数据点时将它们排队,计算线程可以使用带有阻塞语义的foreach
循环将它们出列。
答案 4 :(得分:0)
我认为事件与yield
方法之间在性能方面存在很大差异。 Yield
是惰性评估的,因此它有机会通知生产线程停止。如果您的代码经过精心记录,那么维护也应该是一种清洗。
我的偏好是第三种选择,使用回调方法而不是事件(即使两者都涉及委托)。您的生成器每次有数据时都会调用回调。回调可以返回值,因此您的消费者可以发出信号,指示生产者每次检入数据时都会停止或继续。
如果您拥有大量数据,此方法可以为您提供优化性能的位置。在回调中,您锁定中性对象并将传入数据附加到集合中。运行时内部在锁对象上使用就绪队列,因此这可以作为您的排队点。
这使您可以选择一个集合,例如具有预定义容量的List<T>
,即O(1)用于追加。您还可以对消费者进行双重缓冲,将回调附加到“左”缓冲区,同时从“正确”缓冲区进行合并,依此类推。这最大限度地减少了生产者阻塞和相关的错过数据的数量,这对于突发数据很方便。当您改变螺纹数量时,您还可以轻松测量高水位标记和加工速率。