组合使用Interlocked.Increment和Volatile.Write

5

我一直在查看System.Reactive的源代码(此处),这把我带到了这个地方,其中有一个相同变量上的Volatile.ReadInterlocked.CompareExchange

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

我的理解是,这个逻辑是“如果runDrainOnce为0,并且在我将其更改为1之前它也为0”。这里是否有什么微妙之处?为什么第一个检查不是多余的?

更令人费解的是,在同一个函数中存在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);
        }
    }
}

2
我猜这是关于围栏成本的问题。Volatile.Read使用半围栏(获取),而Interlocked.CompareExchange则是全围栏(获取和释放)。看起来他们试图在_runDrainOnce不为0的情况下避免使用全围栏。 - Sean
根据Eric Lippert在这里的说法:“Interlocked.Exchange被编写为期望一个volatile字段并执行正确操作”,这似乎意味着不需要先进行volatile读取(假设同样适用于CompareExchange())。我看不到源代码,但我猜测它在实现内部执行了等效的volatile读取。 - Matthew Watson
1个回答

5
完全是猜测性的可能性如下。实际上,我会说在没有明确基准测试的情况下,它可能没关系,我们可以简单地删除Volatile.Read测试,甚至只使用一个lock。如果有明确的基准测试,我希望有一个提示(或链接)的评论。
我们可以从命名(_runDrainOnce)中推断出,我们只期望这个操作成功一次,如果某件事只会成功一次,我们真的不需要让成功情况非常优化 - 所以:在成功路径中有一个多余的测试:不是一个很大的问题。相反,让我们猜测失败场景被称为“许多许多次”,因此仅在执行获取-围栏读取而不尝试写入时失败可能是有益的。 Schedule代码由所有调用 - 见OnCompletedOnErrorOnNext等 - 因此,假设意图仅仅是确保尽可能有效地启动调度 - 因此它不会超过必要的频繁地触碰Intelocked(如果有高线程竞争,它会成功一次,可能会指示几次失败)
您没有明确要求,但lock/Pulse是使用Monitor让空闲工作循环等待接收工作的常见模式;如果可能处于空闲状态,则需要唤醒它,这是当计数为零且现在为非零时(因此使用Interlocked.Increment)。

谢谢Marc。这就是那种情况,有一个或多个你不太理解的词汇,导致你无法理解整个句子的意思。 - Benjol

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接