我们有一个方法可以维护应用程序中所有事件的全局序列索引。因为它是网站,所以预计这种方法线程安全。线程安全实现如下:
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);
}
但是,我不清楚,为什么第一次实施不起作用。有人可以帮我描述那种情况下发生的事情吗?
答案 0 :(得分:5)
如果它在原始示例中有效,您也可以说它适用于
的一般情况Interlocked.Increment(ref someValue);
// Any number of operations
return someValue;
要实现这一点,您必须消除Increment
和返回之间的所有并发(包括并行性,重新执行,抢先代码执行......)。更糟糕的是,您需要确保即使在返回和someValue
之间使用Increment
,也不会以任何方式影响返回。换句话说 - someValue
必须在两个陈述之间不可能改变(不可变)。
你可以清楚地看到,如果是这种情况,你首先不需要Interlocked.Increment
- 你只需someValue++
。 Interlocked
和其他原子操作的全部目的是确保操作一次(原子地)或根本不发生。特别是,它可以防止任何类型的指令重新排序(通过CPU优化或通过在两个逻辑CPU上并行运行的多个线程,或者在单个CPU上预先绑定)。但只能在原子操作中。随后对someValue
的读取是不是同一原子操作的一部分(它本身就是原子操作,但是两个原子操作也不会使和原子化。)
但你不是想做“任意数量的操作”,不是吗?实际上,你是。因为有其他线程相对于您的线程异步运行 - 您的线程可能被其中一个线程抢占,或者线程可能真正在多个逻辑CPU上并行运行。
在真实环境中,您的示例提供了一个不断增加的字段(因此 比someValue++
好一点),但它没有为您提供唯一的ID,因为所有你正在阅读的是someValue
在某个不确定的时刻。如果两个线程同时尝试执行增量,则两个线程都会成功(Interlocked.Increment
原子),但它们也将从someValue
读取相同的值。
这并不意味着您总是希望使用Interlocked.Increment
的返回值 - 如果您对增量本身更感兴趣,而不是增加的值。典型的示例可能是廉价的分析方法 - 每个方法调用可以增加共享字段,然后偶尔读取该值一次,例如,平均每秒通话数。
答案 1 :(得分:3)
根据评论,以下情况正在发生。
假设我们有lastUsedIndex == 5
和2个并行线程。
第一个线程将执行Interlocked.Increment(ref lastUsedIndex);
,lastUsedIndex
将变为6
。然后第二个线程将执行Interlocked.Increment(ref lastUsedIndex);
,lastUsedIndex
将变为7
。
然后两个线程都将返回lastUsedIndex
的值(请记住它们是并行的)。该值现在为7
。
在第二个实现中,两个线程都将返回Interlocked.Increment()
函数的结果。每个帖子(6
和7
)都有所不同。换句话说,在第二个实现中,我们返回一个递增值的副本,该副本在其他线程中不受影响。