C# 在这个多线程循环实现中是否可以正确使用联锁的CompareExchange?
由于多线程上下文中的一些速率限制,我需要在N个不同的连接之间循环一些调用。我决定使用一个列表和一个“计数器”来实现这个功能,它应该在每次调用的实例之间“跳转一次” 我将用一个简单的例子来说明这个概念(使用一个名为a的类来代替连接) 我所期望的是C# 在这个多线程循环实现中是否可以正确使用联锁的CompareExchange?,c#,multithreading,round-robin,interlocked,interlocked-increment,C#,Multithreading,Round Robin,Interlocked,Interlocked Increment,由于多线程上下文中的一些速率限制,我需要在N个不同的连接之间循环一些调用。我决定使用一个列表和一个“计数器”来实现这个功能,它应该在每次调用的实例之间“跳转一次” 我将用一个简单的例子来说明这个概念(使用一个名为a的类来代替连接) 我所期望的是是互锁的。CompareExchange(ref-crt,0,itemsont)将仅由一个线程“赢得”,一旦达到可用连接数,就将计数器设置回0。然而,我不知道如何在这种情况下使用它 这里可以使用CompareExchange或Interlocked中的其他
是互锁的。CompareExchange(ref-crt,0,itemsont)
将仅由一个线程“赢得”,一旦达到可用连接数,就将计数器设置回0。然而,我不知道如何在这种情况下使用它
这里可以使用CompareExchange或Interlocked中的其他机制吗?否,该类不提供任何机制,允许您在Int32
值溢出时将其恢复为零。原因是两个线程可以同时调用var newIndex=Interlocked.Increment(ref-crt)代码>语句,在这种情况下,两者都会使计数器溢出,然后都不会成功地将值更新回零。此功能超出了联锁
类的功能。要使这样复杂的操作原子化,您需要使用其他一些同步机制,如锁
更新:xanatos证明上述说法是错误的。9年前问题的答案也证明了这一点是错误的。下面是InterlocatedIncrementRoundRobin
方法的两个实现。第一个是Alex Sorokoletov的答案的简化版本:
public static int InterlockedRoundRobinIncrement(ref int location, int modulo)
{
// Arguments validation omitted (the modulo should be a positive number)
uint current = unchecked((uint)Interlocked.Increment(ref location));
return (int)(current % modulo);
}
此实现非常有效,但其缺点是,backingint
值不能直接使用,因为它在Int32
类型的整个范围内循环(包括负值)。可用信息来自方法本身的返回值,它保证在[0..modulo]
范围内。如果要读取当前值而不增加值,则需要另一个类似的方法,执行相同的int->uint->int
转换:
public static int InterlockedRoundRobinRead(ref int location, int modulo)
{
uint current = unchecked((uint)Volatile.Read(ref location));
return (int)(current % modulo);
}
它还有一个缺点,即每4294967296递增一次,除非模
是2的幂,否则它会在达到模-1
值之前过早返回0
值。换句话说,滚动逻辑在技术上是有缺陷的。这可能是一个大问题,也可能不是,取决于应用程序
第二个实现是xanatos的修改版本:
public static int interlocatedroundrobiniincrement(参考int位置,int模)
{
//省略参数验证(模应为正数)
while(true)
{
int电流=联锁增量(参考位置);
如果(电流>=0&&电流<模)返回电流;
//溢出。请尝试将数字归零。
while(true)
{
int current2=联锁。比较交换(参考位置,0,当前);
if(current2==current)返回0;//成功
电流=电流2;
if(电流>=0&&电流<模)
{
break;//另一个线程将该数字归零。请重试增量。
}
}
}
}
效率稍低(尤其是对于较小的模
值),因为偶尔会有联锁。增量
操作会导致超出范围的值,该值会被拒绝并重复该操作。尽管它的优点是,在该方法的一些调用过程中,backingint
值仍保持在[0..modulo]
范围内,除了一些非常短的时间跨度之外。不,该类不提供任何机制,允许您在Int32
值溢出时将其恢复为零。原因是两个线程可以同时调用var newIndex=Interlocked.Increment(ref-crt)代码>语句,在这种情况下,两者都会使计数器溢出,然后都不会成功地将值更新回零。此功能超出了联锁
类的功能。要使这样复杂的操作原子化,您需要使用其他一些同步机制,如锁
更新:xanatos证明上述说法是错误的。9年前问题的答案也证明了这一点是错误的。下面是InterlocatedIncrementRoundRobin
方法的两个实现。第一个是Alex Sorokoletov的答案的简化版本:
public static int InterlockedRoundRobinIncrement(ref int location, int modulo)
{
// Arguments validation omitted (the modulo should be a positive number)
uint current = unchecked((uint)Interlocked.Increment(ref location));
return (int)(current % modulo);
}
此实现非常有效,但其缺点是,backingint
值不能直接使用,因为它在Int32
类型的整个范围内循环(包括负值)。可用信息来自方法本身的返回值,它保证在[0..modulo]
范围内。如果要读取当前值而不增加值,则需要另一个类似的方法,执行相同的int->uint->int
转换:
public static int InterlockedRoundRobinRead(ref int location, int modulo)
{
uint current = unchecked((uint)Volatile.Read(ref location));
return (int)(current % modulo);
}
它还有一个缺点,即每4294967296递增一次,除非模
是2的幂,否则它会在达到模-1
值之前过早返回0
值。换句话说,滚动逻辑在技术上是有缺陷的。这可能是一个大问题,也可能不是,取决于应用程序
第二个实现是xanatos的修改版本:
public static int interlocatedroundrobiniincrement(参考int位置,int模)
{
//省略参数验证(模应为正数)
while(true)
{
int电流=联锁。增量
public static int InterlockedRoundRobinIncrement(ref int location, int modulo)
{
// Arguments validation omitted (the modulo should be a positive number)
while (true)
{
int current = Interlocked.Increment(ref location);
if (current >= 0 && current < modulo) return current;
// Overflow. Try to zero the number.
while (true)
{
int current2 = Interlocked.CompareExchange(ref location, 0, current);
if (current2 == current) return 0; // Success
current = current2;
if (current >= 0 && current < modulo)
{
break; // Another thread zeroed the number. Retry increment.
}
}
}
}
static int crt = -1;
static readonly IReadOnlyList<A> Items = Enumerable.Range(1, 15).Select(i => new A()).ToList();
static readonly int itemsCount = Items.Count;
static readonly int maxItemCount = itemsCount * 100;
static A GetInstance()
{
int newIndex;
while (true)
{
newIndex = Interlocked.Increment(ref crt);
if (newIndex >= itemsCount)
{
while (newIndex >= itemsCount && Interlocked.CompareExchange(ref crt, -1, newIndex) != newIndex)
{
// There is an implicit memory barrier caused by the Interlockd.CompareExchange around the
// next line
// See for example https://afana.me/archive/2015/07/10/memory-barriers-in-dot-net.aspx/
// A full memory barrier is the strongest and interesting one. At least all of the following generate a full memory barrier implicitly:
// Interlocked class mehods
newIndex = crt;
}
continue;
}
break;
}
var instance = Items[newIndex % itemsCount];
//Console.WriteLine($"{DateTime.Now.Ticks}, {Guid.NewGuid()}, Got instance: {instance.ID}");
return instance;
}
maxItemCount = (int.MaxValue - MaximumNumberOfThreads) / itemsCount * itemsCount;
if (newIndex >= itemsCount)
{
int newIndex2;
while (newIndex >= itemsCount && (newIndex2 = Interlocked.CompareExchange(ref crt, 0, newIndex)) != newIndex)
{
// If the Interlocked.CompareExchange is successfull, the while will end and so we won't be here,
// if it fails, newIndex2 is the current value of crt
newIndex = newIndex2;
}
continue;
}
//this incurs some cost, but "should" ensure that the int range
// is mapped to the unit range (int.MinValue is mapped to 0 in the uint range)
static ulong toPositive(int i) => (uint)1 + long.MaxValue + (uint)i;
static A GetInstance()
{
//this seems to overflow safely without unchecked
var newCounter = Interlocked.Increment(ref crt);
//convert the counter to a list index, that is map the unsigned value
//to a signed range and get the value modulus the itemCount value
var newIndex = (int)(toPositive(newCounter) % (ulong)itemsCount);
var instance = Items[newIndex];
//Console.WriteLine($"{DateTime.Now.Ticks}, Got instance: {instance.ID}");
return instance;
}