首页 > 解决方案 > 在 F# 中设置标志的线程问题

问题描述

TLDR:我在回调中设置了一个标志,它的值在主循环中没有改变。

不幸的是,由于原始代码很大,我必须制作代码的简化版本,但简而言之,这是我面临的问题:

try
    try
        exchangeBus.OnTradeEvent.AddHandler(tradeEventHandler)
        exchangeBus.OnOrderEvent.AddHandler(orderEventHandler)

        let mutable keepGoing = true
        while keepGoing do

            let nodeRunner = buildRunner()
            let waitHandle = new EventWaitHandle(false, EventResetMode.ManualReset)

            // handle the completion event
            nodeRunner.OnCompletionEvent.Add(fun (completionType: CycleCompletionType) ->
                match completionType with
                | CodeException ex -> reportError side ex true
                                      keepGoing <- false
                                      abortHandle.Set() |> ignore

                | Abort reason     -> // do some stuff
                                      keepGoing <- false
                                      abortHandle.Set() |> ignore

                | AutoAbort reason -> // do some stuff
                                      waitHandle.Set() |> ignore

                | NormalClose      -> // do some stuff
                                      waitHandle.Set() |> ignore

            )

            // start the runner
            nodeRunner.Startup()

            // wait for completion, or abort
            WaitHandle.WaitAny([| waitHandle; abortHandle |]) |> ignore


    with ex ->
        status <- ExceptionState
        getRunner(side)          ||> (fun r -> r.Signal(CycleCompletionType.CodeException(ex)))
        getRunner(side.Opposite) ||> (fun r -> r.Signal(CycleCompletionType.Abort($"exception on the {side.Opposite} side, aborting")))

finally
    exchangeBus.OnTradeEvent.RemoveHandler(tradeEventHandler)
    exchangeBus.OnOrderEvent.RemoveHandler(orderEventHandler)        

所以,简而言之,它是如何工作的:

您有两个包含此代码的对象并行运行。他们实现了一个循环,在其中创建了一个对象,该对象包含一个图并基于该图中的节点执行一系列代码。

在执行图表的代码之外,回调tradeEventHandlerorderEventHandler中可能会发生异常,但这是单独处理的。

一旦执行图的核心(nodeRunner)完成,它就会发送一个事件来描述它是如何完成的。有 2 种情况可以(正常和 AutoAbort)和 2 种情况需要停止进程(CodeException 和 Abort)。

有两个 EventWaitHandle 对象,一个是该对象唯一的,用于控制循环(waitHandle),另一个是两个对象共有的(abortHandle),用于告诉它们停止操作。

当其中一个对象进入中止状态时,就会发生此问题。

OnCompletionEvent回调被执行,然后我们发生了两件事:

keepGoing <- false
abortHandle.Set() |> ignore

正如预期的那样,在WaitHandle.WaitAny调用之后设置了 abortHandle 等待并继续执行。

keepGoing标志(该循环的本地)被设置为 false 并且......循环继续进行,就好像它仍然是真的一样。当我在循环开始时打印它的状态时,它仍然显示为真而不是假。

所以一定有一个线程相关的问题。我不知道OnCompletionEvent调用在哪里将keepGoing标志设置为 false,但它肯定与循环中使用的值不同。

我该如何解决这个问题?或者,我错过了什么明显的东西吗?

标签: multithreadingf#

解决方案


您可能需要使用VolatileInterlocked使修改keepGoing在线程间立即可见。

由于keepGoing不是您无法应用的字段,因此您可以通过引用[<VolatileField>]将其传递给Volatile方法,如下所示:

let mutable keepGoing = true 
while (Volatile.Read(&keepGoing)) do
    // Do your processing
    // And eventually disable keepGoing
    Volatile.Write(&keepGoing, false)

或者将其定义为参考单元格并像这样使用它:

let keepGoing = ref true
while (Volatile.Read(keepGoing)) do
    // Do your processing
    // And eventually disable keepGoing
    Volatile.Write(keepGoing, false)

这可能是必要的,因为正如以下注释中Volatile所解释的:

在多处理器系统上,易失性写入操作可确保写入内存位置的值立即对所有处理器可见。易失性读取操作获取任何处理器写入内存位置的最新值。这些操作可能需要刷新处理器缓存,这会影响性能。

在单处理器系统上,易失性读取和写入可确保将值读取或写入内存而不是缓存(例如,在处理器寄存器中)。因此,您可以使用这些操作来同步对可由另一个线程或硬件更新的字段的访问。

... 一些语言,如 Visual Basic,不承认易失性内存操作的概念。Volatile 类以此类语言提供该功能。

因此,当您的一个线程修改 的值时keepGoing,其他线程可能没有及时看到更改,因为它们已经将该值提取到处理器缓存或寄存器中。

也可以看看:


推荐阅读