线程安全组件

在多线程编程中经常需要在线程间共享资源。 例如,多个线程可能需要访问一个共享数据库,或对一组系统变量进行更新。 当多个线程同时竞争共享资源的访问权时,就可能会出现“争用状态”。 如果一个线程将资源修改为无效状态,然后另一个线程又尝试访问该资源并在无效状态中使用该资源,此时便存在争用状态。 请看下面的示例:

Public Class WidgetManipulator
Public TotalWidgets as Integer = 0
Public Sub AddWidget()
   TotalWidgets += 1
   Console.WriteLine("Total widgets = " & TotalWidgets.ToString)
End Sub
Public Sub RemoveWidgets()
   TotalWidgets -= 10
End Sub
End Class
public class WidgetManipulator
{
   public int TotalWidgets = 0;
   public void AddWidget()
   {
      TotalWidgets++;
      Console.WriteLine("Total widgets = " + TotalWidgets.ToString());
   }
   public void RemoveWidgets()
   {
      TotalWidgets -= 10;
   }
}

此类公开两个方法。 一个方法 (AddWidget) 将 1 添加到 TotalWidgets 字段并将该值写入控制台。 第二个方法从 TotalWidgets 的值减 10。 假设两个线程同时尝试访问 WidgetManipulator 类的同一个实例,请考虑将发生什么情况。 在一个线程调用 AddWidget 的同时另一个线程可能调用 RemoveWidgets。 这种情况下,TotalWidgets 的值可能会在第一个线程报告准确值之前就被第二个线程更改。 这种争用状态可能导致报告的结果不准确并可能导致数据损坏。

使用锁来防止争用状态

您可以通过使用“锁”来保护代码的关键部分免受争用状态的损坏。 锁(在 Visual Basic 中用关键字 SyncLock 语句表示,在 C# 中用关键字 lock 语句表示)允许单个执行线程获得对某个对象的独占执行权限。 下面的示例对锁进行了演示:

SyncLock MyObject
' Insert code that affects MyObject.
End SyncLock
lock(MyObject)
{
   // Insert code that affects MyObject.
}

当遇到锁时,对指定对象(在上例中为 MyObject)的执行将被阻塞,直到该线程获得对该对象的独占访问权为止。 当达到锁的末尾时,锁将被释放,执行继续正常进行。 您只能在返回引用的对象上获得锁。 不能用这种方式锁定值类型。

锁的缺点

虽然使用锁可以保证多个线程不会同时访问一个对象,但是它们可能会导致明显的性能降低。 假设正在运行一个包含多个不同线程的程序。 如果每个线程都需要使用一个特定的对象并且必须等待到获得该对象的专用锁后才能执行,则这些线程都将停止执行,一个排在另一个的后面,从而导致性能降低。 由于上述原因,只应该在具有必须作为一个单元执行的代码时使用锁。 例如,您可能更新相互依赖的多个资源。 这种代码称作“原子”代码。 通过将锁的使用范围限制在必须以原子方式执行的代码,就可以编写既能确保代码安全又能保持良好性能的多线程组件。

还要小心地避免可能发生“死锁”的情况。 在发生这种情况时,多个线程彼此都等待对方释放共享资源。 例如,线程 1 可能拥有对资源 A 的锁,而在等待资源 B。 而另一方面,线程 2 可能拥有对资源 B 的锁,而在等待资源 A。 这种情况将使得两个线程都无法进行。 避免死锁情况的唯一方法是仔细编程。

请参见

任务

如何:协调多个执行线程

如何:从线程中操作控件

演练:用 Visual Basic 创作简单的多线程组件

演练:使用 Visual C# 创作简单的多线程组件

参考

BackgroundWorker

概念

基于事件的异步模式概述

其他资源

组件中的多线程处理