65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Interlocked 类进行线程同步

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (4投票s)

2011年11月22日

CPOL

5分钟阅读

viewsIcon

47323

downloadIcon

903

.NET Framework 中用于线程同步的 Interlocked 类的入门介绍

image001.jpg

引言

本文介绍了 .NET Framework 中存在的 Interlocked 类的线程同步技术。Interlocked 类对共享变量提供原子操作。操作系统不会在其执行过程中间断其操作。

Interlocked 类为不同的操作提供了不同的线程安全方法,但对数据类型的支持有限。当我们需要在多个线程之间对共享变量进行增、减、交换等操作时,就会使用此类。

无同步时的并发问题

考虑下面这行代码,它在多线程环境中会增加共享变量 X。

int X=0;
X=X+1;

从编程的角度来看,`X=X+1` 这行代码是一个原子操作。但实际上,计算机需要执行更多步骤才能完成这个操作。我们可以将上面这行代码分解为三个操作:

  • 将 X 的值移到 CPU 寄存器。
  • 增加寄存器中的值。
  • 将值写回内存。

假设有两个线程在执行相同的代码。想象一下,第一个线程正处于第二步,即 X 的值已经增加了 1。此时,操作系统会停止第一个线程。第二个线程也在并行运行。第二个线程完成了所有三个步骤,并将 X 的值更新为 1。现在假设第一个线程恢复了它的操作并执行了第三步。在这一步,X 的值被更新为 1,这是基于旧值 0 进行的更新。这意味着在执行完这行代码后,X 的值将是 1,这是不正确的。X 的值应该是 2。

分辨率

通过使用 Interlocked 类,我们可以解决这个问题并获得预期的值 2。Interlocked 类提供了一个简单的线程安全方法 `Interlocked.Increment(ref variable)` 用于增操作。它确保操作系统不会中断增操作。

Using the Code

在多线程环境中,开发者经常需要对共享变量进行增、减和交换操作。没有同步,我们可能无法获得预期的结果。但有了线程同步,我们就能获得预期的结果。在接下来的部分,我将描述两种场景下的实际实现。

无同步的共享变量增操作

以下代码在没有进行任何同步的情况下,由多个线程增加共享变量。

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();
}
 
///Description : Increment the Number variable and Update Listboxwith current value.
public void UpdateValue()
{
           listBox1.BeginInvoke(new ParameterizedThreadStart(UpdateListBox1), 
		"Thread ID : " +
           Thread.CurrentThread.ManagedThreadId + " - B:: =" + Number.ToString());
           Number++; // Increment without synchronization
           listBox1.BeginInvoke(new ParameterizedThreadStart(UpdateListBox1), 
		"Thread ID : " +
           Thread.CurrentThread.ManagedThreadId + " - A:: =" + Number.ToString());
}
public void UpdateListBox1(object objResult)
{
            listBox1.Items.Add(objResult.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();
}
 
///Description : Increment the Number variable and Update Listboxwith current value.
public void UpdateWithInterlock()
{
            listBox2.BeginInvoke(new ParameterizedThreadStart(UpdateListBox2), 
		"Thread ID : " +
            Thread.CurrentThread.ManagedThreadId + " - B:: =" + Number.ToString());
            Interlocked.Increment(ref Number); //Increment with interlock class.
            listBox2.BeginInvoke(new ParameterizedThreadStart(UpdateListBox2), 
		"Thread ID : " +
            Thread.CurrentThread.ManagedThreadId + " - A:: =" + Number.ToString());
}
 
//Show the current value of Number after incremented by all thread
public void UpdateListBox2(object objResult)
{
            listBox2.Items.Add(objResult.ToString());
}

与之前一样,我连续运行了 10000 个线程来增加共享变量。在执行结束时,我得到了预期的结果。变量的值增加了 10000。因此,这种方式的增操作是原子的,并且完全同步。

Interlocked 类除了 Increment 还支持更多操作。此类所有方法的第一个参数都是引用类型,以便我们在操作完成后可以获取更新后的值。此类中所有非泛型方法都有重载版本。这里我只描述了一种版本。

以下是 Interlocked 类的各种线程安全方法:

Interlocked.Add(ref int intNumber,int value);

此方法将两个参数的值相加,并将第一个参数替换为两者之和,这是一个原子操作。

Interlocked.Increment(ref int intNumber);

此方法将值增加 1,并将更新后的值自分配给自己,这是一个原子操作。

Interlocked.Decrement(ref int intNumber);

此方法将值减少 1,并将更新后的值自分配给自己,这是一个原子操作。

Interlocked.Read(ref int intNumber);

它返回参数中指定的变量的值。

Interlocked.Exchange(ref intNumber1, int intNumber2);

Exchange 方法以原子操作的方式将第一个参数的值替换为第二个参数的值。它本质上是一个设置操作。有人可能会想,为什么我们要使用这个函数,因为新值不依赖于旧值。但在多处理器或多核机器环境中,这是必要的。

Interlocked.Exchange<T>(ref T Location, T value)

此方法是 Exchange 方法的泛型版本。功能与 Exchange 方法相同。这里必须是引用类型。

Interlocked.CompareExchange(ref int intNumber1,int intNumber2,int CompareValue);

CompareExchange 是一个条件赋值方法。第一个参数与最后一个参数进行比较。如果两者相等,则将第一个参数的值替换为第二个参数的值。

Interlocked.CompareExchange<T>(ref T Location, T Value, T CompareValue);

此方法是 CompareExchange 方法的泛型版本。功能与 Exchange 方法相同。这里 T 必须是引用类型。

测试代码

要测试上述代码,请运行应用程序。当点击“Run without Synchronization”(无同步运行)按钮运行无同步代码时,请尝试通过打开一个新的不同应用程序使 CPU 繁忙,以便发生上下文切换。要使用 Interlocked 类运行同步代码,请点击“Run with Synchronization”(有同步运行)并获得实际结果,即预期结果。

结论

我非常享受编写这篇文章。我认为这篇文章提供了 .NET Framework 中提供的线程同步技术 Interlocked 类的基本概述。

历史

  • 2011年11月20日:初稿
  • 2011年11月22日:文章更新
© . All rights reserved.