C# 多线程(2)多线程同步

我们在编程的时候,有时会使用多线程来解决问题,比如你的程序需要在后台处理一大堆数据,但还要使用户界面处于可操作状态;或者你的程序需要访问一些外部资源如数据库或网络文件等。这些情况你都可以创建一个子线程去处理,然而,多线程不可避免地会带来一个问题,就是线程同步的问题。如果有多个线程同时访问共享数据的时候,就必须要用线程同步,防止共享数据被破坏。如果多个线程不会同时访问共享数据,可以不用线程同步。如果这个问题处理不好,我们就会得到一些非预期的结果。

线程同步也会有一些问题存在
1)性能损耗。获取,释放锁,线程上下文建切换都是耗性能的。
2)同步会使线程排队等待执行。

线程同步的方式
线程同步有:临界区、互斥区、事件、信号量四种方式
临界区(Critical Section)、互斥量(Mutex)、信号量(Semaphore)、事件(Event)的区别   
1)临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。
2)互斥量:采用互斥对象机制。 只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享。  
3)信号量:它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。   
4)事件: 通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作。

1. 阻塞和轮询

当线程调用Sleep,Join,EndInvoke,线程就处于阻塞状态(Sleep使调用线程阻塞,Join、EndInvoke使另外一个线程阻塞),会立即从cpu退出。(阻塞状态的线程不消耗cpu)

当线程在阻塞和非阻塞状态间切换时会消耗几毫秒时间。

//Join
static void Main()
{
  Thread t = new Thread (Go);
  Console.WriteLine ("Main方法已经运行....");  
  t.Start();
  t.Join();//阻塞Main方法
  Console.WriteLine ("Main方法解除阻塞,继续运行...");
}

static void Go()
{
  Console.WriteLine ("在t线程上运行Go方法..."); 
}

//Sleep
static void Main()
{
  Console.WriteLine ("Main方法已经运行....");  
  Thread.CurrentThread.Sleep(3000);//阻塞当前线程
  Console.WriteLine ("Main方法解除阻塞,继续运行...");
}

 //Task
 static void Main()
{
   Task Task1=Task.Run(() => {  
            Console.WriteLine("task方法执行..."); 
              Thread.Sleep(1000);
            }); 
   Console.WriteLine(Task1.IsCompleted);             
   Task1.Wait();//阻塞主线程 ,等该Task1完成
   Console.WriteLine(Task1.IsCompleted); 
}

线程可以等待某个确定的条件来明确轮询使用一个轮询的方式,比如:

while (!proceed);

或者:

while (DateTime.Now < nextStartTime);

这是非常浪费CPU时间的:对于CLR和操作系统而言,线程进行了一个重要的计算,所以分配了相应的资源!在这种状态下的轮询线程不算是阻止,不像一个线程等待一个EventWaitHandle(一般使用这样的信号任务来构建)。

阻止和轮询组合使用可以产生一些变换:

while (!proceed) 
{
    Thread.Sleep (x); // "轮询休眠!"
}

x越大CPU效率越高,折中方案是增大潜伏时间,任何20ms的花费是微不足道的,除非循环中的条件是极其复杂的。
除了稍有延迟,这种轮询和休眠的方式可以结合的非常好。可能它最大的用处在于程序员可以放弃使用复杂的信号结构工作了。

2. volatile 关键字

  volatile是最简单的一种同步方法,当然简单是要付出代价的。它只能在变量一级做同步,volatile的含义就是告诉处理器,不要将我放入工作内存, 请直接在主存操作我。因此,当多线程同时访问该变量时,都将直接操作主存,从本质上做到了变量共享。

public class A
{
    private volatile int _i;
    public int I
    {
        get { return _i; }
        set { _i = value; }
    }
}

volatile并不能实现真正的同步,因为它的操作级别只停留在变量级别,而不是原子级别。如果是在单处理器系统中,是没有任何问题的,变量在主存中没有机会被其他人修改,因为只有一个处理器,这就叫作processor Self-Consistency。但在多处理器系统中,可能就会有问题。 每个处理器都有自己的data cach,而且被更新的数据也不一定会立即写回到主存。所以可能会造成不同步,但这种情况很难发生,因为cach的读写速度相当快,flush的频率也相当高,只有在压力测试的时候才有可能发生,而且几率非常非常小。

3. lock 关键字

