结合 Interlocked.Increment 和 Volatile.Write

Ben*_*jol 6 c# concurrency volatile

我一直在查看 System.Reactive 的源代码(这里),它把我带到了这个地方,在同一个变量上有一个Volatile.Read后跟Interlocked.CompareExchange, :

if (Volatile.Read(ref _runDrainOnce) == 0
    && Interlocked.CompareExchange(ref _runDrainOnce, 1, 0) == 0)
{
    //do something
}
Run Code Online (Sandbox Code Playgroud)

当我阅读它时,其逻辑是“如果 runDrainOnce 为 0,并且在我将其更改为 1 之前它为零”。这里有什么微妙的吗?为什么第一次检查不是多余的?

(更令人难以置信的是,在同一个函数中有 alock和 a Monitor.Pulse。这是下注的结果吗?:))

整个功能:

private void Schedule()
{
    // Schedule the suspending drain once
    if (Volatile.Read(ref _runDrainOnce) == 0
        && Interlocked.CompareExchange(ref _runDrainOnce, 1, 0) == 0)
    {
        _drainTask.Disposable = _scheduler.ScheduleLongRunning(this, DrainLongRunning);
    }

    // Indicate more work is to be done by the drain loop
    if (Interlocked.Increment(ref _wip) == 1L)
    {
        // resume the drain loop waiting on the guard
        lock (_suspendGuard)
        {
            Monitor.Pulse(_suspendGuard);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Mar*_*ell 5

完全投机的可能性随之而来。实际上,我会说如果没有明确的基准测试,这可能无关紧要,我们可能会简单地丢失Volatile.Read测试,甚至只是使用lock. 如果有明确的标杆,我希望在那个评论暗示(或链接)。


我们可以从命名 ( _runDrainOnce)推断出我们只希望这会成功一次,如果某事只会成功一次,我们真的不需要成功案例是超级最优的——所以:在成功中进行冗余测试路径:不是一个大问题。相比之下,让我们推测失败场景被多次调用,因此执行受保护的读取(不尝试写入)使其失败可能是有益的。

Schedule代码由所有内容调用- 请参阅OnCompletedOnErrorOnNext等 - 所以大概目的是确保调度开始,尽可能高效 - 所以它不会Intelocked超过必要的(一旦成功,可能还有一些表示失败的次数(如果最初存在高线程竞争)


您没有明确询问,但lock/Pulse是使用Monitor;等待接收工作的空闲工作循环的常见模式;你需要唤醒它,如果它很可能闲置,也就是当数零,现在是非零(因此Interlocked.Increment)。