dca*_*tro 6 .net c# multithreading locking compare-and-swap
我一直在阅读无锁技术,比如比较和交换,并利用Interlocked和SpinWait类来实现线程同步而不需要锁定.
我已经运行了一些我自己的测试,我只是有很多线程尝试将字符附加到字符串.我尝试使用常规locks和比较和交换.令人惊讶的是(至少对我而言),锁比使用CAS显示出更好的结果.
这是我的代码的CAS版本(基于此).它遵循copy-> modify-> swap模式:
private string _str = "";
public void Append(char value)
{
var spin = new SpinWait();
while (true)
{
var original = Interlocked.CompareExchange(ref _str, null, null);
var newString = original + value;
if (Interlocked.CompareExchange(ref _str, newString, original) == original)
break;
spin.SpinOnce();
}
}
Run Code Online (Sandbox Code Playgroud)
更简单(也更有效)的锁定版本:
private object lk = new object();
public void AppendLock(char value)
{
lock (lk)
{
_str += value;
}
}
Run Code Online (Sandbox Code Playgroud)
如果我尝试添加50.000个字符,则CAS版本需要1.2秒,锁定版本需要700毫秒(平均值).对于100k字符,它们分别需要7秒和3.8秒.这是在四核(i5 2500k)上运行的.
我怀疑CAS显示这些结果的原因是因为它在最后一次"交换"步骤失败了.我是正确的.当我尝试添加50k字符(50k成功交换)时,我能够计算出70k(最佳情况)和近200k(最差情况)失败尝试之间的数量.最糟糕的情况是,每5次尝试中有4次失败.
所以我的问题是:
我的理解是,采用CAS的解决方案尽管难以编码,但随着争用的增加,其规模要好得多,并且比锁具更好.在我的例子中,操作非常小且频繁,这意味着高争用和高频率.那么为什么我的测试显示不然呢?
我认为较长的操作会使情况更糟 - >"交换"失败率会进一步增加.
PS:这是我用来运行测试的代码:
Stopwatch watch = Stopwatch.StartNew();
var cl = new Class1();
Parallel.For(0, 50000, i => cl.Append('a'));
var time = watch.Elapsed;
Debug.WriteLine(time.TotalMilliseconds);
Run Code Online (Sandbox Code Playgroud)
Bri*_*eon 10
问题是循环上的失败率和字符串是不可变的这一事实的组合.我使用以下参数自己做了几个测试.
Append10,000次.我观察到的是,弦的最终长度是80,000(8 x 10,000),所以这是完美的.对我来说,追加尝试的次数平均约为300,000.这就是失败率~73%.只有27%的CPU时间带来了有用的工作.现在因为字符串是不可变的,这意味着在堆上创建了字符串的新实例,并且原始内容加上一个额外字符被复制到其中.顺便说一句,这个复制操作是O(n),因此随着字符串的长度增加它变得越来越长.由于复制操作,我的假设是失败率会随着字符串长度的增加而增加.原因在于,由于线程花费更多时间竞争完成ICX,因此复制操作花费的时间越来越多,冲突的可能性就越高.我的测试证实了这一点
这里最大的问题是顺序字符串连接不能很好地适应并行性.由于操作X n的结果取决于X n-1,因此完全锁定会更快,特别是如果它意味着您避免所有故障和重试.在这种情况下,悲观的策略赢得了与乐观的战斗.当你可以将问题分成独立的卡盘时,低技术可以更好地工作,这些卡盘真的可以无阻碍地并行运行.
作为旁注,使用Interlocked.CompareExchange初始读取_str是不必要的.原因是在这种情况下读取不需要存储器屏障.这是因为Interlocked.CompareExchange实际执行工作的调用(代码中的第二个调用)将创建一个完整的屏障.因此最糟糕的情况是第一次读取是"陈旧的",ICX操作未通过测试,并且循环旋转回来再次尝试.然而,这一次,之前的ICX强行进行了"新鲜"阅读.1
以下代码是我如何使用低锁机制推广复杂操作.实际上,下面给出的代码允许您传递代表操作的委托,因此它非常通用.你想在生产中使用它吗?可能不是因为调用委托很慢,但至少你得到了这个想法.您可以随时对操作进行硬编码.
public static class InterlockedEx
{
public static T Change<T>(ref T destination, Func<T, T> operation) where T : class
{
T original, value;
do
{
original = destination;
value = operation(original);
}
while (Interlocked.CompareExchange(ref destination, value, original) != original);
return original;
}
}
Run Code Online (Sandbox Code Playgroud)
1 在讨论记忆障碍时,我实际上不喜欢"陈旧"和"新鲜"这两个词,因为这不是真正的内容.与实际保证相比,它更具有副作用.但是,在这种情况下,它更好地说明了我的观点.