C# 无锁编码健全性检查

C# 无锁编码健全性检查,c#,multithreading,volatile,C#,Multithreading,Volatile,更新:现在使用基于以下评论的只读集合 我相信下面的代码应该是线程安全的无锁代码,但我想确保我没有遗漏什么 public class ViewModel : INotifyPropertyChanged { //INotifyPropertyChanged and other boring stuff goes here... private volatile List<string> _data; public IEnumerable<string>

更新:现在使用基于以下评论的只读集合

我相信下面的代码应该是线程安全的无锁代码,但我想确保我没有遗漏什么

public class ViewModel : INotifyPropertyChanged
{
   //INotifyPropertyChanged and other boring stuff goes here...

   private volatile List<string> _data;
   public IEnumerable<string> Data
   {
      get { return _data; }
   }

   //this function is called on a timer and runs on a background thread
   private void RefreshData()
   {
      List<string> newData = ACallToAService();
      _data = newData.AsReadOnly();
      OnPropertyChanged("Data"); // yes, this dispatches the to UI thread
   }
}

具体地说,我知道我可以使用一个锁,甚至是一个联锁的。交换,但我不认为在这种情况下有必要。volatile关键字应该足以确保该值未被缓存,不是吗?请有人确认一下,或者让我知道我对线程的不理解:

这取决于意图是什么。即使没有易失性和非缓存易失性,列表的get/set也是原子的,但是调用方可以改变列表,这不能保证线程安全

还存在可能丢失数据的竞争条件:

 obj.Data.Add(value);
在这里,价值很容易被丢弃


我会使用不可变的只读集合。

这取决于目的是什么。即使没有易失性和非缓存易失性,列表的get/set也是原子的,但是调用方可以改变列表,这不能保证线程安全

还存在可能丢失数据的竞争条件:

 obj.Data.Add(value);
在这里,价值很容易被丢弃


我会使用一个不可变的只读集合。

我认为,如果您只有两个像您描述的那样的线程,那么您的代码是正确和安全的。而且你也不需要那种挥发性,它在这里是无用的


但请不要称之为线程安全,因为只有使用它的两个线程才是安全的。

我认为,如果您只有两个像您描述的那样的线程,那么您的代码是正确和安全的。而且你也不需要那种挥发性,它在这里是无用的


但请不要称之为线程安全,因为只有使用它的两个线程才是安全的。我不知道这是否安全;这完全取决于你所说的安全。例如,如果将“安全”定义为保证所有线程都能观察到所有易失性写操作的一致顺序,则不能保证程序在所有硬件上都是安全的

这里的最佳实践是使用锁,除非你有非常好的理由不这样做。您编写此高风险代码的最佳理由是什么

更新:我的观点是低锁或无锁代码是非常危险的,世界上只有少数人真正理解它。让我举一个乔·达菲的例子:

// deeply broken, do not use!
class Singleton {
    private static object slock = new object();
    private static Singleton instance;
    private static bool initialized;
    private Singleton() {}
    public Instance {
        get {
            if (!initialized) {
                lock (slock) {
                    if (!initialized) {
                        instance = new Singleton();
                        initialized = true;
                    }
                }
            }
            return instance;
        }
    }
}
这个密码被破坏了;对于C编译器的正确实现来说,为实例编写一个返回null的程序是完全合法的。你知道怎么做吗?如果没有,那么您就没有必要进行低锁或无锁编程;你会弄错的


我自己搞不懂这些东西;它伤了我的大脑。这就是为什么我尽量不做低锁编程,这与专家分析过的标准实践有任何不同

我不知道这是否安全;这完全取决于你所说的安全。例如,如果将“安全”定义为保证所有线程都能观察到所有易失性写操作的一致顺序,则不能保证程序在所有硬件上都是安全的

这里的最佳实践是使用锁,除非你有非常好的理由不这样做。您编写此高风险代码的最佳理由是什么

更新:我的观点是低锁或无锁代码是非常危险的,世界上只有少数人真正理解它。让我举一个乔·达菲的例子:

// deeply broken, do not use!
class Singleton {
    private static object slock = new object();
    private static Singleton instance;
    private static bool initialized;
    private Singleton() {}
    public Instance {
        get {
            if (!initialized) {
                lock (slock) {
                    if (!initialized) {
                        instance = new Singleton();
                        initialized = true;
                    }
                }
            }
            return instance;
        }
    }
}
这个密码被破坏了;对于C编译器的正确实现来说,为实例编写一个返回null的程序是完全合法的。你知道怎么做吗?如果没有,那么您就没有必要进行低锁或无锁编程;你会弄错的


我自己搞不懂这些东西;它伤了我的大脑。这就是为什么我尽量不做低锁编程,这与专家分析过的标准实践有任何不同

我相信即使没有volatile,这本身也是安全的,但是可能会有问题,这取决于其他线程如何使用数据属性

如果您可以保证所有其他线程在对数据执行枚举之前读取和缓存数据值一次,并且不尝试将其强制转换到某个更广泛的接口以执行其他操作,并且对第二次访问属性没有一致性假设,那么您应该可以。如果你不能做出保证,而且如果其中一个用户是通过数据绑定的框架本身,因此是你无法控制的代码,那么就很难做出保证,那么你就不能说它是安全的

