什么是线程安全(C#)?(字符串,数组等?)

24

我对C#还相当陌生,请耐心等待。我有点困惑线程安全性。什么情况下是线程安全的,什么情况下不是?

从字段中读取(只是从之前初始化的某个东西中读取)是否总是线程安全的?

//EXAMPLE
RSACryptoServiceProvider rsa = new RSACrytoServiceProvider();
rsa.FromXmlString(xmlString);  
//Is this thread safe if xml String is predifined 
//and this code can be called from multiple threads?

如果使用for循环枚举数组或列表,访问对象是否总是线程安全的?

//EXAMPLE (a is local to thread, array and list are global)
int a = 0;
for(int i=0; i<10; i++)
{
  a += array[i];
  a -= list.ElementAt(i);
}

枚举类型(enumeration)是否总是/曾经是线程安全的?

//EXAMPLE
foreach(Object o in list)
{
   //do something with o
 }

在特定字段进行写入和读取是否可能导致读取错误(一半字段被更改,一半仍然不变)?

感谢您的回答和时间。

编辑:我的意思是如果所有线程只读取并使用对象(不写入或更改)。 (除了最后一个问题,在那里很明显我指的是线程既读又写)。 因为我不知道普通访问或枚举是否线程安全。

5个回答

26

不同的情况下有不同的答案,但通常来说,只要所有线程都在读取数据,那么读取数据是安全的。如果有任何线程正在写入数据,则无论是读取还是写入都不安全,除非可以原子地执行(在同步块中或使用原子类型)。

并不能确定读取数据是安全的——你永远不知道底层发生了什么情况。例如,getter 方法可能需要在第一次使用时初始化数据(因此会写入本地字段)。

对于字符串,你很幸运——它们是不可变的,所以你只能读取它们。对于其他类型,你需要在读取时采取预防措施,以防止其在其他线程中被修改。


13
从一个字段中读取(只是从之前初始化的某个东西中读取)是否总是线程安全的?
C#语言保证,当读写在单线程上时,读和写始终按照一致的顺序进行,即在第3.10节中:
数据依赖性在执行线程内得到保留。也就是说,每个变量的值都会被计算,好像线程中的所有语句都按照原始程序顺序执行。初始化顺序规则得到保留。
多线程、多处理器系统中的事件并不一定在时间上与彼此具有良好定义的一致顺序。C#语言不保证存在一致的排序。当从另一个线程中观察到一个写序列时,可能会观察到完全不同的顺序,只要不涉及关键的执行点。
因此,这个问题是无法回答的,因为它包含一个未定义的单词。你能否给出一个关于多线程、多处理器系统中事件“之前”意味着什么的精确定义?
该语言仅保证副作用按照关键执行点的顺序排序,并且即使在异常涉及的情况下也不会做出任何强烈的保证。再次引用第3.10节:
C#程序的执行是这样进行的:每个执行线程的副作用在关键执行点处得到保留。副作用定义为易失性字段的读取或写入、非易失性变量的写入、外部资源的写入和异常的抛出。必须在其中保持这些副作用的顺序的关键执行点是易失性字段的引用、锁定语句以及线程的创建和终止。副作用的排序与易失性读取和写入有关。此外,执行环境在推断某个表达式的值没有被使用且不存在任何必需的副作用时(包括通过调用方法或访问易失性字段产生的副作用),可以不评估该表达式的一部分。当异步事件(例如另一个线程抛出的异常)中断程序执行时,不能保证可观察到的副作用在原始程序顺序中可见。
“从数组或列表访问对象”是否总是线程安全的(如果您使用for循环进行枚举)?
通过“线程安全”,您是否意味着两个线程在读取列表时将始终观察到一致的结果?如上所述,C#语言在读取变量时对结果的观察做出了非常有限的保证。您是否能给出关于非易失性读取的“线程安全”的精确定义?
枚举总是/从不是线程安全的吗?
即使在单线程场景中,在枚举集合时修改集合是不合法的。在多线程场景中这样做肯定是不安全的。
在特定字段上写入和读取数据是否可能导致已损坏的读取(一半的字段已更改,另一半仍未更改)?
是的。我参考第5.5节,其中指出:
逻辑类型bool、char、byte、sbyte、short、ushort、uint、int、float和引用类型的读取和写入是原子的。此外,对于具有基础类型在前述列表中的枚举类型的读取和写入也是原子的。其他类型的读取和写入,包括long、ulong、double和decimal以及用户定义的类型,没有保证是原子的。除了专门设计用于此目的的库函数外,在执行类似增量或减量的原子读-改-写时没有保证。

“Before” 的意思是在线程启动之前已经初始化了某些内容。而 “线程安全” 指的是多个线程是否可以枚举和/或迭代相同的对象(数组、列表等 <-它们在“之前”被初始化和填充),并且只读取它们而不会出现问题? - Ben
@Ben:如果在其中有一个线程启动,则存在关键执行点,因此可观察的副作用与该关键点的顺序是有保证的。 - Eric Lippert
1
64位架构上的长整型/无符号长整型读写操作不能保证是原子性的。 - James Dunne
@James:语言规范对64位硬件没有任何限制。它仅说明为了符合实现,必须保证32位变量和引用大小的变量是原子性的。如果你恰好在使用能提供更强保证的硬件上运行C#程序,那么这些保证是由硬件厂商提供的,而不是由友好的C#编译器提供者提供的。我不会代表未知的硬件制造商做出任何声明。 - Eric Lippert
同样地,x86内存模型比我上面概述的内存模型要强得多。x86硬件供应商承诺可以实现这一点;而C#语言肯定不会为x86硬件供应商做出承诺。如果你选择依靠硬件供应商向你作出的承诺,那是你的事情。 - Eric Lippert
此外,CLI规范确保了所有数据结构的原子性,如果它们是本机字大小或更小,并且它们的地址与字边界对齐。 (它承诺您不会意外地出现不对齐问题; 您必须请求它。)但是,C#语言可以被实现在除CLI实现之外的其他平台上运行。 - Eric Lippert

4

我通常认为所有东西都是线程不安全的。在多线程环境下,为了快速访问全局对象,我使用lock(object)关键字。.Net有一套广泛的同步方法,如不同的信号量等。


3

如果有写线程存在,读取操作可能不安全(例如在读取过程中进行写入,将导致异常)。

如果确实需要这样做,可以采用以下方式:

lock(i){
    i.GetElementAt(a)
}

这将强制使i线程安全(只要其他线程在使用它之前尝试锁定i。 只有一件事情可以同时锁定引用类型。

就枚举而言,我将参考MSDN:

The enumerator does not have exclusive access to the collection; therefore, enumerating 
through a collection is intrinsically not a thread-safe procedure. To guarantee thread 
safety during enumeration, you can lock the collection during the entire enumeration. To 
allow the collection to be accessed by multiple threads for reading and writing, you must     
implement your own synchronization.

0

线程不安全的示例:当几个线程增加一个整数时。你可以设置它以一种方式,使你有一个预定数量的增量。但是你可能观察到的是,该整数并没有增加你认为它会增加的那么多。发生的情况是两个线程可能会增加整数的相同值。这只是你在使用几个线程时可能会遇到的许多效果中的一个示例。

附注:

可通过Interlocked.Increment(ref i)实现线程安全递增。


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