lock 关键字将语句块标记为临界区,方法是获取给定对象的互斥锁,执行语句,然后释放该锁。
lock 确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。

private static object objSyncLocker = new object();

public void Function()
{

     lock(objSyncLocker )
     {
           // Access thread-sensitive resources.
     }
}

提供给 lock 关键字的参数必须为基于引用类型的对象,该对象用来定义锁的范围。在上例中,锁的范围限定为此函数,因为函数外不存在任何对该对象的引用。严格地说,提供给 lock 的对象只是用来唯一地标识由多个线程共享的资源,所以它可以是任意类实例。然而,实际上,此对象通常表示需要进行线程同步的资源。例如,如果一个容器对象将被多个线程使用,则可以将该容器传递给 lock,而 lock 后面的同步代码块将访问该容器。只要其他线程在访问该容器前先锁定该容器,则对该对象的访问将是安全同步的。通常,最好避免锁定 public 类型或锁定不受应用程序控制的对象实例,例如,如果该实例可以被公开访问,则 lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。这可能导致死锁,即两个或更多个线程等待释放同一对象。出于同样的原因,锁定公共数据类型(相比于对象)也可能导致问题。锁定字符串尤其危险,因为字符串被公共语言运行库 (CLR)“暂留”。这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。因此,最好锁定不会被暂留的私有或受保护成员。某些类提供专门用于锁定的成员。例如,Array 类型提供 SyncRoot。许多集合类型也提供 SyncRoot。

常见的结构 lock (this)、lock (typeof (MyType)) 和 lock (“myLock”) 违反此准则:
1)如果实例可以被公共访问,将出现 lock (this) 问题。
2)如果 MyType 可以被公共访问,将出现 lock (typeof (MyType)) 问题。
3)由于进程中使用同一字符串的任何其他代码将共享同一个锁,所以出现 lock(“myLock”) 问题。
4)不能在lock中使用await关键字

最佳做法是定义 private 对象来锁定, 或 private static 对象变量来保护所有实例所共有的数据。

锁对象是否必须是静态类型?

如果被锁定的方法是静态的,那么这个锁必须是静态类型。这样就是在全局锁定了该方法,不管该类有多少个实例,都要排队执行。
如果被锁定的方法不是静态的,那么不能使用静态类型的锁,因为被锁定的方法是属于实例的,只要该实例调用锁定方法不产生损坏就可以,不同实例间是不需要锁的。这个锁只锁该实例的方法,而不是锁所有实例的方法。

class ThreadSafe
{
     private static object _locker = new object();
     private object _locker2=new object();

     void Go()
     {
        lock (_locker)
        {
          ......//共享数据的操作 (Static Method),使用静态锁确保所有实例排队执行
        }
      }

      void GoTo()
      {
        lock(_locker2)
        {
            //共享数据的操作,非静态方法,是用非静态锁,确保同一个实例的方法调用者排队执行
        }
      }
}

4. Monitor 监视器

Monitor类提供了与lock类似的功能,不过与lock不同的是,它能更好的控制同步块,当调用了Monitor的Enter(Object o)方法时,会获取o的独占权,直到调用Exit(Object o)方法时,才会释放对o的独占权,可以多次调用Enter(Object o)方法,只需要调用同样次数的Exit(Object o)方法即可,Monitor类同时提供了TryEnter(Object o,[int])的一个重载方法,该方法尝试获取o对象的独占权,当获取独占权失败时,将返回false。
事实上,lock 关键字就是用 Monitor 类来实现的。

private static object objSyncLocker = new object();

public void Function()
{
     System.Threading.Monitor.Enter(objSyncLocker );

     try
     {
        // Access thread-sensitive resources.
     }
     catch{ }
     finally
     {
            System.Threading.Monitor.Exit(UsingPrinterLocker);
     }
}

使用 lock 关键字通常比直接使用 Monitor 类更可取,一方面是因为 lock 更简洁,另一方面是因为 lock 确保了即使受保护的代码引发异常,也可以释放基础监视器。这是通过 finally 关键字来实现的,无论是否引发异常它都执行关联的代码块。

5. Interlocked

如果一个变量被多个线程修改,读取。可以用Interlocked。

计算机上不能保证对一个数据的增删是原子性的,因为对数据的操作也是分步骤的:
1)将实例变量中的值加载到寄存器中。
2)增加或减少该值。
3)在实例变量中存储该值。

