合并 Interlocked.Increment 和 Volatile.Write

Combining Interlocked.Increment and Volatile.Write

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

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

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

(更不可思议的是,同一个函数中有一个lock和一个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);
        }
    }
}

接下来是完全推测的可能性。实际上,我会说 没有明确的基准测试 ,它可能 无关紧要 ,我们可以简单地失去 Volatile.Read测试,甚至只使用 lock。如果 明确的基准测试,我希望对此有评论提示(或链接)。


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

Schedule 代码由 everything 调用 - 参见 OnCompletedOnErrorOnNext 等 - 据推测目的只是确保调度 尽可能高效地开始 - 所以它不会触及 Intelocked 超过必要的(一旦成功,可能有一些时间表明 失败,如果最初存在高线程竞争)


您没有明确询问,但是 lock/Pulse 是使用 Monitor 让空闲工作循环等待接收工作的常见模式;你需要唤醒它 if 它可能处于空闲状态,这是当计数 零并且现在为 non-zero (因此 Interlocked.Increment).