Interlocked在C#中是否保证对其他线程的可见性,还是我仍然需要使用volatile?

23 浏览
0 Comments

Interlocked在C#中是否保证对其他线程的可见性,还是我仍然需要使用volatile?

我一直在阅读一个类似的问题的答案,但我还是有点困惑... Abel给出了一个很好的答案,但这是我不确定的部分:

...声明一个变量为volatile会使得每次访问都是volatile的。无法通过其他方式强制这种行为,因此volatile无法用Interlocked替代。在其他库、接口或硬件可以访问并随时更新变量的情况下,或者需要最新版本的情况下,这是必需的。

Interlocked能保证原子操作对所有线程的可见性,还是我仍然需要在值上使用volatile关键字来保证变更的可见性?

这是我的示例代码:

volatile int value = 100000; // <-- 我需要使用volatile关键字吗
// ....
public void AnotherThreadMethod()
{
 while(Interlocked.Decrement(ref value)>0)
 {
  // do something
 }
}
public void AThreadMethod()
{
 while(value > 0)
 {
  // do something
 }
}

更新:

我改变了原来的示例,所以再次给出原例:

public class CountDownLatch
{
    private volatile int m_remain; // <--- 我需要在这里使用volatile关键字吗?
    private EventWaitHandle m_event;
    public CountDownLatch(int count)
    {
        Reset(count);
    }
    public void Reset(int count)
    {
        if (count < 0)
            throw new ArgumentOutOfRangeException();
        m_remain = count;
        m_event = new ManualResetEvent(false);
        if (m_remain == 0)
        {
            m_event.Set();
        }
    }
    public void Signal()
    {
        // The last thread to signal also sets the event.
        if (Interlocked.Decrement(ref m_remain) == 0)
            m_event.Set();
    }
    public void Wait()
    {
        m_event.WaitOne();
    }
}

0
0 Comments

在C#中,Interlocked类提供了一些原子操作,用于在多线程环境中确保变量的正确访问。然而,在使用Interlocked操作时,有人提出了一个问题:Interlocked是否保证对其他线程的可见性,或者我是否仍然需要使用volatile关键字?

一些人认为,Interlocked操作不需要volatile关键字,因为你永远不会直接检查Interlocked变量的值。相反,你总是检查Interlocked操作返回的值。混合使用Interlocked操作和普通的赋值/比较操作会导致错误的代码。

另外,某些情况下了一个Reset()函数的问题。这段代码中的赋值和直接检查m_remain变量是错误的。建议将其移除,因为不仅实现方式不正确,而且我很怀疑在计数器的中间状态重置的语义是否需要。只保留构造函数、Signal和Wait这三个操作符即可,它们现在的实现是正确的。

还某些情况下,即使你混合使用Interlocked操作和普通操作,volatile关键字仍然是必需的。volatile关键字主要涉及到IL代码和JIT生成的代码,用于确保始终从实际内存位置读取值,并且不进行任何优化,如代码重排序。无论其他地方使用Interlocked操作还是直接赋值,都不会对读取该值的其他部分产生影响。如果没有volatile属性,编译器/JIT可能仍然会生成忽略其他地方发生的写入的代码,无论这些写入是使用Interlocked操作还是直接赋值。

此外,还提到了一种常见的模式,即混合使用普通读取和Interlocked操作,通常涉及到Interlocked.CompareExchange。这种模式的步骤如下:首先读取当前状态,根据当前状态进行一些计算,然后尝试使用Interlocked.CompareExchange替换状态:如果成功,则继续,如果失败,则放弃计算结果并回到第一步。

最后,某些情况下了Reset函数的问题。他们认为,Reset函数可能会被滥用,因此更好的做法是提供一个无法被滥用的API,而不是要求用户小心使用API。他们还提到了另一种模式,即通过循环来确保读取的值不过时。这种模式只需要一个Interlocked操作,而不是两个。

Interlocked操作本身并不保证对其他线程的可见性,因此在某些情况下仍然需要使用volatile关键字。混合使用Interlocked操作和普通操作可能会导致代码错误,因此最好避免这种混合使用。如果需要混合使用,可以考虑使用Interlocked.CompareExchange等模式来确保正确性。最后,Reset函数可能会引起滥用,因此最好避免使用它,提供一个更安全的API。

0
0 Comments

在C#中,使用Interlocked类的方法可以确保原子性操作,并且对其他线程的可见性。然而,有时候我们可能仍然需要使用volatile关键字来确保可见性。

问题的原因是,虽然Interlocked类提供了一些方法来实现原子性操作,但它并不能保证对其他线程的可见性。换句话说,当一个线程使用Interlocked类的方法修改了一个共享变量的值时,其他线程可能不会立即看到这个修改。

解决方法是使用volatile关键字。volatile关键字可以确保对共享变量的写入操作立即对其他线程可见。通过使用volatile关键字,我们可以确保在一个线程修改了共享变量的值后,其他线程能够立即看到这个修改。

下面是使用Interlocked类和volatile关键字的示例代码:

private static volatile int myVariable;
public static void Main()
{
    Thread thread1 = new Thread(IncrementVariable);
    Thread thread2 = new Thread(ReadVariable);
    thread1.Start();
    thread2.Start();
    thread1.Join();
    thread2.Join();
}
private static void IncrementVariable()
{
    for (int i = 0; i < 1000000; i++)
    {
        Interlocked.Increment(ref myVariable);
    }
}
private static void ReadVariable()
{
    int value = Volatile.Read(ref myVariable);
    Console.WriteLine("Value: " + value);
}

在上面的代码中,我们使用Interlocked.Increment方法对myVariable进行递增操作,并使用Volatile.Read方法读取myVariable的值。通过这种方式,我们可以保证递增操作是原子的,并且读取到的值是最新的。

总结起来,虽然Interlocked类可以用于实现原子性操作,但它不能保证对其他线程的可见性。为了确保可见性,我们仍然需要使用volatile关键字。通过使用Interlocked类和volatile关键字,我们可以实现线程安全的操作。

0