Interlocked为多线程共享的变量提供原子操作
Interlocked提供了需要原子操作的方法:
1)Increment、Decrement 可以使参数安全地加1或减1并返回递增后的新值。

class Example
{
      private int a=1;

      public void AddOne()
      {
             int newA=Interlocked.Increment(ref a);
      }
}

2)Exchange可以安全地变量赋值。

public void SetData()
{
       Interlocked.Exchange(ref a,100);
}

3)CompareExchange使用特别方便,它相当于if的用法,如果第一个参数和第三个参数相等,那么就把value赋值给第一个参数。例如:当a等于1时,则把100赋值给a。

public void CompareAndExchange()
 {
        Interlocked.CompareExchange(ref a,100,1);
 }

6.Mutex对象

Mutex 与监视器类似;它防止多个线程在某一时间同时执行某个代码块。事实上,名称“mutex”是术语“互相排斥 (mutually exclusive)”的简写形式。然而与监视器不同的是,Mutex 可以用来使跨进程的线程同步。mutex 由 Mutex 类表示。当用于进程间同步时,mutex 称为“命名 mutex”,因为它将用于另一个应用程序,因此它不能通过全局变量或静态变量共享。必须给它指定一个名称,才能使两个应用程序访问同一个 mutex 对象。
尽管 mutex 可以用于进程内的线程同步,但是使用 Monitor 通常更为可取,因为监视器是专门为 .NET Framework 而设计的,因而它可以更好地利用资源。相比之下,Mutex 类是 Win32 构造的包装。尽管 mutex 比监视器更为强大,但是相对于 Monitor 类,它所需要的互操作转换更消耗计算资源

Mutex 分两种类型:
命名系统 mutex:如果使用接受名称的构造函数创建了 Mutex 对象,那么该对象将与具有该名称的操作系统对象相关联。 命名的系统 mutex 在整个操作系统中都可见,并且可用于同步进程活动。 您可以创建多个 Mutex 对象来表示同一命名系统 mutex,而且您可以使用 OpenExisting 方法打开现有的命名系统 mutex。
本地 mutex :仅存在于进程当中。 进程中引用本地 Mutex 对象的任意线程都可以使用本地 mutex。 每个 Mutex 对象都是一个单独的本地 mutex。

在本地Mutex中,用法与Monitor基本一致

    private static Mutex mutex = new Mutex();

    /// <summary>
    /// 使用打印机进行打印
    /// </summary>
    private static void UsePrinterWithMutex()
    {
        mutex.WaitOne();
        try
        {
           // Access thread-sensitive resources.
        }
        catch{ }
        finally
        {
            mutex.ReleaseMutex();
        }
    }

7. 同步事件和等待句柄

使用锁或监视器对于防止同时执行区分线程的代码块很有用,但是这些构造不允许一个线程向另一个线程传达事件。这需要“同步事件”,它是有两个状态(终止和非终止)的对象,可以用来激活和挂起线程。让线程等待非终止的同步事件可以将线程挂起,将事件状态更改为终止可以将线程激活。如果线程试图等待已经终止的事件,则线程将继续执行,而不会延迟。

同步事件有两种:AutoResetEventManualResetEvent。它们之间唯一的不同在于,无论何时,只要 AutoResetEvent 激活线程,它的状态将自动从终止变为非终止。相反,ManualResetEvent 允许它的终止状态激活任意多个线程,只有当它的 Reset 方法被调用时才还原到非终止状态。

等待句柄可以通过调用一种等待方法,如 WaitOne、WaitAny 或 WaitAll,让线程等待事件。System.Threading.WaitHandle.WaitOne 使线程一直等待,直到单个事件变为终止状态;System.Threading.WaitHandle.WaitAny 阻止线程,直到一个或多个指示的事件变为终止状态;System.Threading.WaitHandle.WaitAll 阻止线程,直到所有指示的事件都变为终止状态。当调用事件的 Set 方法时,事件将变为终止状态。

