1.3 多线程

1.3.1 线程同步

线 程同步是编写多线程程序需要首先考虑问题。C#为同步提供了 Monitor、Mutex、AutoResetEvent 和 ManualResetEvent 对象来分别包装 Win32 的临界区、互斥对象和事件对象这几种基础的同步机制。C#还提供了一个lock语句,方便使用,编译器会自动生成适当的 Monitor.Enter 和 Monitor.Exit 调用。

1.3.1.1 同步粒度

同步粒度可以是整个方法,也可以是方法中某一段代码。为方法指定 MethodImplOptions.Synchronized 属性将标记对整个方法同步。例如:

  1. [MethodImpl(MethodImplOptions.Synchronized)]
  2. public static SerialManager GetInstance()
  3. {
  4. if (instance == null )
  5. {
  6. instance = new SerialManager();
  7. }
  8. return instance;
  9. }

通常情况下,应减小同步的范围,使系统获得更好的性能。简单将整个方法标记为同步不是一个好主意,除非能确定方法中的每个代码都需要受同步保护。

1.3.1.2 同步策略

使用 lock 进行同步,同步对象可以选择 Type、this 或为同步目的专门构造的成员变量。

避免锁定Type★ 锁定Type对象会影响同一进程中所有AppDomain该类型的所有实例,这不仅可能导致严重的性能问题,还可能导致一些无法预期的行为。这是一个很不 好的习惯。即便对于一个只包含static方法的类型,也应额外构造一个static的成员变量,让此成员变量作为锁定对象。

避免锁定 this 锁定 this 会影响该实例的所有方法。假设对象 obj 有 A 和 B 两个方法,其中 A 方法使用 lock(this) 对方法中的某段代码设置同步保护。现在,因为某种原因,B 方法也开始使用 lock(this) 来设置同步保护了,并且可能为了完全不同的目的。这样,A 方法就被干扰了,其行为可能无法预知。所以,作为一种良好的习惯,建议避免使用 lock(this) 这种方式。

使用为同步目的专门构造的成员变量 这是推荐的做法。方式就是 new 一个 object 对象, 该对象仅仅用于同步目的。

如果有多个方法都需要同步,并且有不同的目的,那么就可以为些分别建立几个同步成员变量。

1.3.1.4 集合同步

C#为各种集合类型提供了两种方便的同步机制:Synchronized 包装器和 SyncRoot 属性。

  1. // Creates and initializes a new ArrayList
  2. ArrayList myAL = new ArrayList();
  3. myAL.Add( " The " );
  4. myAL.Add( " quick " );
  5. myAL.Add( " brown " );
  6. myAL.Add( " fox " );
  7. // Creates a synchronized wrapper around the ArrayList
  8. ArrayList mySyncdAL = ArrayList.Synchronized(myAL);

调用 Synchronized 方法会返回一个可保证所有操作都是线程安全的相同集合对象。考虑 mySyncdAL[0] = mySyncdAL[0] + “test” 这一语句,读和写一共要用到两个锁。一般讲,效率不高。推荐使用 SyncRoot 属性,可以做比较精细的控制。

1.3.2 使用 ThreadStatic 替代 NameDataSlot ★

存 取 NameDataSlot 的 Thread.GetData 和 Thread.SetData 方法需要线程同步,涉及两个锁:一个是 LocalDataStore.SetData 方法需要在 AppDomain 一级加锁,另一个是 ThreadNative.GetDomainLocalStore 方法需要在 Process 一级加锁。如果一些底层的基础服务使用了 NameDataSlot,将导致系统出现严重的伸缩性问题。

规避这个问题的方法是使用 ThreadStatic 变量。示例如下:

  1. public sealed class InvokeContext
  2. {
  3. [ThreadStatic]
  4. private static InvokeContext current;
  5. private Hashtable maps = new Hashtable();
  6. }

1.3.3 多线程编程技巧

1.3.3.1 使用 Double Check 技术创建对象

  1. internal IDictionary KeyTable
  2. {
  3. get
  4. {
  5. if ( this ._keyTable == null )
  6. {
  7. lock ( base ._lock)
  8. {
  9. if ( this ._keyTable == null )
  10. {
  11. this ._keyTable = new Hashtable();
  12. }
  13. }
  14. }
  15. return this ._keyTable;
  16. }
  17. }

创建单例对象是很常见的一种编程情况。一般在 lock 语句后就会直接创建对象了,但这不够安全。因为在 lock 锁定对象之前,可能已经有多个线程进入到了第一个 if 语句中。如果不加第二个 if 语句,则单例对象会被重复创建,新的实例替代掉旧的实例。如果单例对象中已有数据不允许被破坏或者别的什么原因,则应考虑使用 Double Check 技术。