我一直在阅读有关无锁技术的文章,例如比较和交换以及利用 Interlocked 和 SpinWait 类来实现线程同步而无需锁定。
我自己进行了一些测试,其中我只是有许多线程试图将字符附加到字符串。我尝试使用常规lock
s 和比较和交换。令人惊讶的是(至少对我而言),锁显示出比使用 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);