AutoResetEvent 允许线程通过发信号互相通信。通常,当线程需要独占访问资源时使用该类。线程通过调用 AutoResetEvent 上的 WaitOne 来等待信号。 如果 AutoResetEvent 为非终止状态,则线程会被阻止,并等待当前控制资源的线程通过调用 Set 来通知资源可用。调用 Set 向 AutoResetEvent 发信号以释放等待线程。 AutoResetEvent 将保持终止状态,直到一个正在等待的线程被释放,然后自动返回非终止状态。 如果没有任何线程在等待,则状态将无限期地保持为终止状态。如果当 AutoResetEvent 为终止状态时线程调用 WaitOne,则线程不会被阻止。 AutoResetEvent 将立即释放线程并返回到非终止状态。
可以通过将一个布尔值传递给构造函数来控制 AutoResetEvent 的初始状态:如果初始状态为终止状态,则为 true;否则为 false。

AutoResetEvent 也可以同 staticWaitAll 和 WaitAny 方法一起使用。

案例介绍:

今天我们来做饭,做饭呢,需要一菜、一粥。今天我们吃鱼。

熬粥和做鱼,是比较复杂的工作流程,
做粥:选材、淘米、熬制
做鱼:洗鱼、切鱼、腌制、烹调
为了提高效率,我们用两个线程来准备这顿饭,但是,现在只有一口锅,只能等一个做完之后,另一个才能进行最后的烹调。

来看实例代码:

using System;
using System.Threading;

namespace MutiThreadSample.ThreadSynchronization
{
    /// <summary>
    /// 案例:做饭
    /// 今天的Dinner准备吃鱼,还要熬粥
    /// 熬粥和做鱼,是比较复杂的工作流程,
    /// 做粥:选材、淘米、熬制
    /// 做鱼:洗鱼、切鱼、腌制、烹调
    /// 我们用两个线程来准备这顿饭
    /// 但是,现在只有一口锅,只能等一个做完之后,另一个才能进行最后的烹调
    /// </summary>
    class CookResetEvent
    {
        /// <summary>
        /// 
        /// </summary>
        private AutoResetEvent resetEvent = new AutoResetEvent(false);

        /// <summary>
        /// 做饭
        /// </summary>
        public void Cook()
        {
            Thread porridgeThread = new Thread(new ThreadStart(Porridge));
            porridgeThread.Name = "Porridge";
            porridgeThread.Start();

            Thread makeFishThread = new Thread(new ThreadStart(MakeFish));
            makeFishThread.Name = "MakeFish";
            makeFishThread.Start();

            //等待5秒
            Thread.Sleep(5000);

            resetEvent.Reset();
        }

        /// <summary>
        /// 熬粥
        /// </summary>
        public void Porridge()
        { 
            //选材
            Console.WriteLine("Thread:{0},开始选材", Thread.CurrentThread.Name);

            //淘米
            Console.WriteLine("Thread:{0},开始淘米", Thread.CurrentThread.Name);

            //熬制
            Console.WriteLine("Thread:{0},开始熬制,需要2秒钟", Thread.CurrentThread.Name);
            //需要2秒钟
            Thread.Sleep(2000);
            Console.WriteLine("Thread:{0},粥已经做好,锅闲了", Thread.CurrentThread.Name);

            resetEvent.Set();
        }
            /// <summary>
        /// 做鱼
        /// </summary>
        public void MakeFish()
        { 
            //洗鱼
            Console.WriteLine("Thread:{0},开始洗鱼",Thread.CurrentThread.Name);

            //腌制
            Console.WriteLine("Thread:{0},开始腌制", Thread.CurrentThread.Name);

            //等待锅空闲出来
            resetEvent.WaitOne();

            //烹调
            Console.WriteLine("Thread:{0},终于有锅了", Thread.CurrentThread.Name);
            Console.WriteLine("Thread:{0},开始做鱼,需要5秒钟", Thread.CurrentThread.Name);
            Thread.Sleep(5000);
            Console.WriteLine("Thread:{0},鱼做好了,好香", Thread.CurrentThread.Name);

            resetEvent.Set();
        }
    }
}

ManualResetEvent与AutoResetEvent用法基本类似,这里不多做介绍。

8. Semaphore 和 SemaphoreSlim

System.Threading.Semaphore 类表示一个命名(系统范围)信号量或本地信号量。 它是一个对 Win32 信号量对象的精简包装。 Win32 信号量是计数信号量,可用于控制对资源池的访问。

