Interlocked.exchange 作为锁

Posted

技术标签:

【中文标题】Interlocked.exchange 作为锁【英文标题】:Interlocked.exchange as a lock 【发布时间】:2014-12-19 15:03:52 【问题描述】:

我正在尝试使用Interlocked.Exchange 为某些对象初始化函数创建线程安全锁。考虑下面的代码。我想确定if 的作用与替换 while 时的作用相同。我问的原因是代码是否反复运行,有时您会在set 消息之前收到退出消息。我只是想确认这只是一个 gui 的事情,因为退出时的状态似乎总是正确的。

class Program

    private static void Main(string[] args)
    
        Thread thread1 = new Thread(new ThreadStart(() => InterlockedCheck("1")));
        Thread thread2 = new Thread(new ThreadStart(() => InterlockedCheck("2")));
        Thread thread3 = new Thread(new ThreadStart(() => InterlockedCheck("3"))); 
        Thread thread4 = new Thread(new ThreadStart(() => InterlockedCheck("4")));
        thread4.Start();
        thread1.Start();
        thread2.Start();
        thread3.Start();

        Console.ReadKey();
    

    const int NOTCALLED = 0;
    const int CALLED = 1;
    static int _state = NOTCALLED;
    //...
    static void InterlockedCheck(string thread)
    
        Console.WriteLine("Enter thread [0], state [1]", thread, _state);

        //while (Interlocked.Exchange(ref _state, CALLED) == NOTCALLED)
        if (Interlocked.Exchange(ref _state, CALLED) == NOTCALLED)
        
            Console.WriteLine("Setting state on T[0], state[1]", thread, _state);
        

        Console.WriteLine("Exit from thread [0] state[1]", thread, _state);
    

【问题讨论】:

【参考方案1】:

我不会称它为 lock,因为它只能使用一次,但如果您假设 if 范围内的语句将仅执行一次,即使 InterlockedCheck 从多个线程并发。

那是因为您从 NOTCALLED 开始并且仅使用原子 Interlocked.Exchange 设置 CALLED。只有第一个调用会返回NOTCALLED,而所有后续调用都会返回CALLED

更好(更简单)的解决方案是使用 .Net 的 Lazy 类,它非常适合初始化:

static Lazy<ExpensiveInstance> _lazy = new Lazy<ExpensiveInstance>(Initialization, LazyThreadSafetyMode.ExecutionAndPublication); 

您可以使用_lazy.Value 检索结果并查询它是否是使用_lazy.IsValueCreated 创建的。 Initialization 不会运行,直到它需要并且不超过一次。

【讨论】:

【参考方案2】:

我不明白你为什么在这里使用Interlocked 而不是更易读和易于理解的lock 语句。就个人而言,我会建议后者。

无论哪种方式,您在“设置”消息之前看到一个或多个“退出”消息的原因是由于线程调度。即使第一个命中Interlocked 的线程将始终执行“设置”操作,但该线程可能在它有机会执行操作之前被抢占,从而允许其他线程首先发出其“退出”消息。

请注意,根据您在此处的确切需求,虽然使用if 与使用while 循环相同,但很可能两者都无法实现您想要的。 IE。第一个命中if 的线程会将值设置为CALLED 值,因此任何其他线程都会继续运行。如果你真的想初始化这里的东西,你可能希望其他线程等待实际执行初始化代码的线程,以便所有线程可以继续知道初始化状态是有效的。

我确实认为一种或另一种方式是个好主意(即,减少对用户的混淆,更重要的是,如果实际代码比您显示的更复杂,则更有可能产生正确的结果)如果你同步整个方法。

使用Interlocked 来完成这比您现在拥有的代码要复杂得多。它将涉及一个循环和至少一个额外的状态值。但是使用lock 声明,它简单易读。它看起来更像这样:

const int NOTCALLED = 0;
const int CALLED = 1;
static int _state = NOTCALLED;
static readonly object _lock = new object();
//...
static void InterlockedCheck(string thread)

    lock (_lock)
    
        Console.WriteLine("Enter thread [0], state [1]", thread, _state);

        if (_state == NOTCALLED)
        
            Console.WriteLine("Setting state on T[0], state[1]", thread, _state);
            _state = CALLED;
        

        Console.WriteLine("Exit from thread [0] state[1]", thread, _state);
    

这样,第一个获得锁的线程在任何其他线程之前执行所有它的代码,特别是它确保第一个线程中的“设置”操作发生在之前任何其他线程中的“退出”操作。

【讨论】:

你可能应该仔细检查lock 我假设您的意思是“仔细检查 _state 值”。在这个具体的例子中这样做是没有意义的。这里的锁定点既是为了保护对_state 字段的处理,也是为了确保“进入”和“退出”消息的有序执行。双重检查的实现将无法完成后者。在任何情况下,双重检查锁的性能改进几乎都不足以证明代码复杂性的增加是合理的。 “进入”和“退出”消息不需要在锁内。将这些消息作为关键部分的一部分没有任何价值。双重检查是一种已知模式,它提高了快速路径的性能。考虑到 OP 询问Interlocked 操作性能可能是一个问题。如果双重检查太复杂,那么Lazy(针对快速路径进行了优化)会为您完成。 “将这些消息作为关键部分的一部分没有任何价值”——问题特别指出了“exit”消息出现的顺序与预期不符的担忧初始化。由于所有这些代码显然不是真正的代码,这对我来说显然意味着它确实需要成为正确解决方案中关键部分的一部分。无论如何,澄清需要来自 OP,而不是其他人。双重检查是一种已知的模式,但也是一个已知的陷阱,因为它通常实施错误并且通常不会有效地提高性能 Lazy&lt;T&gt; 非常适合初始化对象或值的实例,但是当初始化涉及多个操作且没有明确的返回值时,它在语义上很尴尬。执行此类初始化的最佳方法实际上是在构造函数中(静态或实例,取决于需要)。最后,OP询问Interlocked这一事实毫无意义;许多人认为他们需要无锁代码,而实际上根本没有证据支持这种信念。但感谢您的意见。

以上是关于Interlocked.exchange 作为锁的主要内容,如果未能解决你的问题,请参考以下文章

C#ThreadInterlocked 轻量级锁

C#ThreadInterlocked 轻量级锁

Mono 上的 Interlocked.Read / Interlocked.Exchange 比 .NET 慢得多?

Interlocked.Exchange<T> 比 Interlocked.CompareExchange<T> 慢吗?

引用分配是原子的,那么为啥需要 Interlocked.Exchange(ref Object, Object)?

如何为 C# 中的枚举类型应用 InterLocked.Exchange?