【问题标题】:Do I need to lock or mark as volatile when accessing a simple boolean flag in C#?在 C# 中访问简单的布尔标志时,是否需要锁定或标记为 volatile?
【发布时间】:2009-08-03 12:50:05
【问题描述】:

假设您有一个在后台线程上运行的简单操作。您想提供一种取消此操作的方法,因此您可以创建一个布尔标志,将其从取消按钮的单击事件处理程序中设置为 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。有必要吗?

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

似乎有两种理论。

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

哪种方法是正确的? (为什么?)

[编辑:在这方面似乎有两种明确定义和对立的思想流派。我正在寻找一个明确的答案,所以如果可能的话,请发表你的理由,并在你的答案中引用你的来源。]

【问题讨论】:

  • 是的,您确实需要volatilelock(作为内存屏障):请参阅stackoverflow.com/questions/458173/…
  • 这是否意味着西蒙将在他与 EFraim (stackoverflow.com/questions/1221839/…) 进行的大规模辩论中面红耳赤
  • @Marc:很好的例子,谢谢。 @ThePower:当我不确定时,我很高兴举起手来,所以希望我能逃脱只是略带粉红色的脸 =:)
  • Joe Duffy 的“Windows 上的并发编程”正在添加到我的书单中!
  • @Mitch:这是一本很棒的书,内容很多,尽管他偶尔会有点啰嗦。 :)

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


【解决方案1】:

首先,线程很棘手;-p

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

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

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

举个例子,如果你不同步,它会破坏多严重(即永远循环) - see here

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

【讨论】:

  • 我认为这解决了问题。如果您不使用 lock 或 volatile,它可能会出错。
  • @MarcGravell♦ 是否有可能有 2 个线程同时写入 volatile 变量?
  • @entropy 是的,没有什么可以阻止它。其中一个值将获胜;没有定义哪个。请注意,编译器仅允许您将 volatile 与可以原子编写的类型一起使用,因此您最终不会得到一个撕裂的值
【解决方案2】:

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

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

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

CLI 规范的第 12.6.6 节指出: “符合标准的 CLI 应保证 正确的读写权限 对齐的内存位置不大于 比原生字长是原子的 当所有的写访问 位置大小相同。”

【讨论】:

  • @Mitch:但在这种情况下,这意味着它可能会把事情搞砸。
  • 作为更正点,锁定不会影响对变量的读取或写入是否被缓存。锁定不是 volatile 变量的替代品。
  • 原子性与它几乎没有关系...如果另一个线程仍在咀嚼自己的寄存器,那么无论您将值写入 1 个块还是 26 个块,都将产生零差异。
  • @Adam - 我相信锁的作用是作为内存屏障 - 服务于相同的目的。
  • 使用 lock() 产生与使字段易失性相同的保证。
【解决方案3】:

锁定不是必需的,因为您只有一个编写器场景,并且布尔字段是一个简单的结构,没有破坏状态的风险 (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 的效果相同。

【讨论】:

  • 没错。由于您要么读取或设置此标志的值,因此您只需要一个 volatile 属性。
  • 但是理论上,如果布尔字段的更新不是原子的并且部分更新导致的值既不是真也不是假,那么理论上你可能会遇到一个非常奇怪的情况,但这不会导致任何问题给定的使用场景。
  • @Daniel - bool 更新被规范保证是原子的。
  • ECMA334v4 §12.5 变量引用的原子性 以下数据类型的读写应是原子的:bool、char、byte、sbyte、short、ushort、uint、int、float 和引用类型。此外,上一个列表中具有基础类型的枚举类型的读取和写入也应该是原子的。
  • 谢谢。刚刚查看了 RFC 2119,发现我倾向于将“shall”解释为“should”,因为它类似于德语中的“should”。
【解决方案4】:

对于线程同步,建议您使用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();

【讨论】:

  • 操作系统原语对于单个应用程序(IMO)来说通常是多余的。大多数事情都可以通过Monitor 完成。
  • @Marc:虽然这里可能没有明确要求 *ResetEvent,但我不认为我愿意说它对于单个应用程序来说太过分了。在许多情况下,一个应用程序中的多个线程可能需要使用 EventWaitHandle 的功能,例如 ManualResetEvent。
【解决方案5】:

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

【讨论】:

  • +1,并不总是最好的,但它是三种(锁定/互锁/易失)可能性之一。
  • 对于用于控制循环的变量,Interlocked.Exchange() 不是我的首选。我会使用事件/WaitHandle。如果您需要原子地获取单个整数/布尔变量,这是一个不错的选择。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-14
  • 2023-03-07
相关资源
最近更新 更多