SemaphoreSlim 类表示一个轻量的快速信号量,可用于在一个预计等待时间会非常短的进程内进行等待。 SemaphoreSlim 会尽可能多地依赖由公共语言运行时 (CLR) 提供的同步基元。 但是,它也会根据需要提供延迟初始化的、基于内核的等待句柄,以支持等待多个信号量。 SemaphoreSlim 还支持使用取消标记,但它不支持命名信号量或使用等待句柄来进行同步。

线程通过调用 WaitOne 方法来进入信号量,此方法是从 WaitHandle 类派生的。 当调用返回时,信号量的计数将减少。 当一个线程请求项而计数为零时,该线程会被阻止。 当线程通过调用 Release 方法释放信号量时,将允许被阻止的线程进入。 并不保证被阻塞的线程进入信号量的顺序,例如先进先出 (FIFO) 或后进先出 (LIFO)。信号量的计数在每次线程进入信号量时减小,在线程释放信号量时增加。 当计数为零时,后面的请求将被阻塞,直到有其他线程释放信号量。 当所有的线程都已释放信号量时,计数达到创建信号量时所指定的最大值。

案例分析:购买火车票

还得排队进行购买,购买窗口是有限的,只有窗口空闲时才能购买 。

    /// <summary>
    /// 案例:支付流程
    /// 如超市、药店、火车票等,都有限定的几个窗口进行结算,只有有窗口空闲,才能进行结算。
    /// 我们就用多线程来模拟结算过程
    /// </summary>
    class PaymentWithSemaphore
    {
        /// <summary>
        /// 声明收银员总数为3个,但是当前空闲的个数为0,可能还没开始上班。
        /// </summary>
        private static Semaphore IdleCashiers = new Semaphore(0, 3);

        /// <summary>
        /// 测试支付过程
        /// </summary>
        public static void TestPay()
        {
            ParameterizedThreadStart start = new ParameterizedThreadStart(Pay);
            //假设同时有5个人来买票
            for (int i = 0; i < 5; i++)
            {
                Thread thread = new Thread(start);
                thread.Start(i);
            }

            //主线程等待,让所有的的线程都激活
        Thread.Sleep(1000);
        //释放信号量,2个收银员开始上班了或者有两个空闲出来了
        IdleCashiers.Release(2);
    }
    /// <summary>
    /// 
    /// </summary>
    /// <param name="obj"></param>
    public static void Pay(object obj)
    {
        Console.WriteLine("Thread {0} begins and waits for the semaphore.", obj);
        IdleCashiers.WaitOne();
        Console.WriteLine("Thread {0} starts to Pay.",obj);
        //结算
        Thread.Sleep(2000);
        Console.WriteLine("Thread {0}: The payment has been finished.",obj);

        Console.WriteLine("Thread {0}: Release the semaphore.", obj);
        IdleCashiers.Release();
    }
}

9. 读取器/编写器锁

ReaderWriterLockSlim 类允许多个线程同时读取一个资源,但在向该资源写入时要求线程等待以获得独占锁
可以在应用程序中使用 ReaderWriterLockSlim,以便在访问一个共享资源的线程之间提供协调同步。 获得的锁是针对 ReaderWriterLockSlim 本身的。
设计您应用程序的结构,让读取和写入操作的时间尽可能最短。 因为写入锁是排他的,所以长时间的写入操作会直接影响吞吐量。 长时间的读取操作会阻止处于等待状态的编写器,并且,如果至少有一个线程在等待写入访问,则请求读取访问的线程也将被阻止。

案例:构造一个线程安全的缓存

using System;
using System.Threading;
using System.Collections.Generic;

