在多线程编程中,对共享资源的访问需要特别小心,以避免数据不一致的问题。.NET框架提供了一个名为Interlocked
的类,用于在多线程环境下对共享变量执行原子操作。原子操作是指在执行过程中不会被操作系统中断的操作。这意味着,当一个线程正在执行一个原子操作时,其他线程不能中断它。
考虑以下代码,它在多线程环境中递增共享变量X
:
int X = 0;
X = X + 1;
从编程的角度来看,X = X + 1
是一个原子操作。但是,计算机在内部执行这条指令时,需要进行更多的操作。可以将上述指令分解为三个步骤:
X
的值移动到CPU寄存器。假设有两个线程同时执行相同的代码。假设第一个线程处于第二步,即X
的值已经递增到1。此时,操作系统停止了第一个线程。第二个线程也在并行运行。第二个线程完成了所有三个步骤,并更新了X
的值为1。现在假设第一个线程恢复其操作并执行第三步。在这一步,X
的值从0更新为1,这是旧值被更新。这意味着执行上述代码后,X
的值将是1,这是不正确的。X
的值应该是2。
通过使用Interlocked
类,可以解决这个问题,并得到预期的值,即2。Interlocked
类提供了一个简单的线程安全方法Interlocked.Increment(ref variable)
,它确保操作系统不会中断递增操作。
在多线程环境中,对共享变量进行递增、递减和交换操作是非常常见的。如果没有同步,可能无法得到预期的结果。但是通过线程同步,可以得到预期的结果。在下面的部分中,描述了两种情况下的实际实现。
以下代码在没有任何同步的情况下,通过多个线程递增共享变量:
int Number = 0;
private void btnWithSync_Click(object sender, EventArgs e)
{
int TotalThread = 10000;
Thread[] IntreLockThread = new Thread[TotalThread];
for (int i = 0; i < TotalThread; i++)
{
IntreLockThread[i] = new Thread(new ThreadStart(UpdateValue));
IntreLockThread[i].IsBackground = true;
IntreLockThread[i].Priority = ThreadPriority.Lowest;
IntreLockThread[i].Start();
}
for (int i = 0; i < TotalThread; i++)
{
// Block main thread until all child thread to finish.
IntreLockThread[i].Join();
}
// Show the current value of Number after incremented by all thread
lblTotalValue.Text = Number.ToString();
}
在上述代码中,连续运行了10000个线程来递增共享变量。但是,当所有线程执行完成后,变量的值应该是10000。但是,有时并不是这样。有时,值小于10000(在上次运行时,值为9995)。这是由于线程之间的上下文切换。操作系统可以在递增的中间停止当前执行的线程,然后继续执行操作。这将用旧值替换变量的值。因此,这种方式的递增操作不是原子的。可以使用Interlocked
类来解决这个问题。
以下代码通过Interlocked
类的帮助,通过多个线程递增共享变量。Interlocked
类的Increment
方法用于此目的。
int Number = 0;
private void brnWithSync_Click(object sender, EventArgs e)
{
Number = 0;
int TotalThread = 10000;
Thread[] IntreLockThread = new Thread[TotalThread];
for (int i = 0; i < TotalThread; i++)
{
IntreLockThread[i] = new Thread(new ThreadStart(UpdateWithInterlock));
IntreLockThread[i].IsBackground = true;
IntreLockThread[i].Start();
}
for (int i = 0; i < TotalThread; i++)
{
// Block main thread until all child thread to finish.
IntreLockThread[i].Join();
}
// Show the current value of Number after incremented by all thread
lblTotalValueSync.Text = Number.ToString();
}
与之前一样,连续运行了10000个线程来递增共享变量。执行结束后,得到了预期的结果。变量的值递增到了10000。因此,这种方式的递增操作是原子的,并且完全同步。
Interlocked
类除了Increment
之外,还支持更多操作。这个类的所有方法都接受第一个参数作为引用类型,以便在操作完成后获取更新后的值。这个类的所有非泛型方法都有重载版本。这里只描述了一个版本。以下是Interlocked
类的不同线程安全方法:
Interlocked.Add(ref int intNumber, int value);
This method adds the value of two parameters and replaces the first one with the sum of both values as an atomic operation.
Interlocked.Increment(ref int intNumber);
This method increments the value by 1 and assigns the updated value to itself as an atomic operation.
Interlocked.Decrement(ref int intNumber);
This method decrement the value by 1 and assigns the updated value to itself as an atomic operation.
Interlocked.Read(ref int intNumber);
It returns the value of variable specified in parameter.
Interlocked.Exchange(ref intNumber1, int intNumber2);
The exchange method assigns the value of first parameter with the value of second parameter as an atomic operation. It’s basically a set operation. Someone can think why we should use this function as the new value doesn’t depend on the old one. But it's necessary multi-processor or multi-core machine environment.
Interlocked.Exchange(ref T Location, T value)
This method is a generic version of exchange method. The functionality is the same as Exchange method. Here it must be reference type.
Interlocked.CompareExchange(ref int intNumber1, int intNumber2, int CompareValue);
CompareExchange is a conditional assignment method. The first parameter is compared with the last parameter. If both are equal, then the value of the first parameter is replaced by the second parameter.
Interlocked.CompareExchange(ref T Location, T Value, T CompareValue);
This method is a generic version of CompareExchange method. The functionality is the same as Exchange method. Here T must be reference type.
要测试上述代码,请运行应用程序。在点击“无同步运行”按钮运行代码时,请尝试通过打开一个新的不同的应用程序来使CPU保持忙碌,以便发生上下文切换。要使用Interlocked
类运行同步代码,请单击“同步运行”,并获取实际结果作为预期结果。