锁定部分是否始终保证螺纹安全?

时间:2014-05-04 19:57:27

标签: c# thread-safety volatile critical-section

我试图理解对字段的线程安全访问。为此,我实施了一些测试样本:

class Program
{   
    public static void Main()
    {
        Foo test = new Foo();
        bool temp;

        new Thread(() => { test.Loop = false; }).Start();

        do
        {
            temp = test.Loop;
        }
        while (temp == true);
    }
}

class Foo
{
    public bool Loop = true;
}

正如预期的那样,有时候它并没有终止。我知道可以使用volatile关键字或使用lock来解决此问题。我认为我不是Foo类的作者,所以我不能让字段变得不稳定。我尝试使用锁:

public static void Main()
{
    Foo test = new Foo();
    object locker = new Object();
    bool temp;

    new Thread(() => { test.Loop = false; }).Start();

    do
    {
        lock (locker)
        {
            temp = test.Loop;
        }
    }
    while (temp == true);
}
这似乎解决了这个问题。只是为了确保我将循环移动到锁定块内:

lock(locker)
{
    do
    {
        temp = test.Loop;
    }
    while (temp == true);
}

和...程序不再终止。

这让我很困惑。 不锁定提供线程安全访问吗?如果没有,如何安全地访问非易失性字段?我可以使用VolatileRead(),但它不适用于任何情况,如非原始类型或属性。 我认为Monitor.Enter完成了这项工作,我是对的吗?我不明白它是如何运作的。

5 个答案:

答案 0 :(得分:4)

这段代码:

do
{
    lock (locker)
    {
        temp = test.Loop;
    }
}
while (temp == true);

lock的副作用而起作用:它会导致'memory-fence'。实际锁定在这里无关紧要。等效代码:

do
{
   Thread.MemoryBarrier();   
   temp = test.Loop;       
}
while (temp == true);

你在这里试图解决的问题并不完全是线程安全的,而是关于缓存变量(陈旧数据)。

答案 1 :(得分:2)

它不再终止,因为你也在锁外部访问变量。

new Thread(() => { test.Loop = false; }).Start();

你写入锁外的变量。此写入不保证可见。

对同一位置的两次并发访问(其中至少一次是写入)是数据争用。不要那样做。

答案 2 :(得分:0)

Lock为使用锁的不同线程上的2个或更多代码块提供线程安全性。 新线程声明中的循环赋值未包含在锁定中。 这意味着那里没有线程安全。

答案 3 :(得分:0)

一般来说,不,lock不会神奇地使其中的所有代码都是线程安全的。

简单的规则是:如果你有一些由多个线程共享的数据,但你总是只在一个锁(使用相同的锁对象)内访问它,那么该访问是线程安全的。

一旦你离开那个“简单”的代码并开始提问“我怎么能在这里安全地使用volatile / VolatileRed()?”或“为什么这段代码没有使用锁定正确似乎工作?“,事情变得很复杂。除非你准备花很多时间学习C#内存模型,否则你应该避免这种情况。即便如此,只有一百万次运行或仅在某些CPU(ARM)上出现的错误很容易制作。

答案 4 :(得分:0)

锁定仅在通过锁控制对该字段的所有访问时起作用。在您的示例中,只有读取被锁定,但由于写入不是,因此没有线程安全性。

然而,锁定发生在共享对象上也是至关重要的,否则其他线程无法知道某人正在尝试访问该字段。因此,在您锁定一个仅限在Main方法范围内的对象的情况下,另一个线程上的任何其他调用都无法阻止。

如果你无法改变Foo,获得线程安全的唯一方法是让所有调用实际锁定在同一个Foo实例上。但通常不建议这样做,因为对象上的所有方法都将被锁定。

volatile关键字本身并不是线程安全的保证。它意味着可以指示字段的值可以从不同的线程更改,因此读取该字段的任何线程都不应该缓存它,因为值可能会更改。

为了实现线程安全,Foo应该看起来像这样:

class Program
{   
    public static void Main()
    {
        Foo test = new Foo();
        test.Run();

        new Thread(() => { test.Loop = false; }).Start();

        do
        {            
            temp = test.Loop;
        }
        while (temp == true);
    }
}

class Foo
{
    private volatile bool _loop = true;
    private object _syncRoot = new object();

    public bool Loop
    {
        // All access to the Loop value, is controlled by a lock on an instance-scoped object. I.e. when one thread accesses the value, all other threads are blocked.
        get { lock(_syncRoot) return _loop; }
        set { lock(_syncRoot) _loop = value; }
    }

    public void Run()
    {
        Task(() => 
        {
            while(_loop) // _loop is volatile, so value is not cached
            {
                // Do something
            }
        });
    }
}