namespace MutiThreadSample.ThreadSynchronization
{
    /// <summary>
    /// 同步Cache
    /// </summary>
    public class SynchronizedCache
    {
        private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
        private Dictionary<int, string> innerCache = new Dictionary<int, string>();
        /// <summary>
        /// 读取
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public string Read(int key)
        {
            cacheLock.EnterReadLock();
            try
            {
                return innerCache[key];
            }
            finally
            {
                cacheLock.ExitReadLock();
            }
        }
        /// <summary>
        /// 添加项
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public void Add(int key, string value)
        {
            cacheLock.EnterWriteLock();
            try
            {
                innerCache.Add(key, value);
            }
            finally
            {
                cacheLock.ExitWriteLock();
            }
        }
        /// <summary>
        /// 添加项,有超时限制
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <param name="timeout"></param>
        /// <returns></returns>
        public bool AddWithTimeout(int key, string value, int timeout)
        {
            if (cacheLock.TryEnterWriteLock(timeout))
            {
                try
                {
                    innerCache.Add(key, value);
                }
                finally
                {
                    cacheLock.ExitWriteLock();
                }
                return true;
            }
            else
            {
                return false;
            }
        }
        /// <summary>
        /// 添加或者更新
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public AddOrUpdateStatus AddOrUpdate(int key, string value)
        {
            cacheLock.EnterUpgradeableReadLock();
            try
            {
                string result = null;
                if (innerCache.TryGetValue(key, out result))
                {
                    if (result == value)
                    {
                        return AddOrUpdateStatus.Unchanged;
                    }
                    else
                    {
                        cacheLock.EnterWriteLock();
                        try
                        {
                            innerCache[key] = value;
                        }
                        finally
                        {
                            cacheLock.ExitWriteLock();
                        }
                        return AddOrUpdateStatus.Updated;
                    }
                }
                else
                {
                    cacheLock.EnterWriteLock();
                    try
                    {
                        innerCache.Add(key, value);
                    }
                    finally
                    {
                        cacheLock.ExitWriteLock();
                    }
                    return AddOrUpdateStatus.Added;
                }
            }
            finally
            {
                cacheLock.ExitUpgradeableReadLock();
            }
        }
        /// <summary>
        /// 删除项
        /// </summary>
        /// <param name="key"></param>
        public void Delete(int key)
        {
            cacheLock.EnterWriteLock();
            try
            {
                innerCache.Remove(key);
            }
            finally
            {
                cacheLock.ExitWriteLock();
            }
        }
        /// <summary>
        /// 
        /// </summary>
        public enum AddOrUpdateStatus
        {
            Added,
            Updated,
            Unchanged
        };
    }
}

9. 障碍(Barrier)4.0后技术

使多个任务能够采用并行方式依据某种算法在多个阶段中协同工作。
通过在一系列阶段间移动来协作完成一组任务,此时该组中的每个任务发信号指出它已经到达指定阶段的 Barrier 并且暗中等待其他任务到达。 相同的 Barrier 可用于多个阶段。

    class Program
    {
        static void Main(string[] args)
         {
            Thread t1 = new Thread(() => PlayMusic("the gutarist", "play an amazing solo", 5));
            Thread t2 = new Thread(() => PlayMusic("the signer", "sing his song", 2));
            t1.Start();
            t2.Start();
            Console.ReadLine();
        }
        //后面的Lamda表达式是回调函数。执行完SignalAndWait后执行
        static Barrier barrier = new Barrier(2, b=>Console.WriteLine($"End of phase {b.CurrentPhaseNumber + 1}"));

    static void PlayMusic(string name, string message, int seconds)
    {
        for (int i = 0; i < 3; i++)
        {
            Console.WriteLine("===========================");
            Thread.Sleep(TimeSpan.FromSeconds(seconds));
            Console.WriteLine($"{name} starts to {message}");
            Thread.Sleep(TimeSpan.FromSeconds(seconds));
            Console.WriteLine($"{name} finishes to {message}");
            //等所有调用线程都结束
            barrier.SignalAndWait();
        }
    }
 }

10、SpinLock(4.0后)

SpinLock结构是一个低级别的互斥同步基元,它在等待获取锁时进行旋转。 在多核计算机上,当等待时间预计较短且极少出现争用情况时,SpinLock 的性能将高于其他类型的锁。 不过,我们建议您仅在通过分析确定 System.Threading.Monitor 方法或 Interlocked 方法显著降低了程序的性能时使用 SpinLock。
即使 SpinLock 未获取锁,它也会产生线程的时间片。 它这样做是为了避免线程优先级别反转,并使垃圾回收器能够继续执行。 在使用 SpinLock 时,请确保任何线程持有锁的时间不会超过一个非常短的时间段,并确保任何线程在持有锁时不会阻塞。
由于 SpinLock 是一个值类型,因此,如果您希望两个副本都引用同一个锁,则必须通过引用显式传递该锁。