例如,这将是安全的:

foreach (var item in x.Data)
{
   // do something with item
}
如果不允许JIT优化本地资源,这将是安全的,我认为是这样的:

var data = x.Data;
var item1 = FindItem(data, a);
var item2 = FindItem(data, b);
DoSomething(item1, item2);
上述两种方法可能会作用于过时的数据,但它始终是一致的数据。但这并不一定是安全的:

var item1 = FindItem(x.Data, a);
var item2 = FindItem(x.Data, b);
DoSomething(item1, item2);
这个可以 d可能在某个线程替换集合之前和之后搜索集合的两种不同状态,因此对每个单独枚举中找到的项进行操作可能不安全,因为它们可能彼此不一致


如果有更广泛的接口,问题会更糟;例如,如果数据暴露在IList中,您还必须注意计数和索引器操作的一致性。

我相信即使没有volatile,这本身也是安全的,但是可能会有问题,这取决于其他线程如何使用数据属性

如果您可以保证所有其他线程在对数据执行枚举之前读取和缓存数据值一次,并且不尝试将其强制转换到某个更广泛的接口以执行其他操作,并且对第二次访问属性没有一致性假设,那么您应该可以。如果你不能做出保证,而且如果其中一个用户是通过数据绑定的框架本身,因此是你无法控制的代码,那么就很难做出保证,那么你就不能说它是安全的

例如,这将是安全的:

foreach (var item in x.Data)
{
   // do something with item
}
如果不允许JIT优化本地资源,这将是安全的,我认为是这样的:

var data = x.Data;
var item1 = FindItem(data, a);
var item2 = FindItem(data, b);
DoSomething(item1, item2);
上述两种方法可能会作用于过时的数据,但它始终是一致的数据。但这并不一定是安全的:

var item1 = FindItem(x.Data, a);
var item2 = FindItem(x.Data, b);
DoSomething(item1, item2);
这可能是在某个线程替换集合之前和之后搜索集合的两种不同状态,因此对每个单独枚举中找到的项进行操作可能不安全,因为它们可能彼此不一致



如果有更广泛的接口,问题会更糟;如果数据暴露在IList中,您还必须注意计数和索引器操作的一致性。

