C#如何保证读/写操作的原子性?
C#如何保证读/写操作的原子性?
C#规范在第5.5节中指出,对于特定类型(包括bool、char、byte、sbyte、short、ushort、uint、int、float和引用类型),读写操作是保证原子性的。
这引起了我的兴趣。你是怎么做到的呢?我的个人经验告诉我,如果我想要读写操作具有原子性,就要锁定变量或者使用屏障;但如果每个读写操作都要这样做,那将会严重影响性能。然而,C#似乎做了一些类似的事情。
也许其他语言(比如Java)也可以做到。我真的不知道。我的问题并不是特定于某种语言,只是我知道C#可以做到。
我理解这可能与特定的处理器指令有关,并且可能无法在C/C++中使用。然而,我仍然想知道它是如何工作的。
[编辑] 说实话,我曾经认为在某些情况下,读写操作可能不是原子的,比如当一个CPU在写入内存位置时,另一个CPU可能正在访问那里。这只发生在CPU无法一次处理整个对象时才会发生,比如对象太大或者内存未对齐在正确边界上。
C# 如何保证读写操作的原子性?
C# 保证读写操作的原子性是因为这些类型的大小都是32位或更小。由于.NET只在32位和64位操作系统上运行,处理器架构可以在一个操作中读取和写入整个值。这与在32位平台上使用两个32位操作读取和写入Int64不同。
我不是一个硬件专家,所以如果我的术语让我听起来像个傻瓜,我向您道歉,但这是基本思想。
唉,我总觉得在另一个CPU的写操作期间,另一个线程可能访问内存位置,导致不一致的结果。那么我一直错了吗?
除了原子读/写之外,还有许多其他问题。两个操作相同变量的线程很可能会导致不希望的结果,因为它们经常进行读取/修改/写入操作,这不是原子操作。在多处理器机器上还存在内存可见性问题,需要特别注意。
我理解这一点。但这不是我现在关心的问题。:) 它只是涉及读取和写入操作。
C#中如何保证读写操作的原子性?
C#中保证读写操作的原子性相对廉价,因为CLR只保证32位或更小变量的原子性。只需要确保变量正确对齐,不跨越缓存行即可实现。JIT编译器通过在4字节对齐的堆栈偏移上分配局部变量来实现这一点。GC堆管理器对堆分配也是如此。
值得注意的是,CLR的保证并不是很好。对于双精度数组,仅仅依靠对齐承诺无法编写一致高效的代码,这在这个线程中有很好的展示。由于这个原因,与使用SIMD指令的机器代码的互操作也非常困难。
值得思考的是,内存分配器为解决双精度数组的对齐问题而将它们强制放在大对象堆(LOH)上,而不是例如在GC分配或复制32个或更大的double[]
时,如果下一个可用空间不是64位对齐的,首先分配并丢弃一个12字节的虚拟对象。5%的内存浪费应该比将事物强制放在LOH上的成本更低。
C#中如何保证读写操作的原子性?
在x86架构中,读写操作本身就是原子的。这是在硬件层面上支持的。然而,这并不意味着诸如加法和乘法等操作是原子的;它们需要进行加载、计算和存储,这意味着它们可能会相互干扰。这就是使用锁定前缀的原因。
你提到了锁定和内存屏障;它们与读写操作的原子性没有任何关系。在x86架构中,无论是否使用内存屏障,你都不会看到半写的32位值。
是的,我知道乘法等操作不是原子的。我从来没有对此产生过困惑。我有些误以为一个CPU在另一个CPU写入内存的同时可以访问该内存,这可能导致读取时出现不一致的结果。这种情况可能发生吗?如果可能,我能做些什么来防止它发生?
在C#中,为了确保读写操作的原子性,可以使用互斥锁(Mutex)或者使用volatile关键字修饰变量。互斥锁可以用于保护共享资源,确保在任何给定时刻只有一个线程可以访问该资源。而使用volatile关键字修饰的变量可以确保对该变量的读写操作是原子的,即不会被中断或干扰。
以下是使用互斥锁和volatile关键字的示例代码:
using System;
using System.Threading;
public class Program
{
private static int sharedValue = 0;
private static Mutex mutex = new Mutex();
public static void Main(string[] args)
{
Thread thread1 = new Thread(IncrementSharedValue);
Thread thread2 = new Thread(IncrementSharedValue);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine("Final shared value: " + sharedValue);
}
private static void IncrementSharedValue()
{
for (int i = 0; i < 100000; i++)
{
mutex.WaitOne();
sharedValue++;
mutex.ReleaseMutex();
}
}
}
在上述代码中,使用了互斥锁来保护对sharedValue变量的读写操作。在每次访问sharedValue之前,线程会先获取互斥锁,并在访问完成后释放锁。这样可以确保每次对sharedValue的读写操作都是原子的,不会被其他线程干扰。
另外,如果不使用互斥锁,而是使用volatile关键字修饰sharedValue变量,也可以达到相同的效果。使用volatile关键字修饰的变量在进行读写操作时,会保证每次都是从内存中读取或写入,而不会使用缓存。这样可以避免多线程环境下的数据不一致问题。
总结起来,C#通过使用互斥锁或volatile关键字来保证读写操作的原子性。互斥锁可以用于保护共享资源,而volatile关键字可以确保对变量的读写操作是原子的,不会被中断或干扰。这样可以避免多线程环境下的数据竞争和不一致问题。