using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace MutiThreadSample.ThreadSynchronization
{
    class SpinLockSample
    {
        public static void Test()
        {
            SpinLock sLock = new SpinLock();
            StringBuilder sb = new StringBuilder();
            Action action = () =>
            {
                bool gotLock = false;
                for (int i = 0; i < 100; i++)
                {
                    gotLock = false;
                    try
                    {
                        sLock.Enter(ref gotLock);
                        sb.Append(i.ToString());
                    }
                    finally
                    {
                        //真正获取之后,才释放
                        if (gotLock) sLock.Exit();
                    }
                }
            };

            //多线程调用action
            Parallel.Invoke(action, action, action);
            Console.WriteLine("输出:{0}",sb.ToString());
        }
    }
}

11、SpinWait(4.0后)

System.Threading.SpinWait 是一个轻量同步类型,可以在低级别方案中使用它来避免内核事件所需的高开销的上下文切换和内核转换。 在多核计算机上,当预计资源不会保留很长一段时间时,如果让等待线程以用户模式旋转数十或数百个周期,然后重新尝试获取资源,则效率会更高。 如果在旋转后资源变为可用的,则可以节省数千个周期。 如果资源仍然不可用,则只花费了少量周期,并且仍然可以进行基于内核的等待。 这一旋转-等待的组合有时称为“两阶段等待操作”。

下面的基本示例采用微软案例:无锁堆栈

using System;
using System.Threading;

namespace MutiThreadSample.ThreadSynchronization
{
    public class LockFreeStack<T>
    {
        private volatile Node m_head;

        private class Node { public Node Next; public T Value; }

        public void Push(T item)
        {
            var spin = new SpinWait();
            Node node = new Node { Value = item }, head;
            while (true)
            {
                head = m_head;
                node.Next = head;
                if (Interlocked.CompareExchange(ref m_head, node, head) == head) break;
                spin.SpinOnce();
            }
        }

        public bool TryPop(out T result)
        {
            result = default(T);
            var spin = new SpinWait();

            Node head;
            while (true)
            {
                head = m_head;
                if (head == null) return false;
                if (Interlocked.CompareExchange(ref m_head, head.Next, head) == head)
                {
                    result = head.Value;
                    return true;
                }
                spin.SpinOnce();
            }
        }
    }
}

12. 同步环境

与手工的锁定相比,你可以进行说明性的锁定,用衍生自ContextBoundObject 并标以Synchronization特性的类,
它告诉CLR自动执行锁操作,看这个例子:

using System;
using System.Threading;
using System.Runtime.Remoting.Contexts;

[Synchronization]
public class AutoLock : ContextBoundObject {
  public void Demo() {
    Console.Write ("Start...");
    Thread.Sleep (1000);           // We can't be preempted here
    Console.WriteLine ("end");     // thanks to automatic locking!
  } 
}

public class Test {
  public static void Main() {
    AutoLock safeInstance = new AutoLock();
    new Thread (safeInstance.Demo).Start();     // Call the Demo
    new Thread (safeInstance.Demo).Start();     // method 3 times
    safeInstance.Demo();                        // concurrently.
  }
}

CLR确保了同一时刻只有一个线程可以执行 safeInstance中的代码。
它创建了一个同步对象来完成工作,并在每次调用safeInstance的方法和属性时在其周围只能够行锁定。
锁的作用域—这里是safeInstance对象,被称为同步环境。
那么,它是如何工作的呢?Synchronization特性的命名空间:System.Runtime.Remoting.Contexts是一个线索。
ContextBoundObject可以被认为是一个“远程”对象,这意味着所有方法的调用是被监听的。让这个监听称为可能,
就像我们的例子AutoLock,CLR自动的返回了一个具有相同方法和属性的AutoLock对象的代理对象,它扮演着一个中间者的角色。总的来说,监听在每个方法调用时增加了数微秒的时间。

自动同步不能用于静态类型的成员,和非继承自 ContextBoundObject(例如:Windows Form)的类。

锁在内部以相同的方式运作,你可能期待下面的例子与之前的有一样的结果:

[Synchronization]
public class AutoLock : ContextBoundObject {
  public void Demo() {
    Console.Write ("Start...");
    Thread.Sleep (1000);
    Console.WriteLine ("end");
  }

  public void Test() {
    new Thread (Demo).Start();
    new Thread (Demo).Start();
    new Thread (Demo).Start();
    Console.ReadLine();
  }

  public static void Main() {
    new AutoLock().Test();
  }
} 

