2017-02-09 38 views
4

我们有一个方法可以在我们的应用程序中维护所有事件的全局序列索引。因为它是网站,所以预计这种方法线程安全。线程安全的实现是以下几点:Interlocked.Increment和返回递增值

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); 
} 

不过,我并不了解清楚,为什么第一个实现没有工作。任何人都可以帮助我描述在这种情况下发生的事情吗?

+1

因为'lastUsedIndex'可能已经通过对获得结果并将其返回给调用者之间的'Interlocked.Increment'的另一个调用进行更新。 –

+4

标准线程竞争错误,另一个线程也可能会在'return lastUsedIndex;'之前递增该变量;'小的可能性不是零。代码执行'[读取修改写入]读取'。您从Interlocked获得的原子性保证只会持续用于括号内的操作。你必须使用'lock [read modify write read]'来使其安全。理解,汉斯帕斯特, –

+0

RB。你们中的任何人都可以把它写成答案,所以它会被完全回答的问题吗? –

回答

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 - 如果您对增量本身更感兴趣,而不是增量值。一个典型的例子可能是便宜的分析方法 - 每个方法调用可能增加一个共享字段,然后该值稍后被读取一次,例如,平均每秒通话。

2

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

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

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

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

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

+0

请注意,即使在单CPU机器上(两个线程不会同时运行),理论上也会发生同样的情况,尽管它显然更不可能(并且取决于CPU,编译器,运行时和操作系统确实可能完全不可能,尽管不可靠)。第一个线程*可能会在'Increment'之后被优先考虑,但在随后从'someValue'中读取之前。 – Luaan