【问题标题】:trying to replace a lock() with a SpinWait.SpinUntil() but it doesnt work试图用 SpinWait.SpinUntil() 替换 lock() 但它不起作用
【发布时间】:2026-02-21 18:35:01
【问题描述】:

让我们从代码开始;

checkedUnlockHashSet<ulong>

_hashsetLock 是一个对象

lock (_hashsetLock)
    newMap = checkedUnlock.Add(uniqueId);

fun int

SpinWait.SpinUntil(() => Interlocked.CompareExchange(ref fun, 1, 0) == 1);
newMap = checkedUnlock.Add(uniqueId);
fun = 0;

我的理解是 SpinWait 在这种情况下应该像 lock() 一样工作,但在 HashSet 中添加了更多项目,有时它匹配锁定,有时其中还有 1 到 5 个项目,这使得很明显它不起作用

我的理解有问题吗?

编辑

我试过了,它似乎有效,到目前为止我的测试显示与lock() 相同的数字

SpinWait spin = new SpinWait();
while (Interlocked.CompareExchange(ref fun, 1, 0) == 1)
   spin.SpinOnce();

那么为什么它可以用这个而不是SpinWait.SpinUntil() 呢?

编辑#2

小完整应用查看

在这段代码中,SpinWait.SpinUntil 有时会爆炸(添加会抛出异常)但是当它工作时,计数会不同,所以我对此的预期行为是错误的

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var list = new List<int>();
            var rnd = new Random(42);
            for (var i = 0; i < 1000000; ++i)
                list.Add(rnd.Next(500000));



            object _lock1 = new object();
            var hashset1 = new HashSet<int>();

            int _lock2 = 0;
            var hashset2 = new HashSet<int>();

            int _lock3 = 0;
            var hashset3 = new HashSet<int>();

            Parallel.ForEach(list, item =>

            {
                /******************/
                lock (_lock1)
                    hashset1.Add(item);
                /******************/

                /******************/
                SpinWait.SpinUntil(() => Interlocked.CompareExchange(ref _lock2, 1, 0) == 1);

                hashset2.Add(item);

                _lock2 = 0;
                /******************/

                /******************/
                SpinWait spin = new SpinWait();
                while (Interlocked.CompareExchange(ref _lock3, 1, 0) == 1)
                    spin.SpinOnce();

                hashset3.Add(item);

                _lock3 = 0;
                /******************/

            });


            Console.WriteLine("Lock: {0}", hashset1.Count);
            Console.WriteLine("SpinWaitUntil: {0}", hashset2.Count);
            Console.WriteLine("SpinWait: {0}", hashset3.Count);

            Console.ReadKey();
        }

    }
}

【问题讨论】:

  • 你为什么不使用并发散列集的数据类型,见*.com/a/18923091/7565574?它们通常针对并发访问进行了优化,甚至不需要锁。
  • lock 有什么问题?你看过SemaphoreSlim 类吗?这是一个很好的选择,而且更容易推理。
  • @ckuri 在我的代码中,这是一个仅添加的场景,并发字典已针对读取进行了优化,我无法使用它
  • @tigerswithguitars 正在查看 github.com/Microsoft/referencesource/blob/master/mscorlib/… 的代码,它使用了 spinwait & lock & monitor 的组合,有点重
  • 为了清楚起见,了解更多关于为什么lock 不适用和SemaphoreSlim 太重的细节会很有帮助。似乎用例很重要,这是一个有趣的问题。

标签: c# multithreading concurrency thread-safety spinwait


【解决方案1】:

SpinWait.SpinUntil 中使用的条件错误。

  1. Interlocked.CompareExchange 返回变量的原始值。
  2. SpinWait.SpinUntil 的 MSDN 文档说,条件是

要反复执行的委托,直到它返回 true。

你想旋转直到发生 0 -> 1 转换,所以条件应该是

Interlocked.CompareExchange(ref fun, 1, 0) == 0

在其他线程上对 CompareExchange 的后续调用结果为 1,因此它们将一直等待,直到“获胜者”线程将 fun 标志恢复为 0。

一些进一步的评论:

  • fun = 0; 应该适用于 x86 架构,但我不确定它是否在任何地方都是正确的。如果您使用 Interlocked 访问某个字段,最好使用 Interlocked 访问该字段的所有访问权限。所以我建议改为Interlocked.Exchange(ref fun, 0)
  • SpinWait 在性能方面很少是一个好的解决方案,因为它可以防止操作系统将旋转线程置于空闲状态。它应该只用于非常短的等待。 (An example of a proper usage)。简单的锁(又名 Monitor.Enter/Exit)或 SemaphoreSlim 通常可以使用,或者如果读取次数 >> 写入次数,您可以考虑使用 ReaderWriterLockSlim。

【讨论】:

  • 我使用 CompareExchange 的方式是模拟锁的正确方法,其他在线示例只需执行一个 != 原始值,我会执行一个 == 新值,如果我打算与 == 0 一起使用,它会在几分之一秒内爆炸,因为它就像没有锁定到位,直接进行交换也不会做我所期望的,如果有另一个线程则停止处理代码的那部分。对于 spinwait 的使用,明确指出应该使用 VERY SHORT 时间段,添加到 hashset 的操作应该属于该类别
  • CompareExchange 返回原始值,即更改前的值(如果有)。 CompareExchange 返回 0 表示标志为 0,并且在当前调用期间已更改为 1。在任何其他情况下,CompareExchange 返回 1,这意味着“已锁定”。
  • @Fredou 试试this code。我希望这能说服你。
  • 这个代码示例没有真正的多线程,只是在整个代码周围的任务中放置一个循环,一个任务将运行 X 时间然后第二个任务将启动 X 时间,它不会在线程之间切换。
  • 但是你的权利,对于 SpinUntil,compareExchange 应该是 SpintWait 逻辑实例的反面,这引起了我的困惑
最近更新 更多