注意我们放入了Console.ReadLine语句
因为在同一时刻的同一个此类的对象中只有一个线程可以执行代码,三个新线程将保持被阻止在Demo放中,直到Test方法完成,需要等待ReadLine来完成。因此我们以与之前的有相同结果而告终,但是只有在按完Enter键之后。
这是一个线程安全的手段,差不多足够能在类中排除任何有用的多线程!此外,我们仍未解决之前描述的一个问题:如果AutoLock是一个集合类,比如说,我们仍然需要一个像下面一样的锁,假设运行在另一个类里:

if (safeInstance.Count > 0) safeInstance.RemoveAt (0);

除非使用这代码的类本身是一个同步的ContextBoundObject!同步环境可以扩展到超过一个单独对象的区域。
默认地,如果一个同步对象被实例化从在另一段代码之内,它们拥有共享相同的同步环境(换言之,一个大锁)。
这个行为可以由改变Synchronization特性的构造器的参数来指定。
使用SynchronizationAttribute类定义的常量之一:

常量 含义
NOT_SUPPORTED 相当于不使用同步特性
SUPPORTED 如果从另一个同步对象被实例化,则合并已存在的同步环境,否则只剩下非同步。
REQUIRED(默认) 如果从另一个同步对象被实例化,则合并已存在的同步环境,否则创建一个新的同步环境。
REQUIRES_NEW 总是创建新的同步环境

所以如果SynchronizedA的实例被实例化于SynchronizedB的对象中,如果SynchronizedB像下面这样声明的话,

它们将有分离的同步环境:

   [Synchronization (SynchronizationAttribute.REQUIRES_NEW)]

    public class SynchronizedB : ContextBoundObject { ...

越大的同步环境越容易管理,但是减少机会对有用的并发。换个有限的角度,分离的同步环境会造成死锁,看这个例子:

[Synchronization]
public class Deadlock : ContextBoundObject {
  public DeadLock Other;
  public void Demo() { Thread.Sleep (1000); Other.Hello(); }
  void Hello()       { Console.WriteLine ("hello");         }
}

public class Test {
  static void Main() {
    Deadlock dead1 = new Deadlock();
    Deadlock dead2 = new Deadlock();
    dead1.Other = dead2;
    dead2.Other = dead1;
    new Thread (dead1.Demo).Start();
    dead2.Demo();
  }
}

因为每个Deadlock的实例在Test内创建一个非同步类,每个实例将有它自己的同步环境,因此,有它自己的锁。
当它们彼此调用的时候,不会花太多时间就会死锁(确切的说是一秒!)。如果Deadlock 和 Test是由不同开发团队来写的,这个问题特别容易发生。别指望Test知道如何产生的错误,更别指望他们来解决它了。在死锁显而易见的情况下,这与使用明确的锁的方式形成鲜明的对比。

12.1 可重入性问题

线程安全方法有时候也被称为可重入式的,因为在它执行的时候可以被抢占部分线路,在另外的线程调用也不会带来坏效果。从某个意义上讲,术语线程安全和 可重入式的是同义的或者是贴义的。
不过在自动锁方式上,如果Synchronization的参数可重入式的 为true的话,可重入性会有潜在的问题:

   [Synchronization(true)]

同步环境的锁在执行离开上下文时被临时地释放。在之前的例子里,这将能预防死锁的发生;很明显很需要这样的功能。然而一个副作用是,在这期间,任何线程都 可以自由的调用在目标对象(“重进入”的同步上下文)的上任何方法,而非常复杂的多线程中试图避免不释放资源是排在首位的。这就是可重入性的问题。 因为[Synchronization(true)]作用于类级别,这特性打开了对于非上下文的方法访问,由于可重入性问题使它们混入类的调用。

虽然可重入性是危险的,但有些时候它是不错的选择。比如:设想一个在其内部实现多线程同步的类,将逻辑工作线程运行在不同的语境中。在没有可重入性问题的情况下,工作线程在它们彼此之间或目标对象之间可能被无理地阻碍。

这凸显了自动同步的一个基本弱点:超过适用的大范围的锁定带来了其它情况没有带来的巨大麻烦。这些困难:死锁,可重入性问题和被阉割的并发,使另一个更简单的方案——手动的锁定变得更为合适

总结:
尽管有这么多的技术,但是不同的技术对应不同的场景,我们必须熟悉其特点和适用范围。在应用时,必须具体问题具体分析,选择最佳的同步方式。

猜你喜欢

转载自blog.csdn.net/num197/article/details/80312998