【问题标题】:Interlocked.Increment and return of incremented valueInterlocked.Increment 并返回增量值
【发布时间】:2017-06-27 11:44:43
【问题描述】:

我们有一个方法可以维护应用程序中所有事件的全局序列索引。由于它是网站,因此预计会有这种方法线程安全。线程安全实现如下:

private static long lastUsedIndex = -1;

public static long GetNextIndex()
{
    Interlocked.Increment(ref lastUsedIndex);
    return lastUsedIndex;
}

但是我们注意到,在一些负载不重的情况下,系统中会出现重复的索引。简单的测试表明,对于 100000 次迭代,大约有 1500 个重复项。

internal class Program
{
    private static void Main(string[] args)
    {
        TestInterlockedIncrement.Run();
    }
}

internal class TestInterlockedIncrement
{
    private static long lastUsedIndex = -1;

    public static long GetNextIndex()
    {
        Interlocked.Increment(ref lastUsedIndex);
        return lastUsedIndex;
    }

    public static void Run()
    {
        var indexes = Enumerable
            .Range(0, 100000)
            .AsParallel()
            .WithDegreeOfParallelism(32)
            .WithExecutionMode(ParallelExecutionMode.ForceParallelism)
            .Select(_ => GetNextIndex())
            .ToList();

        Console.WriteLine($"Total values: {indexes.Count}");
        Console.WriteLine($"Duplicate values: {indexes.GroupBy(i => i).Count(g => g.Count() > 1)}");
    }
}

这可以通过以下实现来解决:

public static long GetNextIndex()
{
    return Interlocked.Increment(ref lastUsedIndex);
}

但是,我不清楚,为什么第一次实施没有奏效。谁能帮我描述一下在这种情况下发生了什么?

【问题讨论】:

  • 因为 lastUsedIndex 在获取结果和返回给调用者之间可能已经被另一个调用 Interlocked.Increment 更新了。
  • 标准线程竞争错误,另一个线程也可能在return lastUsedIndex; 之前增加变量@ 小几率,而不是零。代码是[read modify write] read。您从 Interlocked 获得的原子性保证仅适用于括号中的操作。您必须使用 lock [read modify write read] 以确保安全。
  • RB,Hans Passant,明白了。你们中的任何人都可以把它写成答案,这样它就会得到完全回答的问题吗?
  • 您得到了很好的提示,因此您可以编写自己的答案。请务必使用它们。

标签: .net multithreading increment interlocked-increment


【解决方案1】:

根据 cmets,以下情况正在发生。

假设我们有 lastUsedIndex == 5 和 2 个并行线程。

第一个线程将执行Interlocked.Increment(ref lastUsedIndex);lastUsedIndex 将变为6。然后第二个线程将执行Interlocked.Increment(ref lastUsedIndex);lastUsedIndex 将变为7

然后两个线程将返回lastUsedIndex 的值(记住它们是并行的)。这个值现在是7

在第二个实现中,两个线程都将返回Interlocked.Increment() 函数的结果。每个线程(67)都会有所不同。换句话说,在第二个实现中,我们返回一个递增值的副本,并且该副本在其他线程中不受影响。

【讨论】:

  • 请注意,即使在单 CPU 机器上(两个线程不会同时运行),理论上同样的事情也可能发生,尽管它显然更不可能(并且取决于CPU、编译器、运行时和操作系统到底做了什么,也许完全不可能,尽管不可靠)。第一个线程可能Increment之后但在随后从someValue读取之前被抢占。
【解决方案2】:

如果它在您的原始示例中起作用,您也可以说它适用于一般情况

Interlocked.Increment(ref someValue);

// Any number of operations

return someValue;

要做到这一点,您必须消除Increment 和返回之间的所有并发性(包括并行性、重入性、抢先执行代码...)。更糟糕的是,您需要确保即使在返回和Increment 之间使用someValue,它也不会以任何方式影响返回。换句话说 - someValue 必须在两个语句之间不可能更改(不可变)。

您可以清楚地看到,如果是这种情况,您首先不需要Interlocked.Increment - 您只需使用someValue++Interlocked 和其他原子操作的全部目的是确保操作要么立即(原子地)发生,要么根本不发生。特别是,它可以保护您免受任何类型的指令重新排序(通过 CPU 优化或通过在两个逻辑 CPU 上并行运行的多个线程,或在单个 CPU 上被抢占)。但仅限于原子操作之内。对someValue 的后续读取不是同一原子操作的一部分(它本身是原子操作,但两个原子操作也不会使总和成为原子操作)。

但是您并没有尝试进行“任意数量的操作”,是吗?事实上,你是。因为有其他线程相对于您的线程异步运行 - 您的线程可能被其中一个线程抢占,或者线程可能真正在多个逻辑 CPU 上并行运行。

在真实环境中,您的示例提供了一个不断增加的字段(因此它someValue++ 好一点),但它没有为您提供唯一的 id,因为所有您'正在阅读是someValue 在某个不确定的时刻。如果两个线程尝试同时进行增量,则两者都会成功(Interlocked.Increment 原子的),但它们也会从 someValue 读取相同的值。

这并不意味着您总是想使用Interlocked.Increment 的返回值——如果您对增量本身而不是增量值更感兴趣的话。一个典型的例子可能是一个廉价的分析方法——每个方法调用可能会增加一个共享字段,然后偶尔读取一次值,例如平均每秒调用次数。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-14
    • 2019-09-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多