在C#中访问简单的布尔标志时,我需要锁定或标记为易失性吗?

44

假设你有一个在后台线程上运行的简单操作。你想提供一种取消这个操作的方式,因此你创建了一个布尔标志,并从取消按钮的单击事件处理程序中将其设置为true。

private bool _cancelled;

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

现在你是在GUI线程中设置取消标志,但在后台线程中读取它。在访问这个布尔值之前需要加锁吗?

你需要这样做吗(显然也需要在按钮点击事件处理程序中加锁):

while(operationNotComplete)
{
    // Do complex operation

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

或者这样做是否可行(没有锁):

while(!_cancelled & operationNotComplete)
{
    // Do complex operation
}
或者将_cancelled变量标记为volatile。这是必要的吗?
有两种理论。
1)因为它是一个简单的内置类型(在.NET中访问内置类型是原子的),我们只在一个地方写入并在后台线程上读取,所以不需要锁定或标记为volatile。 2)您应该将其标记为volatile,因为如果不这样做,编译器可能会优化掉while循环中的读取,因为它认为没有任何东西能够修改该值。
哪种技术是正确的?(为什么?)
【编辑:看来这里有两个明确定义和对立的观点。我正在寻找一个明确的答案,请尽可能在回答中附上您的理由和引用来源。】

6
是的,你确实需要使用 volatile 或者 lock(作为内存屏障)来避免 C++ 线程缓存一个值并忽略其他线程对该值的更改。具体请参考此链接:https://dev59.com/lHRB5IYBdhLWcg3w9Lvn#458193 - Marc Gravell
1
这是否意味着Simon在他一直与EFraim(https://dev59.com/TUjSa4cB1Zd3GeqPHrjB#1221854)进行的大规模辩论中将会面临尴尬? - Lloyd Powell
1
@Marc:不错的例子,谢谢。 @ThePower:当我不确定的时候,我很乐意承认,希望我只会略微脸红 =:) - Simon P Stevens
2
Joe Duffy的《Windows并发编程》被加入到我的书单中了! - Mitch Wheat
@Mitch:这是一本很棒的书,内容非常丰富,尽管有时他会有些啰嗦。 :) - Greg D
5个回答

40

首先,线程编程是很棘手的 ;-p

尽管有许多谣言声称不需要,但在从多个线程中访问 bool 时,要么 使用 lock 要么 使用 volatile(但不要两者都使用)是必须的。

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

对于较大的值(其中原子性是一个问题),或者您想要同步一系列操作(典型示例是“如果不存在则添加”字典访问),lock 更加灵活。它充当内存屏障,因此仍然为您提供线程安全,但提供其他功能,例如脉冲/等待。请注意,不应在值类型或 string 上使用 lock;也不要在 Typethis 上使用;最好的选择是将自己的锁对象作为字段(readonly object syncLock = new object();)并在其上锁定。

如果您不进行同步,将会有多糟糕的错误示例(即永远循环)- 请看这里

要跨多个程序使用,像 Mutex*ResetEvent 这样的操作系统原语也可能是有用的,但对于单个 exe 来说,这有点过度处理。


我认为这解决了问题。如果您不使用锁或volatile,它会出现错误的可重现示例。 - Simon P Stevens
@MarcGravell♦ 有没有可能两个线程在完全相同的时间写入volatile变量? - onmyway133
1
@entropy 是的,完全没有任何阻止这种情况发生的东西。其中一个值将会胜出;但是并没有定义哪个会胜出。请注意,编译器只允许您在可以原子写入的类型中使用 volatile,因此您不会得到一个被撕裂的值。 - Marc Gravell

6

_cancelled必须是volatile的。(如果您选择不锁定)

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

此外,我认为_cancelled的读/写操作是原子的

CLI规范的第12.6.6节规定: “符合CLI的实现应保证 对于大小不超过本地字长的正确对齐的内存位置的读取和写入访问是原子的, 当所有对位置的写入访问具有相同的大小时。”


@Mitch:但在这种情况下,这意味着它可能会搞砸事情。 - EFraim
仅作为更正,锁定对于变量的读取或写入是否被缓存没有任何影响。锁定不是易失变量的替代品。 - Adam Robinson
2
@Adam - 我相信锁起到的作用就像是内存屏障一样,具有同样的目的。 - Marc Gravell
@Marc:Monitor.Enter(就像您所知道的那样,但其他人可能不知道的)仅获取独占锁,至少根据文档。我不确定它如何作为内存屏障,至少肯定不是锁定之外的任何内容。需要的逻辑必须是前瞻性的(要查看需要受保护的内容,因为“刷新”所有缓存和写入似乎效率低下),并且乍一看似乎对于我的小脑袋来说并不真实可能 ;) 是否有某些信息可以确认这一点? - Adam Robinson
1
使用lock()会产生与将字段声明为volatile相同的保证。 - Daniel Brückner
显示剩余4条评论

5
由于您处于单写线程场景,并且布尔字段是一个简单结构,不会出现损坏状态的风险(虽然有可能获取到既不为false也不为true的布尔值),因此不需要进行锁定。但是,您必须将该字段标记为volatile,以防止编译器进行某些优化。如果没有volatile修饰符,则编译器可以在工作线程上的循环执行期间将该值缓存到寄存器中,反过来,该循环将永远无法识别更改的值。本MSDN文章(如何:创建和终止线程(C#编程指南))解决了这个问题。 当有需要进行锁定时,使用锁定将与将字段标记为volatile具有相同的效果。

没错。由于您要么读取要么设置此标志的值,所以您只需要一个“volatile”属性。 - jpbochi
但是理论上,如果布尔字段的更新不是原子性的,并且部分更新导致一个既不是真也不是假的值,那么可能会出现相当奇怪的情况,但在给定的使用场景中这不会造成任何问题。 - Daniel Brückner
@Daniel - 根据规范,bool类型的更新操作是保证原子性的。 - Marc Gravell
1
ECMA334v4 §12.5 变量引用的原子性 以下数据类型的读写应具有原子性:bool、char、byte、sbyte、short、ushort、uint、int、float和引用类型。此外,具有前面列表中基础类型的枚举类型的读写也应具有原子性。 - Marc Gravell
谢谢。我刚看了一下RFC 2119,发现我倾向于将“shall”解释为“should”,因为它与德语中的“should”相似。 - Daniel Brückner

2

为了实现线程同步,建议使用一个EventWaitHandle类,例如ManualResetEvent。虽然像在这里一样使用一个简单的布尔标志稍微简单一些(是的,您需要将其标记为volatile),但依我之见,最好还是养成使用线程工具的习惯。对于您的目的,您可以这样做...

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();

在我看来,对于单个应用程序来说,操作系统原语通常过于复杂。大多数事情只需要使用“监视器”即可完成。 - Marc Gravell
@Marc:虽然在这里可能不需要显式使用*ResetEvent,但我认为对于单个应用程序来说,它并不过度。在一个应用程序中,有许多情况下,多个线程可能需要使用EventWaitHandle的功能,比如ManualResetEvent。 - Adam Robinson

1

查询Interlocked.Exchange()。它可以非常快速地将值复制到本地变量中,可用于比较。它比lock()更快。


+1,不一定是最好的选择,但它是三种可能性之一(lock/interlocked/volatile)。 - H H
Interlocked.Exchange() 不是我控制循环变量的首选。我会使用 Event/WaitHandle。如果您需要原子地获取单个整数/布尔变量,则它是一个不错的选择。 - hughdbrown

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接