【发布时间】:2013-10-27 21:38:43
【问题描述】:
我一直在阅读有关无锁技术的文章,例如比较和交换以及利用 Interlocked 和 SpinWait 类来实现无锁定的线程同步。
我自己进行了一些测试,其中我只是有许多线程试图将字符附加到字符串。我尝试使用常规的locks 和比较和交换。令人惊讶的是(至少对我而言),锁显示出比使用 CAS 更好的结果。
这是我的代码的 CAS 版本(基于 this)。它遵循复制->修改->交换模式:
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();
}
}
还有更简单(更高效)的锁版本:
private object lk = new object();
public void AppendLock(char value)
{
lock (lk)
{
_str += value;
}
}
如果我尝试添加 50.000 个字符,CAS 版本需要 1.2 秒,锁定版本需要 700 毫秒(平均)。对于 100k 个字符,它们分别需要 7 秒和 3.8 秒。 这是在四核 (i5 2500k) 上运行的。
我怀疑 CAS 显示这些结果的原因是因为它在最后一个“交换”步骤中失败了很多。我是对的。当我尝试添加 50k 字符(50k 成功交换)时,我能够在 70k(最佳情况)和几乎 200k(最坏情况)之间进行计数失败尝试。最坏的情况,每 5 次尝试中有 4 次失败。
所以我的问题是:
- 我错过了什么? CAS不应该给出更好的结果吗?好处在哪里?
- 为什么以及何时 CAS 是更好的选择? (我知道有人问过这个问题,但我找不到任何令人满意的答案来解释我的具体情况)。
据我了解,使用 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);
【问题讨论】:
-
不,你不测量CAS的执行时间,但主要是字符串比较的执行时间。不幸的是,Interlocked 类没有针对引用类型的原子读取-修改-写入操作(这就是您在“锁定”示例中基本上所做的事情,而不依赖于字符串比较。)
-
您的无锁解决方案比有锁版本做的工作更多。首先,读取现有值的初始
CompareExchange是多余的,执行易失性读取 (Thread.VolatileRead) 将给您相同的结果,而不会减少开销。其次,循环中的每次尝试更新都会复制字符串的“当前”值并附加新值。您对此无能为力,但锁定版本不会遇到此问题。最有可能造成大部分时间差异的是字符串副本。 -
对于我们这些凡人来说,坚持使用现有的锁,而不是尝试使用自己的锁。无需处理ABA 问题,多线程就足够困难了。
-
所以你是说问题出在字符串复制和字符串比较操作上?但如果是这样的话,这不会使这种模式在所有场景中都无效吗?模式要求复制一个值,修改它,然后用新值交换旧值。如果交换失败(您通过比较来验证),则重新开始。
-
另外:高故障率是“正常的”吗?而@William,
Thread.VolatileRead对字符串(或引用类型)没有任何重载。我很确定使用CompareExchange是执行易失性读取的好方法,它似乎比仅将字段标记为volatile更好(不是100% 肯定)。
标签: c# .net multithreading locking compare-and-swap