在C#中访问简单的布尔标志时,是否需要锁定或标记为volatile?

时间:2009-08-03 12:50:06

标签: c# .net multithreading locking thread-safety

让我们假设你有一个在后台线程上运行的简单操作。您希望提供一种取消此操作的方法,以便创建一个布尔标志,您可以从取消按钮的单击事件处理程序设置为true。

private bool _cancelled;

private void CancelButton_Click(Object sender ClickEventArgs e)
{
    _cancelled = true;
}

现在您正在从GUI线程设置取消标志,但是您正在从后台线程中读取它。在访问bool之前你需要锁定吗?

您是否需要这样做(显然也会锁定按钮点击事件处理程序):

while(operationNotComplete)
{
    // Do complex operation

    lock(_lockObject)
    {
        if(_cancelled)
        {
            break;
        }
    }
}

或者这样做是否可以接受(没有锁定):

while(!_cancelled & operationNotComplete)
{
    // Do complex operation
}

或者将_cancelled变量标记为volatile。这有必要吗?

[我知道BackgroundWorker类有内置的CancelAsync()方法,但我对这里的语义和锁定以及线程变量访问的使用感兴趣,而不是具体的实现,代码只是一个例子。

似乎有两种理论。

1)因为它是一个简单的内置类型(并且对内置类型的访问在.net中是原子的)并且因为我们只在一个地方写入它并且只在后台线程上读取而不需要锁定或标记因为不稳定 2)你应该将它标记为volatile,因为如果你不这样做,编译器可能会优化while循环中的读取,因为它认为它无法修改该值。

哪种技术是正确的? (为什么?)

<编辑:在这方面似乎有两个明确定义和反对的思想流派。我正在寻找一个明确的答案,所以请尽可能发布你的理由并引用你的消息来源和你的答案。]

5 个答案:

答案 0 :(得分:37)

首先,线程很棘手; -p

是的,尽管有相反的谣言, 使用lock {{1}需要 从多个线程访问volatile时(但不是两者)。

对于简单类型和访问,例如退出标志(bool),那么bool就足够了 - 这可以确保线程不会将值缓存在它们的寄存器中(意思是:其中一个线程)永远不会看到更新。

对于较大的值(原子性是一个问题),或者您希望同步序列操作的位置(典型示例为“如果不存在并添加”字典访问),则为{{ 1}}更通用。这充当了内存屏障,因此仍然为您提供线程安全性,但提供其他功能,如脉冲/等待。请注意,您不应在值类型或volatile上使用lock;也不是lockstring;最好的选择是将您自己的锁定对象作为字段(Type)并锁定它。

如果您没有同步 - see here,请举例说明它有多么糟糕(即永远循环)。

要跨越多个程序,像thisreadonly object syncLock = new object();这样的OS原语也可能有用,但对于单个exe来说这是过度的。

答案 1 :(得分:6)

_cancelled必须为volatile。 (如果你不选择锁定)

如果一个线程更改了_cancelled的值,则其他线程可能看不到更新的结果。

另外,我认为_cancelled的读/写操作是 atomic

  

CLI规范的第12.6.6节规定:   “符合要求的CLI应保证   正确读写访问权限   对齐的内存位置不大   比原生单词大小是原子的   当所有写入访问a时   位置大小相同。“

答案 2 :(得分:5)

不需要锁定,因为您有一个编写器场景,而布尔字段是一个简单的结构,没有破坏状态的风险(while it was possible to get a boolean value that is neither false nor true)。但是您必须将该字段标记为volatile以防止编译器进行某些优化。如果没有volatile修饰符,编译器可以在您的工作线程上执行循环期间将值缓存在寄存器中,反过来循环将永远不会识别更改的值。此MSDN文章(How to: Create and Terminate Threads (C# Programming Guide))解决了此问题。 虽然需要锁定,但锁定与标记字段volatile具有相同的效果。

答案 3 :(得分:2)

对于线程同步,建议您使用EventWaitHandle类之一,例如ManualResetEvent。虽然在这里使用一个简单的布尔标志稍微简单一点(是的,你想把它标记为volatile),IMO最好进入使用线程工具的做法。为了你的目的,你会做这样的事情......

private System.Threading.ManualResetEvent threadStop;

void StartThread()
{
    // do your setup

    // instantiate it unset
    threadStop = new System.Threading.ManualResetEvent(false); 

    // start the thread
}

在你的帖子中..

while(!threadStop.WaitOne(0) && !operationComplete)
{
    // work
}

然后在GUI中取消...

threadStop.Set();

答案 4 :(得分:1)

查找Interlocked.Exchange()。它可以非常快速地复制到可用于比较的局部变量中。它比lock()快。