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