C#/.NET 中不可重置的“标志”线程是不是安全?

Posted

技术标签:

【中文标题】C#/.NET 中不可重置的“标志”线程是不是安全?【英文标题】:Are unresettable "flags" threadsafe in C#/.NET?C#/.NET 中不可重置的“标志”线程是否安全? 【发布时间】:2013-05-09 13:12:56 【问题描述】:

(注意:我已经问过这个问题,但答案是针对 Java 的,所以我对 C# 和 .NET 框架提出了同样的问题。它不是重复的。)

我使用这种模式已经有一段时间了,但直到最近我才开始认为这样做可能不太好。基本上,我使用了这种模式的一些变体:

public class SampleAsync

    public SampleAsync()  

    private bool completed;
    public void Start()
    
        var worker = new BackgroundWorker();
        worker.DoWork += (sender, e) => 
            //... do something on a different thread
            completed = true;
        ;
        worker.RunWorkerAsync();
    

    public void Update()
    
        if (!completed) return;
        //... do something else
    

*用户负责确保Start 只被调用一次。随时随地调用Update

我一直认为这在 C#/.NET 框架中是线程安全的,因为即使没有严格同步,我也只将 completed 设置为 true。一旦观察到它是true,它就不会重置为false。它在构造函数中被初始化为 false,根据定义,它是线程安全的(除非你在其中做了一些愚蠢的事情)。那么,以这种方式使用不可重置标志是线程安全的吗? (如果是这样,它甚至会提供任何性能优势吗?)

谢谢

【问题讨论】:

在 C# 和 Java 中?否则它是线程安全的吗? “一旦它被观察到是真实的”是如果你不使用volatile可能永远不会发生的位。 为什么不直接使用后台worker的IsBusy属性来判断是否完成呢?我想他们确保可以从任何线程安全地访问它。 @Servy 我并没有真正询问BackgroundWorker - 这只是为了演示。关键是completed 设置在不同的线程中,不一定在线程生命周期结束时。 @aboveyou00 我的意思是,这就是为什么存在诸如BackgroundWorker 之类的类的原因;帮助管理来自最常见用例的线程之间的同步。你应该学会利用它。 【参考方案1】:

它严重依赖于目标架构。英特尔处理器具有强大的内存模型,因此您倾向于使用这样的代码。但是,抖动很可能会搞砸你。例如,x86 抖动倾向于将变量存储在 cpu 寄存器中,尤其是当 if() 语句出现在紧密循环中时。并且仅在发布版本中这样做,令人敬畏的调试噩梦。声明变量 volatile 是一种创可贴。 x64 抖动不需要,至少在当前版本中是这样。但创可贴往往无法阻止内存模型较弱的处理器(如 ARM 和安腾)的流血。他们当然不保证变量的更新状​​态很快就会在另一个线程中可见。线程调度程序倾向于刷新 cpu 缓存。最终。

没有正确执行此操作是没有意义的。使用适当的同步对象,例如 AutoResetEvent。如果您担心循环,或者 Interlocked.CompareExchange()。

【讨论】:

volatile 是否保证该字段直接存储并始终从内存中重新加载? (这两个属性都是必需的)。我不清楚 volatile 实际上提供了什么保证。 @usr:您还需要另一个属性——它不是早期编写的或推测性的。 +1 你的最后一段完美地总结了它。您不必询问某些东西是否是线程安全的。使用同步对象并知道【参考方案2】:

您的代码是线程安全的,因为bool 是原子类型。

MSDN:

以下数据类型的读写是原子的:bool、char、 byte、sbyte、short、ushort、uint、int、float 和引用类型。在 此外,读取和写入具有基础类型的枚举类型 前面的列表也是原子的。其他类型的读写, 包括long、ulong、double和decimal,以及用户自定义 类型,不保证是原子的。除了图书馆 为此目的而设计的功能,不能保证原子性 read-modify-write,比如递增或递减的情况。

见:http://msdn.microsoft.com/en-us/library/aa691278(v=vs.71).aspx

请用volatile标记您的字段:

 private volatile bool completed;

MSDN:

volatile关键字表示一个字段可以在 通过诸如操作系统、硬件或 并发执行线程。

见:http://msdn.microsoft.com/en-us/library/x13ttww7(v=vs.71).aspx

【讨论】:

...但如果您使用[FieldOffset][StructLayout] 明确对齐字段,则所有赌注都将失败。见this question。 这是否保证对该字段的写入会传播到其他线程?我不这么认为。该值可能已注册。 completed 的写入本身可能是原子的且“安全”的,但如果Updatecompleted 的值缓存在寄存器中(或做其他具有相同效果的转换)。 @usr:你是对的。这就是程序员应该使用volatile 来防止缓存的原因。 @virtlink:这不是 100% 正确的。所有布尔值True 都是相同的:它们都按位等于 new Int32(1)。否则,当使用 Object.Equals 时,来自 VB.NET 库的 True 将不等于来自 C#.Library 的 True。我只是尝试了一个具有显式布局的结构,该结构具有一个 bool 和一个 int 相互重叠。所以OP实际上仍在修改一位。但是...如果您是对的,如果布尔值中的任何位被设置为两次写入操作的一部分,Update 方法仍然会收到正确的信号。

以上是关于C#/.NET 中不可重置的“标志”线程是不是安全?的主要内容,如果未能解决你的问题,请参考以下文章

C语言fft库函数是线程安全吗

Java高并发程序设计—— java内存模型和线程安全

如果我只想以线程安全的方式测试和设置标志,那么线程类是啥?

.Net 应用程序中的安全标志未设置为 Cookie

使用多线程在 GNU C 中使用写入函数是不是安全

C ++如果一个线程写入一旦完成就会切换一个布尔值,那么在另一个线程的循环中读取该布尔值是不是安全?