[英]A MailboxProcessor that operates with a LIFO logic
我正在学习F#代理( MailboxProcessor
)。
我正在处理一个非常常规的问题。
dataSource
),它是流数据的源。 数据必须由代理数组( dataProcessor
)处理。 我们可以将dataProcessor
视为某种跟踪设备。 dataProcessor
可能处理其输入的速度快。 我正在探索解决此问题的方法。
第一个想法是在dataSource
实现堆栈 (LIFO)。 当dataProcessor
可用于接收和处理数据时, dataSource
将发送可用的最新观察值。 该解决方案可能有效,但由于可能需要阻止并重新激活dataProcessor
因此可能会变dataProcessor
复杂。 并将其状态传达给dataSource
,从而导致双向通讯问题。 这个问题可能归结为消费者-生产者问题中的blocking queue
,但是我不确定。
第二个想法是让dataProcessor
负责消息排序。 在这种体系结构中, dataSource
只会将更新发布到dataProcessor
的队列中。 dataProcessor
将使用“ Scan
来获取队列中可用的最新数据。 这可能是要走的路。 但是,我不确定在当前的MailboxProcessor
设计中是否可以清除消息队列,删除较旧的过时消息。 此外, 在这里 ,写道:
不幸的是,当前版本的F#中的TryScan函数以两种方式被破坏。 首先,重点是指定一个超时,但是实现实际上并没有兑现它。 具体来说,无关的消息会重置计时器。 其次,与其他“扫描”功能一样,将在锁定下检查消息队列,该锁定可防止在扫描期间(可能是任意长时间)发布任何其他线程。 因此,TryScan函数本身倾向于锁定并发系统,甚至可能引入死锁,因为调用者的代码是在锁内求值的(例如,当锁下的代码阻塞等待时,从函数参数到Scan或TryScan的发布会死锁代理。获取它已经在下面的锁)。
使最新的观测结果反弹可能是一个问题。 这篇文章的作者@Jon Harrop建议
我设法围绕它进行了架构,并且最终的架构实际上更好。 本质上,我渴望使用我自己的本地队列
Receive
所有消息并进行过滤。
这个想法肯定值得探索,但是在开始使用代码之前,我将欢迎一些有关如何构建解决方案的意见。
谢谢。
tl; dr我会尝试:从FSharp.Actor或Zach Bray的博客文章中获取邮箱实现,将ConcurrentQueue替换为ConcurrentStack(并添加一些有限容量逻辑),然后使用此已更改的代理作为调度程序,将消息从数据源传递到实现为普通MBP或Actor的dataProcessor。
tl; dr2如果worker是一种稀缺且缓慢的资源,并且我们需要处理一条消息,该消息是worker准备就绪时的最新消息,那么所有这些消息都归结为具有堆栈而不是队列(具有一定界限)的代理容量逻辑)以及工作人员的BlockingQueue。 分派器使准备就绪的工作人员出队,然后从堆栈中弹出一条消息,然后将此消息发送给该工作人员。 作业完成后,工作者准备就绪时let! msg = inbox.Receive()
自己排入队列(例如,在let! msg = inbox.Receive()
)。 然后,分派器使用者线程将阻塞,直到任何工作程序就绪为止,而生产者线程将使有界堆栈保持更新。 (有界堆栈可以用数组+偏移+锁中的大小来完成,下面太复杂了)
细节
MailBoxProcessor设计为只有一个使用者。 这甚至在MBP的源代码注释这里 (搜索单词“龙吟” :))
如果将数据发布到MBP,则只有一个线程可以从内部队列或堆栈中获取数据。 在您的特定用例中,我将直接使用ConcurrentStack或更好地包装到BlockingCollection中 :
BlockingCollection
具有BoundedCapacity
属性,该属性使您可以限制集合的大小。 它会引发Add
,但是您可以捕获它或使用TryAdd
。 如果A是主堆栈,B是备用堆栈,则将TryAdd
Add
到A,如果是false则Add
到B,然后将它们与Interlocked.Exchange交换,然后在A中处理所需的消息,将其清除,制作一个新的备用-或使用三个堆栈如果处理A的时间可能长于B的时间,则处理B可能再次变满; 这样,您不会阻塞也不会丢失任何消息,但是可以丢弃不需要的消息是一种受控方式。 BlockingCollection具有AddToAny / TakeFromAny之类的方法,这些方法可用于BlockingCollections的数组。 这可能会有所帮助,例如:
像这样:
(data stream produces 'T)
|
[dispatcher's BCSC]
|
(a dispatcher thread consumes 'T and pushes to processors, manages capacity of BCCS and LRU queue)
| |
[processor1's BCCS/Actor/MBP] ... [processorN's BCCS/Actor/MBP]
| |
(process) (process)
代替ConcurrentStack,您可能想了解堆数据结构 。 如果您需要消息的某些属性(例如时间戳)而不是消息到达堆栈的顺序(例如,如果传输和到达顺序<>创建顺序可能存在延迟),则需要获取最新消息,则可以获取最新消息通过使用堆消息。
如果您仍然需要Agents语义/ API,则除了阅读Dave的链接之外,还可以阅读其他资源,并以某种方式对多个并发使用者采用实现:
Zach Bray的一篇有趣的文章 ,介绍了Actor的高效实现。 在那里,您确实需要替换(在注释中// Might want to schedule this call on another thread.
)该行由行async { execute true } |> Async.Start
或类似的行execute true
,因为否则会产生线程线程-不利于单个快速生产者。 但是,对于如上所述的调度员,这正是需要的。
FSharp.Actor (又名Fakka
) 开发分支和FSharp MPB源代码(上面的第一个链接)对于实现细节可能非常有用。 FSharp.Actors库已经冻结了几个月,但在dev分支中有一些活动。
我有一个类似的用例,在过去的两天中,我研究了可以在F#Agents / Actor上找到的所有内容。 这个答案对我来说是一种尝试尝试这些想法的TODO,其中一半是在撰写过程中诞生的。
最简单的解决方案是在收件箱到达时贪婪地吃掉收件箱中的所有邮件,并丢弃除最新邮件以外的所有邮件。 使用TryReceive
轻松完成:
let rec readLatestLoop oldMsg =
async { let! newMsg = inbox.TryReceive 0
match newMsg with
| None -> oldMsg
| Some newMsg -> return! readLatestLoop newMsg }
let readLatest() =
async { let! msg = inbox.Receive()
return! readLatestLoop msg }
遇到相同的问题时,我设计了一个更复杂,更有效的解决方案,称为可取消流,并在此处的F#Journal文章中进行了介绍 。 想法是开始处理消息,如果消息被取代,则取消该处理。 如果正在进行大量处理,这将显着提高并发性。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.