无锁如何表示线程安全?锁的作用不在于强制执行同步访问并使操作不受多线程的影响吗?@StingyJack-Lock-Free简单地暗示了一种不需要锁的线程安全算法。锁使访问同步,但也可能导致性能或死锁问题,因此尽可能避免它们总是很好的。@Eric Petroleje-在第一部分,我指出它们不一定相关。第二,除了语法简洁之外,我不认为消除锁{}更好。@StingyJack实际上有几个线程读取这些数据,其中一个线程对性能至关重要。使用锁{}会导致性能问题,如果可能的话,我想消除这些问题。@StingyJack无锁编程很少在并发编程环境之外使用-我想说它们是密切相关的。通常,没有人为了语法简洁而避免锁。。。如果这是人们应该避免锁的主要原因,那么您就错过了并发编程的全部要点。无锁如何意味着线程安全?锁的作用不在于强制执行同步访问并使操作不受多线程的影响吗?@StingyJack-Lock-Free简单地暗示了一种不需要锁的线程安全算法。锁使访问同步,但也可能导致性能或死锁问题,因此尽可能避免它们总是很好的。@Eric Petroleje-在第一部分,我指出它们不一定相关。第二,除了语法简洁之外,我不认为消除锁{}更好。@StingyJack实际上有几个线程读取这些数据,其中一个线程对性能至关重要。使用锁{}会导致性能问题,如果可能的话,我想消除这些问题。@StingyJack无锁编程很少在并发编程环境之外使用-我想说它们是密切相关的。通常,没有人为了语法简洁而避免锁。。。如果这是一个人应该避免锁的主要原因,那么你就错过了并发编程的全部要点。我认为他只在UI线程中读取集合,所以它是安全的。这一点很好。实际上,我打算使用IEnumerable而不是List作为属性类型,但是将集合设为只读会更好。这样做的目的是类的使用者不会编辑数据。@AlKepp如果所有的访问都转到UI线程,那么整个问题就解决了moot@herbrandson-使用AsReadOnly看起来更快乐。我认为他只在UI线程中阅读收藏,因此它是安全的。这一点很好。实际上,我打算使用IEnumerable而不是List作为属性类型,但是将集合设为只读会更好。这样做的目的是类的使用者不会编辑数据。@AlKepp如果所有的访问都转到UI线程,那么整个问题就解决了moot@herbrandson-使用AsReadOnly,它看起来更快乐。为什么不称之为线程安全?只有内部线程有能力更改数据,任何数量的线程都可以使用它。请注意,我只更新了一点代码,使数据属性为只读。volatile是一个奇怪的野兽;如果目的是防止寄存器缓存
,那么我认为当它无用的时候说它是不平凡的。举一个例子来说明它的重要性是很容易的,但反过来就不那么容易了。一个例子可以用来反驳,但不能用来证明。想解释一下为什么这是无用的吗?@Marc:AFAIK C有顺序写操作,没有全局优化,所以这段特定的代码是安全的,不管它是否易失性。为什么它不被称为线程安全的?只有内部线程有能力更改数据,任何数量的线程都可以使用它。请注意,我只更新了一点代码,使数据属性为只读。volatile是一个奇怪的野兽;如果其目的是防止寄存器缓存,那么我认为说什么时候它是无用的是很重要的。举一个例子来说明它的重要性是很容易的,但反过来就不那么容易了。一个例子可以用来反驳,但不能用来证明。想解释一下为什么这没用吗?@Marc:AFAIK C有顺序写入,没有全局优化,所以这段特定的代码是安全的,不受易失性的影响。谢谢你的反馈。实际上有多个线程访问数据。目标是消除数据的资源争用。请帮助我理解这一点-如果您将“安全”定义为“保证从所有线程观察到所有易失性写入的一致顺序”,那么您的程序就不能保证在所有硬件上都是安全的。如果数据对于N个使用者是只读的,并且该类只有一个内部线程写入数据,那么是什么使其不安全?我并不是说它是安全的,我只是想理解为什么它不是。@herbrandson:好吧,你是否将安全定义为所有线程都能保证遵守所有易失性写操作的一致顺序?如果您这样做,那么您的程序就不能保证是安全的,因为C语言不提供这种保证。如果您对安全的定义包括允许从读取线程中观察到任何不一致的易失性写入顺序,那么我们仍然不知道它是否安全,因为您还没有说明安全的含义。@herbrandson:现在,如果您的目标是减少争用,那么使用无锁解决方案是危险的。我首先要集中精力弄清楚你为什么有这么多争论。如果你有很多争用,因为你有很多频繁的读者,只有一个不频繁的作者,那么消除争用的方法肯定是使用ReaderWriterLockSlim,不是吗?我不熟悉ReaderWriterLockSlim。谢谢你!但是,在这种情况下,ReaderWriterLockSlim会比Interlocked.Exchange更好吗?如果是,为什么?@herbrandson:谁在乎只有一个线程在写呢?这完全无关。现在的问题是:是否保证从所有其他线程观察到的所有写操作的顺序一致?对此的答案是,规范明确指出,它没有做出这样的保证。如果您对安全的定义包括从所有其他线程看到的所有volatile写入的顺序是一致的,那么使用volatile是不安全的。我不知道我可以用多少种不同的方式来表达。谢谢你的反馈。实际上有多个线程访问数据。目标是消除数据的资源争用。请帮助我理解这一点-如果您将“安全”定义为“保证从所有线程观察到所有易失性写入的一致顺序”,那么您的程序就不能保证在所有硬件上都是安全的。如果数据对于N个使用者是只读的,并且该类只有一个内部线程写入数据,那么是什么使其不安全?我并不是说它是安全的,我只是想理解为什么它不是。@herbrandson:好吧,你是否将安全定义为所有线程都能保证遵守所有易失性写操作的一致顺序?如果您这样做,那么您的程序就不能保证是安全的,因为C语言不提供这种保证。如果您对安全的定义包括允许从读取线程中观察到任何不一致的易失性写入顺序,那么我们仍然不知道它是否安全,因为您还没有说明安全的含义。@herbrandson:现在,如果您的目标是减少争用,那么使用无锁解决方案是危险的。我首先要集中精力弄清楚你为什么有这么多争论。如果你有很多争用,因为你有很多频繁的读者,只有一个不频繁的作者,那么消除争用的方法肯定是使用ReaderWriterLockSlim,不是吗?我不熟悉ReaderWriterLockSlim。谢谢你!但是,在这种情况下,ReaderWriterLockSlim会比Interlocked.Exchange更好吗?如果是,为什么?@herbrandson:谁在乎只有一个线程在写呢?这完全无关。现在的问题是:是否保证所有观察到的写入都有一致的顺序
从所有其他线程?对此的答案是,规范明确指出,它没有做出这样的保证。如果您对安全的定义包括从所有其他线程看到的所有volatile写入的顺序是一致的,那么使用volatile是不安全的。我不知道有多少种不同的方式可以这么说。我同意,但如果使用了锁,不会存在同样的问题吗?是的,但如果使用了锁并公开公开,那么锁可以缠绕在整个第三个街区上,使其再次安全。这种事情甚至可以由第三方代码通过SyncRoot属性自动完成。除了向所有用户引入锁,或者能够重写代码以匹配第二个示例之外,没有其他方法可以与无锁变体进行类似的操作。我同意,但是如果使用锁,不会存在同样的问题吗?是的,但是,如果使用了锁并将其公开,那么可以将锁包裹在整个第三个街区,使其再次安全。这种事情甚至可以由第三方代码通过SyncRoot属性自动完成。除了向所有用户引入锁,或者能够重写代码以匹配第二个示例之外,没有任何方法可以对无锁变体执行类似的操作。