多个线程同时操作一个数据的话,可能会发生数据的错误。这个时候就需要进行线程同步了。线程同步可以使用多种方法来进行。下面来逐一说明。本文参考了《CLR via C#》中关于线程同步的很多内容。
用户模式同步
易变构造
当对32位及32位以下变量的读写时,CLR保证读写操作是原子性的。也就是说bool、char、int等类型的变量可以一次性读取或者写入。但是对于long、ulong这些64位数据类型来说,就有可能不是原子操作。此外,由于编译器优化的存在,这些语句执行的顺序可能和编写代码时的顺序不同。这样的话,在多线程的环境下就有可能会出现同步问题。鉴于此,FCL提供了Volatile类,用来控制变量的读写和编译器的优化,这样的访问称为易变访问。
Volatile是一个静态类,包含了对于各种基元数据类型已经泛型类型的Write和Read方法。使用这些方法,可以做到:
- 禁止编译器进行任何优化,对变量进行原子操作,只有在调用读写方法的时候才将值读取或写入。
- 插入内存屏障,按照编码顺序,Write方法之前的存取操作必须在调用Write方法之前完成,Read方法之后的存取操作必须在调用Read方法之后完成。
这里有一个例子,在使用发布模式运行(非调试)程序的时候,代码行为会发生变化,导致死循环。原因如下:编译器发现在Work方法中,continue的值并没有发生改变,所以会将代码优化为在while循环之前求值,然后每次循环直接使用这个值。所以,优化后的代码while循环会直接变成死循环。从而导致程序出现问题。这也告诫我们:在多线程的环境下,对于程序应该引起足够的重视,有可能出现一些正式发布时才会出现的问题。
//当启用发布模式时,该类的行为会发生变化
//由于优化导致程序进入死循环
static class ProblemA
{
//添加volatile关键字即可保证正确
static bool continued = true;
public static void ShowProblem()
{
Console.WriteLine("运行3秒之后停止工作:");
Thread t = new Thread(Work);
t.Start();
Thread.Sleep(3000);
continued = false;
Console.WriteLine("等待工作结束...");
t.Join();
Console.WriteLine("--------------------------------");
}
private static void Work(object state)
{
int n = 0;
//优化会导致这里变为死循环
while (continued)
{
n++;
}
Console.WriteLine("循环结束时n的值是" + n);
}
}
C#同时还提供了volatile
关键字,标记为volatile
的变量,对其的所有操作都是易变操作。但是由于只有少数情况下才需要进行易变读写,直接标记volatile关键字会影响性能。所以还是推荐使用Volatile静态类,只有在需要的时候才进行易变操作。
互锁构造
Volatile类只有Write和Read两个方法。而Interlocked类增加了更多的方法,并且所有方法都是原子操作并提供内存屏障。
下面的例子说明了Interlocked类的主要方法。Interlocked类在多线程环境下十分有用。注意Exchange和CompareExchange方法返回的都是交换之前的值。
public static void InterlockTest()
{
int i = 5;
Console.WriteLine("演示Interlocked的方法:");
Console.WriteLine("增加一个值:");
Console.WriteLine("增加之后的值:" + Interlocked.Increment(ref i));
Console.WriteLine("减少一个值:");
Console.WriteLine("减少之后的值:" + Interlocked.Decrement(ref i));
Console.WriteLine("用一个值交换:");