线程和不安全的变量

时间:2009-11-18 13:25:26

标签: multithreading c#-3.0 locking volatile interlocked

我在此处列出了代码:Threading and Sockets

该问题的答案是使用isListening修改volatile。正如我所说,该修饰符允许我从另一个线程访问该变量。在阅读MSDN之后,我意识到我正在从以下新创建的线程进程中读取isListening。

所以,我现在的问题是:

  • volatile是首选方法,因为我基本上是对变量发出非线程安全请求吗?我已经阅读了关于Interlocked类的内容,并想知道这是否更适合在我的代码中使用。联锁看起来类似于lock(myObj)正在做的事情 - 但具有更多'天赋'和控制力。我知道只是在lock(myObj)周围应用isListening代码块不起作用。

  • 我应该实施Interlocked类吗?

感谢您的时间和回复。

3 个答案:

答案 0 :(得分:2)

如果你所做的只是在C#中的多个线程上读写变量,那么你不必担心同步访问(锁定)变量提供它的类型是bool,char,byte,sbyte ,short,ushort,int,uint,float和reference types。有关详细信息,请参阅here

在您的其他帖子的示例中,您必须将字段标记为volatile的原因是为了确保它不受编译器优化的影响,并且字段中始终存在最新值。有关volatile关键字的详细信息,请参阅here。这样做允许跨线程读取和写入该字段,而不必锁定(同步访问)它。但请注意,volatile关键字只能用于您的字段,因为它的类型为bool。如果它是双倍的,例如volatile关键字不起作用,你必须使用锁。

Interlocked类用于特殊目的,即递增,递减和交换(通常)数值类型的值。这些操作不是原子的。例如,如果要在一个线程中递增一个值并尝试在另一个线程中读取结果值,则通常必须锁定该变量以防止读取中间结果。 Interlocked类只提供一些便利功能,因此您无需在执行增量操作时自行锁定变量。

使用isListening标志执行的操作不需要使用Interlocked类。将该字段标记为不稳定就足够了。

答案 1 :(得分:1)

编辑由于午餐时间赶紧回答..

您之前代码中使用的lock语句是锁定在方法范围内创建的对象实例,因此它对调用同一方法的另一个线程没有任何影响。每个线程必须能够锁定对象的同一实例,以便同步对给定代码块的访问。执行此操作的一种方法(取决于您需要的语义)是使锁定对象成为其使用的类的私有静态变量。这将允许给定对象的多个实例同步对代码块的访问或单个共享资源。如果对象的单个实例或特定于实例的资源需要同步,则应发出静态。

Volatile不保证对给定变量的读取或写入在不同线程中是原子的。它是一个编译器提示,用于保留指令的顺序并防止变量缓存在寄存器中。一般情况下,除非您正在处理对性能非常敏感的事情(低锁定/无锁算法,数据结构等)或者真的知道您正在做的事情,否则我会选择使用Interlocked。在大多数应用程序中使用volatile / interlocked / lock之间的性能差异将是微不足道的,所以如果你不确定它最好用什么给你最安全的保证(阅读Joe Duffy的博客& book)。

例如,在下面的示例中使用volatile不是线程安全的,增量计数器不会达到10,000,000(当我运行测试时它达到了8848450)。这是因为只有传感器才能读取最新值(例如,没有从寄存器缓存)。当使用互锁时,操作是线程安全的,计数器确实达到10,000,000。

public class Incrementor
{
    private volatile int count;

    public int Count
    {
        get { return count; }   
    }

    public void UnsafeIncrement()
    {
        count++;
    }

    public void SafeIncrement()
    {
        Interlocked.Increment(ref count);
    }
}

[TestFixture]
public class ThreadingTest
{
    private const int fiveMillion = 5000000;
    private const int tenMillion = 10000000;

    [Test]
    public void UnsafeCountShouldNotCountToTenMillion()
    {
        const int iterations = fiveMillion;
        Incrementor incrementor = new Incrementor();
        Thread thread1 = new Thread(() => UnsafeIncrement(incrementor, iterations));
        Thread thread2 = new Thread(() => UnsafeIncrement(incrementor, iterations));

        thread1.Start();
        thread2.Start();

        thread1.Join();
        thread2.Join();

        Assert.AreEqual(tenMillion, incrementor.Count);
    }

    [Test]
    public void SafeIncrementShouldCountToTenMillion()
    {
        const int iterations = fiveMillion;
        Incrementor incrementor = new Incrementor();
        Thread thread1 = new Thread(() => SafeIncrement(incrementor, iterations));
        Thread thread2 = new Thread(() => SafeIncrement(incrementor, iterations));

        thread1.Start();
        thread2.Start();

        thread1.Join();
        thread2.Join();

        Assert.AreEqual(tenMillion, incrementor.Count);
    }

    private void UnsafeIncrement(Incrementor incrementor, int times)
    {
        for (int i =0; i < times; ++i)
            incrementor.UnsafeIncrement();
    }

    private void SafeIncrement(Incrementor incrementor, int times)
    {
        for (int i = 0; i < times; ++i)
            incrementor.SafeIncrement();
    }
}

如果您搜索'interlocked volatile',您会找到许多问题的答案。下面的例子解决了你的问题:

下面的一个简单示例显示

Volatile vs. Interlocked vs. lock

答案 2 :(得分:0)

“实现此目的的一种方法是使锁定对象成为其使用的类的私有静态变量。” 为什么它应该是静态的?只要它们在不同的对象上工作,您就可以从多个线程访问同一个函数。我并不是说它不会起作用,但会严重降低应用程序的速度而没有任何优势。或者我错过了什么?

以下是MSDN关于挥发物的说法: “此外,在优化时,编译器必须维护对易失性对象的引用之间的顺序以及对其他全局对象的引用。特别是,

对volatile对象的写入(volatile write)具有Release语义;对在指令序列中写入易失性对象之前发生的全局或静态对象的引用将在编译二进制文件中的易失性写入之前发生。

读取volatile对象(volatile read)具有Acquire语义;在读取指令序列中的易失性存储器之后发生的全局或静态对象的引用将在编译的二进制文件中的易失性读取之后发生。

这允许volatile对象用于多线程应用程序中的内存锁定和释放。“