欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

线程死锁与同步

程序员文章站 2022-04-17 14:37:37
...

1. 争用条件和死锁

1.1 争用条件

​ 如果两个或多个线程访问相同的对象,并且对共享状态的访问没有同步,就会出现争用条件。为了说明争用条件,下面的例子定义一个StateObject类,它包含一个私有int字段 state 和公有ChangeState()方法。在ChangeState()方法的实现代码中,验证状态变量 state 是否等于5。如果相等,就递增。下一条Trace.Assert立刻验证 state 现在是否等于6。

​ 在变量 state 等于5再递增1后,可能认为该变量的值就是6。但事实不一定是这样。例如,如果一个线程刚刚执行完 If(state==5) 语句,它就被其它线程抢占,调度器运行另一个线程。第二个线程现在进入if体,因为 state 的值仍是5,所以将它递增到6.第一个线程现在再次被调度,在下一条语句,state 递增到7。这时就发生了争用条件(Race Condition),并显示断言消息。

public class StateObject
{
	private int state = 5;
	public void ChangeState(int loop)
	{
		if (state == 5)
		{
			state++;
			Trace.Assert(state == 6, "Race condition occurred after " + loop + " loops");
		}
		state = 5;
	}
}

​ 下面通过给任务定义一个方法来验证这一点。SampleTask类的**RaceCondition()方法将一个 StateObject 类作为其参数。在一个无限while循环中,调用ChangeState()**方法。变量 i 仅用于显示断言消息中的循环次数。

public class SampleTask
{
   public void RaceCondition(object o)
    {
       Trace.Assert(o is StateObject, "o must be of type StateObject");
       StateObject state = o as StateObject;
        int i = 0;
        while(true)
        {
           state.ChangeState(i++);
        }
    }
}

​ 在程序的main()方法中,新建了一个StateObject对象,它由所有任务共享。

static void Main(string[] args)
{
   StateObject state = new StateObject();
   for (int i = 0; i < 2; i++)
   {
       Task.Run(() =>
       {
           new SampleTask().RaceCondition(state);
        });
   }
 Console.ReadKey();

​ 启动程序,就会出现争用条件。如果系统中有多个CPU或使用双核/四核CPU,其中对各线程可以同时运行,则该问题也会比单核CPU的出现的次数多。本地调试显示如下结果,多次调试,总是会得到不同的结果:

---- DEBUG ASSERTION FAILED ----
---- Assert Short Message ----
Race condition occurred after 1261 loops
---- Assert Long Message ----

​ 要避免该问题,可以锁定共享的对象。这可以在线程中完成:用下面的 lock 语句锁定住在线程*享的 state 对象。保证只有一个线程能在锁定块中处理共享的 state 对象。由于这个对象在所有的线程之间共享,因此如果有一个线程锁定了 state,;另一个线程就必须等待该锁定的解除。一旦接受锁定,线程就拥有该锁定,直到该锁定块的末尾才解除锁定。如果拥有改变共享对象 state 的每个线程都使用一个锁定,就不会出现争用条件。

public void RaceCondition(object o)
{
      Trace.Assert(o is StateObject, "o must be of type StateObject");
      StateObject state = o as StateObject;
      int i = 0;
      while(true)
      {
          lock(state) //no race condition with this lock
      	 {
          	state.ChangeState(i++);
          }
	 }
}

​ 在使用共享对象时,除了对共享进行对象锁定之外,还可以将共享对象设置为线程安全的对象。在下面的代码中,ChangeState() 方法包含一条 lock 语句。由于不能锁定 state 变量本身**(因为只有引用类型才能用于锁定)**,因此定义一个 object 类型的变量 sync,将它用于 lock 语句。如果每次 state 的值更改时,都使用同一个同步对象来锁定,就不会出现争用条件。

public class StateObject
{
	private int state = 5;
	private object sync = new object();
	public void ChangeState(int loop)
	{
		lock(sync) //no race condition with this lock
		{
			if (state == 5)
			{
				state++;
				Trace.Assert(state == 6, "Race condition occurred after " + loop + " loops");
			}
			state = 5;
	}
}

1.2 死锁

​ 过多的锁定可能会引起死锁。在死锁中,至少有两个线程被挂起,并等待对方解除锁定。由于两个线程都在等待对方,就出现了死锁,线程将无限等待下去。

​ 为了说明死锁,下面实例化 StateObject 类型的两个对象,并把它们传递个 SampleTask 类的构造函数。创建两个Task,其中一个运行 DeadLock1() 方法,另一个运行 DeadLock2() 方法:

var s1 = new StateObject();
var s2 = new StateObject();
var T1 = new Task(new SampleTask(s1, s2).DeadLock1);
var T2 =  new Task(new SampleTask(s1, s2).DeadLock2);
T1.Start();
T2.Start();
Task.WaitAll(T1, T2);

DeakLock1()DeadLock2() 方法现在改变两个对象 s1 和 s2 的状态,所以生成了两个锁。lock1 方法先锁定 s1,接着锁定 s2。lock2 方法先锁定 s2,再锁定 s1。T1 => 现在,有可能 lock1 方法中 s1 的锁定会被解除;接着,出现一次线程切换,T2 => lock2 方法开始运行,并锁定 s2。T2 线程现在等待 s1 锁定的解除。因为它需要等待,所以线程调度器再次调度 T1,但 T1 线程在等待 s1 锁定的解除。这两个线程都在等待,只要锁定块没有结束,就不会解除锁定。这是一个典型的死锁

private readonly StateObject s1;
        private readonly StateObject s2;
        public SampleTask(StateObject s1, StateObject s2)
        {
            this.s1 = s1;
            this.s2 = s2;
        }
        public void DeadLock1()
        {
            int i = 0;
            while (true)
            { 
                lock(s1)
                {
                    lock(s2)
                    {
                        s1.ChangeState(i);
                        s2.ChangeState(i++);
                        Console.WriteLine("DeadLock1 still running {0}", i);
                    }
                }
            }    
        }
        public void DeadLock2()
        {
            int i = 0;
            while (true)
            {
                lock (s2)
                {
                    lock (s1)
                    {
                        s1.ChangeState(i);
                        s2.ChangeState(i++);
                        Console.WriteLine("DeadLock2 still running {0}", i);
                    }
                }
            }
        }

​ 结果是,程序运行了许多次循环,不久就没有了响应。“仍在运行”的消息仅写入控制台几次。在VS 2019 中,可以在调试模式下运行程序,在诊断工具中点击CPU使用率 => 全部中断,其中显示线程出于 死锁 状态。

​ 死锁问题并不总是像上面的例子那样明显。一个线程锁定了 s1,接着锁定 s2;另一个线程锁定了 s2,接着锁定 s1。在本例中只需要改变锁定顺序,这两个线程就会以相同的顺序进行锁定。但是锁定可能隐藏在方法的深处。为了避免这个问题,可以在应用程序的体系架构中,从一开始就设计好锁定顺序,也可以为锁定定义超时时间。

2. 同步

​ 要避免同步问题,最好不要在线程之间共享数据。当然,这不总是可行的。如果需要共享数据,就必须使用同步技术,确保一次只有一个线程访问和改变共享状态。同步问题与争用条件和死锁有关。如果不注意这些问题,就很难在应用程序中找到相关问题的原因,因为线程问题是不定期发生的。

​ 本章节主要描述可以用于多线程的同步技术:

  • lock 语句块 互斥锁
  • Interlocked 类 *锁
  • Monitor 类 监视器
  • SpinLock 结构 自旋锁
  • WaitHandle 类 线程等待
  • Mutex 类 系统内核对象 互斥对象
  • Semaphore 类 信号量
  • EventWaitHandle 类 事件
  • Barrier 类 屏障 => 线程隔离
  • ReaderWriterLockSlim 类 读写锁

2.1 lock 语句块和线程安全

​ C# 为多个线程同步提供了自己的关键字:lock 语句。lock 语句是设置锁定和解除锁定的一种简单方式。在添加 lock 语句之前,先进入另一个争用条件。SharedState 类说明了如何使用线程之间的共享状态,并共享一个整数值。

public class SharedState
{
    public int State { get; set; }
}

Job 类包含 DoTheJob() 方法,该方法是新任务的入口点。通过其实现代码,将 SharedState 变量的 State递增 50000次。sharedState 变量在这个类的构造函数中初始化。

public class Job
    {
        readonly SharedState sharedState;
        public Job(SharedState sharedState)
        {
            this.sharedState = sharedState;
        }

        public void DoTheJob()
        {
            for (int i = 0; i < 50000; i++)
            {
                sharedState.State += 1;
            }
        }
    }

​ 在 Main() 方法中,创建一个 SharedState 对象,并把它传递给 20 个 Task 对象的构造函数,在启动所有的任务后, Main() 方法进入另一个循环,等待20个任务都执行完毕。任务执行完毕后,把共享状态的合计值写入控制台中。因为执行了 50000 次循环,有20个任务,所以写入控制台的值应是 100 0000。但是,事实并非如此。

static void Main(string[] args)
{
	int numTasks = 20;
	var state = new SharedState();
	var tasks = new Task[numTasks];

	for (int i = 0; i < numTasks; i++)
	{
		tasks[i] = Task.Run(() => {
			new Job(state).DoTheJob();
		});
	}

	for (int i = 0; i < numTasks; i++)
	{
		tasks[i].Wait();
	}
	Console.Write("summarized {0}", state.State);
           
}

多次运行程序的结果如下:

summarized 229554
summarized 209104
summarized 194886
summarized 191467

​ 每次运行结果都不同,但没有一个结果是正确的。这就产生了典型的 争用条件。那么必须在程序中添加 同步 功能,这可以用 lock 关键字实现。用 lock 语句定义的对象,表示要等待指定对象的锁定。只能传引用类型,锁定值类型只是锁定了一个副本,这没有什么意义。进行了锁定后 => 只锁定了一个线程,就可以开始运行 lock 语句块。在 lock 语句块的最后,对象锁定被解除,另一个等待锁定的线程就可以获得改锁定块了。

最简单的方法就是在 类 JobDoTheJob() 方法中,将共享对象 state 转换为类型安全的对象 =>

    public void DoTheJob()
        {
        	for (int i = 0; i < 50000; i++)
            {
                lock (sharedState) //turn to thread safe object
                {
                    //=> use atomic operation
                    sharedState.State += 1; // => IncrementState()
                }
            }
        }

在一个地方使用 lock 语句并不意味着,访问对象的其他线程都正在等待,必须对每个访问共享状态的线程显示地使用同步功能。

​ 所以,还必须修改 SharedState 类的设计,并作为一个原子操作提供递增方式。锁定状态的递增还有一种更快的方式,下一小节 讲到的 *锁(Interlocked)

public class SharedState
    {
        private int state = 0;
        private readonly object syncRoot = new object();

        //public int State { get; set; }
        public int State
        {
            get { return state; }
            //set {} => IncrementState()
        }

        //对外提供递增的原子操作
        public int IncrementState()
        {
            lock(syncRoot)
            {
                return ++state;
            }
        }
    }

2.2 Interlocked 类

Interlocked 类用于使用变量的简单语句原子化。i ++ 不是线程安全的,它的操作包括从内存中获取一个值 => 给该值递增 1 => 再将它存储回内存。这些操作都可能会被线程调度器打断。Interlocked 类提供了以线程安全的方式递增、递减、交换和读取值的方法。

​ 与其他同步技术相比,使用 Interlocked 类会快很多。但是,它只能用于简单的同步问题。

​ 例如,不使用 lock 语句块锁定对 someState 变量的访问,把它设置为一个新值,以防它是空的,可以使用 Interlocked 类,它比较快。使用 Interlocked.Increment() 方法取代 lock 语句块的递增。

lock(this)
{
    if(someState == null)
    {
        someState = newState
	}
}
=>
Interlocked.CompareExchange<SomeState>(ref someState,newState,null);
/*******************************************************************/
public int State
{
    get
    {
        lock(this)
        {
            return ++ state;
        }
    }
}
=>
public int State
{
    get
    {
        Interlocked.Increment(ref state);
    }
    
}

2.3 Monitor 类

lock 语句由 C# 编译器解析为使用 Monitor 类。下面的 lock 语句,被解析为调用 Enter() 方法,该方法会一直等待,直到线程锁定对象为止。一次只有一个线程能够锁定对象。只要解除了锁定,线程就可以进入同步阶段。Monitor 类的 Exit() 方法解除了锁定。编译器把 **Exit()**方法放在 try 块的 finally 处理程序中,所以如果抛出了异常,就也会解除该锁定。

lock(obj){
    //handle
}
=>
Monitor.Enter(obj);
try{
    //handle
}finally{
    Monitor.Exit();
}

​ 与C#的 lock 语句相比,Monitor 类的主要优点是:可以添加一个等待被锁定的超时值。这样就不会无限等待被锁定,像下面的例子中使用 TryEnter() 方法,其中给它传一个超时值,指定等待被锁定的最长时间。如果 obj 被锁定,TryEnter() 方法就会把布尔类型的引用参数设置为 true,并同步地访问由对象 obj 锁定的状态。如果另一个线程锁定 obj 的时间超过了 500 毫秒,TryEnter() 方法就会把变量 lockTaken 设置为 false,线程不再等待,而是执行其他操作。

bool lockTaken = false;
Monitor.TryEnter(obj, 500, ref lockTaken);
if(lockTaken)
{
    try{
        //acquired the lock
    }finally{
        Monitor.Exit();
    }
}
else{
    //didn't get the lock, do sth else
}

2.4 SpinLock 结构

​ 提供一个相互排斥锁基元,在该基元中,尝试获取锁的线程将在重复检查的循环中等待,直至该锁变为可用为止。

​ 如果基于对象的锁定对象(Monitor)的系统开销由于垃圾回收而过高,就可以使用 SpinLock 结构。如果有大量的锁定(例如,列表中的每个节点都有一个锁定),且锁定的时间总是非常短,SpinLock 结构就很有用。应避免使用多个 SpinLock 结构,也不要调用任何可能阻塞的内容。

​ 除了体系结构上的区别之外,SpinLock 结构的用法非常类似 Monitor 类。获得锁锁定使用 Enter()TryEnter() 方法,释放锁用 Exit() 方法。SpinLock 结构还提供了属性 IsHeldIsHeldByCurrentThread,指定它当前是否是锁定的。

class SpinLockDemo
{

    // Demonstrates:
    //      Default SpinLock construction ()
    //      SpinLock.Enter(ref bool)
    //      SpinLock.Exit()
    static void SpinLockSample1()
    {
        SpinLock sl = new SpinLock();

        StringBuilder sb = new StringBuilder();

        // Action taken by each parallel job.
        // Append to the StringBuilder 10000 times, protecting
        // access to sb with a SpinLock.
        Action action = () =>
        {
            bool gotLock = false;
            for (int i = 0; i < 10000; i++)
            {
                gotLock = false;
                try
                {
                    sl.Enter(ref gotLock);
                    sb.Append((i % 10).ToString());
                }
                finally
                {
                    // Only give up the lock if you actually acquired it
                    if (gotLock) sl.Exit();
                }
            }
        };

        // Invoke 3 concurrent instances of the action above
        Parallel.Invoke(action, action, action);

        // Check/Show the results
        Console.WriteLine("sb.Length = {0} (should be 30000)", sb.Length);
        Console.WriteLine("number of occurrences of '5' in sb: {0} (should be 3000)",
            sb.ToString().Where(c => (c == '5')).Count());
    }
}

2.5 WaitHandle 基类

WaitHandle 是一个抽象基类,用于等待一个信号的设置。可以等待不同的信号,因为 WaitHandle 是一个基类,可以从中派生一些类。如 MutexEventWaitHandleSemaphore

​ 下面代码示例演示了在主线程使用类的静态和方法等待任务完成时,两个线程可以执行后台任务的方式 WaitAny WaitAll WaitHandle 。使用 WaitHandle 基类可以等待一个信号的出现(WaitOne() 方法)、等待必须发出信号的多个对象(WaitAll() 方法)、等待多个对象中的一个(**WaitAny()**方法)。WaitAll()WaitAny()WaitHandle 类的静态方法,接收一个 WaitHandle 参数数组。

using System;
using System.Threading;

public sealed class App
{
    // Define an array with two AutoResetEvent WaitHandles.
    static WaitHandle[] waitHandles = new WaitHandle[]
    {
        new AutoResetEvent(false),
        new AutoResetEvent(false)
    };

    // Define a random number generator for testing.
    static Random r = new Random();

    static void Main()
    {
        // Queue up two tasks on two different threads;
        // wait until all tasks are completed.
        DateTime dt = DateTime.Now;
        Console.WriteLine("Main thread is waiting for BOTH tasks to complete.");
        ThreadPool.QueueUserWorkItem(new WaitCallback(DoTask), waitHandles[0]);
        ThreadPool.QueueUserWorkItem(new WaitCallback(DoTask), waitHandles[1]);
        WaitHandle.WaitAll(waitHandles);
        // The time shown below should match the longest task.
        Console.WriteLine("Both tasks are completed (time waited={0})",
            (DateTime.Now - dt).TotalMilliseconds);

        // Queue up two tasks on two different threads;
        // wait until any tasks are completed.
        dt = DateTime.Now;
        Console.WriteLine();
        Console.WriteLine("The main thread is waiting for either task to complete.");
        ThreadPool.QueueUserWorkItem(new WaitCallback(DoTask), waitHandles[0]);
        ThreadPool.QueueUserWorkItem(new WaitCallback(DoTask), waitHandles[1]);
        int index = WaitHandle.WaitAny(waitHandles);
        // The time shown below should match the shortest task.
        Console.WriteLine("Task {0} finished first (time waited={1}).",
            index + 1, (DateTime.Now - dt).TotalMilliseconds);
    }

    static void DoTask(Object state)
    {
        AutoResetEvent are = (AutoResetEvent) state;
        int time = 1000 * r.Next(2, 10);
        Console.WriteLine("Performing a task for {0} milliseconds.", time);
        Thread.Sleep(time);
        are.Set();
    }
}

输出结果:

Main thread is waiting for BOTH tasks to complete.
Performing a task for 3000 milliseconds.
Performing a task for 2000 milliseconds.
Both tasks are completed (time waited=3080.382)

The main thread is waiting for either task to complete.
Performing a task for 9000 milliseconds.
Performing a task for 9000 milliseconds.
Task 1 finished first (time waited=9010.1025).

2.6 Mutex 类

Mutex (mutual exclusion 互斥)是 .NET Framework 中提供跨多个进程同步访问的一个类。它非常类似于 Monitor 类,因为它们都只有一个线程能拥有锁定。只有一个线程能获得互斥锁定,访问受互斥保护的同步区域。

class MutexSample
{
        // Create a new Mutex. The creating thread does not own the mutex.
        private static Mutex mut = new Mutex();
        private const int numIterations = 1;
        private const int numThreads = 3;

        public static void Test()
        {
            // Create the threads that will use the protected resource.
            for (int i = 0; i < numThreads; i++)
            {
                Thread newThread = new Thread(new ThreadStart(ThreadProc));
                newThread.Name = String.Format("Thread{0}", i + 1);
                newThread.Start();
            }

            // The main thread exits, but the application continues to
            // run until all foreground threads have exited.
        }

        private static void ThreadProc()
        {
            for (int i = 0; i < numIterations; i++)
            {
                UseResource();
            }
        }

        // This method represents a resource that must be synchronized
        // so that only one thread at a time can enter.
        private static void UseResource()
        {
            // Wait until it is safe to enter.
            Console.WriteLine("{0} is requesting the mutex",
                              Thread.CurrentThread.Name);
            mut.WaitOne();

            Console.WriteLine("{0} has entered the protected area",
                              Thread.CurrentThread.Name);

            // Place code to access non-reentrant resources here.

            // Simulate some work.
            Thread.Sleep(500);

            Console.WriteLine("{0} is leaving the protected area",
                Thread.CurrentThread.Name);

            // Release the Mutex.
            mut.ReleaseMutex();
            Console.WriteLine("{0} has released the mutex",
                Thread.CurrentThread.Name);
        }
}

​ 输出结果可以看出同一时间只有一个线程拥有锁定。

Thread1 is requesting the mutex
Thread2 is requesting the mutex
Thread3 is requesting the mutex
Thread1 has entered the protected area
Thread1 is leaving the protected area
Thread2 has entered the protected area
Thread1 has released the mutex
Thread2 is leaving the protected area
Thread2 has released the mutex
Thread3 has entered the protected area
Thread3 is leaving the protected area
Thread3 has released the mutex

​ 如果将 UserResource() 方法中的 WaitOne增加一个超时,结果又是不一样的;每个线程都调用 WaitOne(Int32)方法来获取互斥体。 如果超时间隔已过,则该方法将返回 false ,并且该线程既不会获取互斥体,也不会获得对互斥体保护的资源的访问权限。 ReleaseMutex 方法只由获取互斥体的线程调用。

private static void UseResource()
    {
        // Wait until it is safe to enter, and do not enter if the request times out.
        Console.WriteLine("{0} is requesting the mutex", Thread.CurrentThread.Name);
        if (mut.WaitOne(1000)) {
           Console.WriteLine("{0} has entered the protected area", 
               Thread.CurrentThread.Name);
   
           // Place code to access non-reentrant resources here.
   
           // Simulate some work.
           Thread.Sleep(5000);
   
           Console.WriteLine("{0} is leaving the protected area", 
               Thread.CurrentThread.Name);
   
           // Release the Mutex.
              mut.ReleaseMutex();
           Console.WriteLine("{0} has released the mutex", 
                             Thread.CurrentThread.Name);
        }
        else {
           Console.WriteLine("{0} will not acquire the mutex", 
                             Thread.CurrentThread.Name);
        }
    }

运行结果可以看出,只有一个线程获取到了资源:

Thread3 is requesting the mutex
Thread1 is requesting the mutex
Thread2 is requesting the mutex
Thread2 has entered the protected area
Thread1 will not acquire the mutex
Thread3 will not acquire the mutex
Thread2 is leaving the protected area
Thread2 has released the mutex

​ 不过Mutex更多用在系统的进程互斥,只能开启一个程序。下面的代码示例演示如何使用已命名的 mutex 在进程或线程之间发出信号。 从两个或更多命令窗口中运行该程序。 每个进程都将创建一个 Mutex 对象,该对象表示已命名的互斥体 “MyMutex”。 命名的 mutex 是系统对象。 在此示例中,其生存期由表示它的对象的生存期界定 Mutex 。 当第一个进程创建其本地对象时,将创建已命名的 mutex Mutex ,并在所有 Mutex 表示它的对象被释放时销毁。 命名的 mutex 最初由第一个进程拥有。 第二个进程和任何后续进程将等待前面的进程释放已命名的互斥体。

参数

  • initiallyOwned Boolean

如果为 true,则给予调用线程已命名的系统互斥体的初始所属权(如果已命名的系统互斥体是通过此调用创建的);否则为 false

  • name String

如果要与其他进程共享同步对象,则为名称;否则为 null 或空字符串。 该名称区分大小写。

  • createdNew Boolean

在此方法返回时,如果创建了局部互斥体(即,如果 namenull 或空字符串)或指定的命名系统互斥体,则包含布尔值 true

public class Test
{
    public static void Main()
    {
        
        bool requestInitialOwnership = true;
        bool mutexWasCreated;

        Mutex m = new Mutex(requestInitialOwnership, 
                            "MyMutex", 
                            out mutexWasCreated);
        if (!(requestInitialOwnership && mutexWasCreated))
        {
            Console.WriteLine("Waiting for the named mutex.");
            //其它已开启的进程等待获取锁定
            m.WaitOne();
        }

        Console.WriteLine("This process owns the named mutex. " +
            "Press ENTER to release the mutex and exit.");
        Console.ReadLine();

       
        m.ReleaseMutex();
    }
}

2.7 Semaphore 类

​ 信号量也很类似于互斥,其区别是,信号量可以多个线程使用。信号量是一种计数的互斥锁定。使用信号量,可以定于允许同时受信号量锁定保护的资源的线程个数。如果需要限制可以访问资源的线程数,信号量就很有用。例如,如果系统有3个物理端口可以使用,就允许3个线程同时访问 I/O 端口,但第4个线程就需要等待前3个任意一个释放资源。

public class SemaphoreSample
    {
        private static Semaphore _pool;

        // A padding interval to make the output more orderly.
        private static int _padding;

        public static void Test()
        {
            int taskCount = 6;
            _pool = new Semaphore(3, 3);

            var tasks = new Task[taskCount];
            // Create and start five numbered threads. 
            //
            for (int i = 0; i < taskCount; i++)
            {
                int j = i;
                tasks[j] = Task.Run(() => Worker(j));
            }
            Task.WaitAll(tasks);

            Console.WriteLine("All tasks finished");
        }

        private static void Worker(object num)
        {
            _pool.WaitOne();

            // A padding interval to make the output more orderly.
            int padding = Interlocked.Add(ref _padding, 100);

            Console.WriteLine("Thread {0} enters the semaphore.", num);

            Thread.Sleep(1000 + padding);

            Console.WriteLine("Thread {0} releases the semaphore.", num);
            Console.WriteLine("Thread {0} previous semaphore count: {1}",
                num, _pool.Release());
        }
    }

运行结果如下,可以骄傲到有3个线程很快被锁定。ID 为0、1和3的线程需要等待。该等待会重复进行,直到其中一个被锁定的线程之一解除了信号量。

Thread 2 enters the semaphore.
Thread 5 enters the semaphore.
Thread 4 enters the semaphore.
Thread 2 releases the semaphore.
Thread 1 enters the semaphore.
Thread 2 previous semaphore count: 0
Thread 4 releases the semaphore.
Thread 4 previous semaphore count: 0
Thread 0 enters the semaphore.
Thread 5 releases the semaphore.
Thread 5 previous semaphore count: 0
Thread 3 enters the semaphore.
Thread 1 releases the semaphore.
Thread 1 previous semaphore count: 0
Thread 0 releases the semaphore.
Thread 0 previous semaphore count: 1
Thread 3 releases the semaphore.
Thread 3 previous semaphore count: 2
All tasks finished

​ 下面的代码示例演示命名信号量的跨进程行为。 该示例创建一个名为的信号量,其最大计数为5,初始计数为5。 该程序对方法进行了三次调用 WaitOne 。 因此,如果从两个命令窗口运行已编译的示例,则第三次调用时会阻止第二个副本 WaitOne 。 释放程序的第一个副本中的一个或多个项以解除阻止第二个副本。

using System;
using System.Threading;

public class Example
{
    public static void Main()
    {
        // Create a Semaphore object that represents the named 
        // system semaphore "SemaphoreExample3". The semaphore has a
        // maximum count of five. The initial count is also five. 
        // There is no point in using a smaller initial count,
        // because the initial count is not used if this program
        // doesn't create the named system semaphore, and with 
        // this method overload there is no way to tell. Thus, this
        // program assumes that it is competing with other
        // programs for the semaphore.
        //
        Semaphore sem = new Semaphore(5, 5, "SemaphoreExample3");

        // Attempt to enter the semaphore three times. If another 
        // copy of this program is already running, only the first
        // two requests can be satisfied. The third blocks. Note 
        // that in a real application, timeouts should be used
        // on the WaitOne calls, to avoid deadlocks.
        //
        sem.WaitOne();
        Console.WriteLine("Entered the semaphore once.");
        sem.WaitOne();
        Console.WriteLine("Entered the semaphore twice.");
        sem.WaitOne();
        Console.WriteLine("Entered the semaphore three times.");

        // The thread executing this program has entered the 
        // semaphore three times. If a second copy of the program
        // is run, it will block until this program releases the 
        // semaphore at least once.
        //
        Console.WriteLine("Enter the number of times to call Release.");
        int n;
        if (int.TryParse(Console.ReadLine(), out n))
        {
            sem.Release(n);
        }

        int remaining = 3 - n;
        if (remaining > 0)
        {
            Console.WriteLine("Press Enter to release the remaining " +
                "count ({0}) and exit the program.", remaining);
            Console.ReadLine();
            sem.Release(remaining);
        }
    }
}

输出结果 =>

DOS 1 =>
Entered the semaphore twice.
Entered the semaphore three times.
Enter the number of times to call Release.
DOS 2 =>
Entered the semaphore twice.
Entered the semaphore three times.

2.8 EventWaitHandle 类

派生

System.Threading.AutoResetEvent 一次释放一个线程

System.Threading.ManualResetEvent 释放所有线程

​ 与互斥对象意义,事件也是一个系统范围内的资源同步方法。为了从托管代码中使用系统事件,.NET 提供了 ManualResetEventAutoResetEventManualResetEventSlimCountdownEvent 类。

​ 可以使用事件同之其它任务:这里有一些数据,并完成了一些操作等。事件可以发信号,也可以不发信号。使用前面介绍的 WaitHandle 类,任务可以等待处于发信号的状态。

下面的代码示例使用 SignalAndWait(WaitHandle, WaitHandle) 方法重载,以允许主线程向阻止的线程发出信号,并等待线程完成任务。该示例启动五个线程,并允许它们阻止 EventWaitHandle 使用标志创建的 EventResetMode.AutoReset ,然后在每次用户按 ENTER 键时释放一个线程。 然后,该示例将另一线程排队,并通过使用标志创建的所有线程释放它们 EventWaitHandle. EventResetMode.ManualReset

using System;
using System.Threading;

public class Example
{
    // The EventWaitHandle used to demonstrate the difference
    // between AutoReset and ManualReset synchronization events.
    //
    private static EventWaitHandle ewh;

    // A counter to make sure all threads are started and
    // blocked before any are released. A Long is used to show
    // the use of the 64-bit Interlocked methods.
    //
    private static long threadCount = 0;

    // An AutoReset event that allows the main thread to block
    // until an exiting thread has decremented the count.
    //
    private static EventWaitHandle clearCount = 
        new EventWaitHandle(false, EventResetMode.AutoReset);

    [MTAThread]
    public static void Main()
    {
        // Create an AutoReset EventWaitHandle.
        //
        ewh = new EventWaitHandle(false, EventResetMode.AutoReset);

        // Create and start five numbered threads. Use the
        // ParameterizedThreadStart delegate, so the thread
        // number can be passed as an argument to the Start 
        // method.
        for (int i = 0; i <= 4; i++)
        {
            Thread t = new Thread(
                new ParameterizedThreadStart(ThreadProc)
            );
            t.Start(i);
        }

        // Wait until all the threads have started and blocked.
        // When multiple threads use a 64-bit value on a 32-bit
        // system, you must access the value through the
        // Interlocked class to guarantee thread safety.
        //
        while (Interlocked.Read(ref threadCount) < 5)
        {
            Thread.Sleep(500);
        }

        // Release one thread each time the user presses ENTER,
        // until all threads have been released.
        //
        while (Interlocked.Read(ref threadCount) > 0)
        {
            Console.WriteLine("Press ENTER to release a waiting thread.");
            Console.ReadLine();

            // SignalAndWait signals the EventWaitHandle, which
            // releases exactly one thread before resetting, 
            // because it was created with AutoReset mode. 
            // SignalAndWait then blocks on clearCount, to 
            // allow the signaled thread to decrement the count
            // before looping again.
            //
            WaitHandle.SignalAndWait(ewh, clearCount);
        }
        Console.WriteLine();

        // Create a ManualReset EventWaitHandle.
        //
        ewh = new EventWaitHandle(false, EventResetMode.ManualReset);

        // Create and start five more numbered threads.
        //
        for(int i=0; i<=4; i++)
        {
            Thread t = new Thread(
                new ParameterizedThreadStart(ThreadProc)
            );
            t.Start(i);
        }

        // Wait until all the threads have started and blocked.
        //
        while (Interlocked.Read(ref threadCount) < 5)
        {
            Thread.Sleep(500);
        }

        // Because the EventWaitHandle was created with
        // ManualReset mode, signaling it releases all the
        // waiting threads.
        //
        Console.WriteLine("Press ENTER to release the waiting threads.");
        Console.ReadLine();
        ewh.Set();
    }

    public static void ThreadProc(object data)
    {
        int index = (int) data;

        Console.WriteLine("Thread {0} blocks.", data);
        // Increment the count of blocked threads.
        Interlocked.Increment(ref threadCount);

        // Wait on the EventWaitHandle.
        ewh.WaitOne();

        Console.WriteLine("Thread {0} exits.", data);
        // Decrement the count of blocked threads.
        Interlocked.Decrement(ref threadCount);

        // After signaling ewh, the main thread blocks on
        // clearCount until the signaled thread has 
        // decremented the count. Signal it now.
        //
        clearCount.Set();
    }
}

输出结果:

AutoReset =>
Thread 1 blocks.
threadCount => 1
Thread 3 blocks.
threadCount => 2
Thread 0 blocks.
threadCount => 3
Thread 2 blocks.
threadCount => 4
threadCount => 0
Thread 4 blocks.
threadCount => 5
threadCount => 5
Press ENTER to release a waiting thread.

Thread 0 exits.
threadCount=> 4
threadCount => 4
Press ENTER to release a waiting thread.

Thread 1 exits.
threadCount=> 3
threadCount => 3
Press ENTER to release a waiting thread.

Thread 2 exits.
threadCount=> 2
threadCount => 2
Press ENTER to release a waiting thread.

Thread 3 exits.
threadCount=> 1
threadCount => 1
Press ENTER to release a waiting thread.

Thread 4 exits.
threadCount=> 0

ManualReset =>
Thread 0 blocks.
threadCount => 1
Thread 1 blocks.
threadCount => 2
Thread 2 blocks.
threadCount => 3
Thread 3 blocks.
threadCount => 4
Thread 4 blocks.
threadCount => 5
Press ENTER to release the waiting threads.

Thread 0 exits.
threadCount=> 4
Thread 1 exits.
threadCount=> 3
Thread 3 exits.
threadCount=> 2
Thread 2 exits.
threadCount=> 1
Thread 4 exits.
threadCount=> 0

2.9 Barrier 类

​ 对于同步,Barrier 类非常适用于其中工作又多个任务分支且以后又需要合并工作的情况。Barrier 类用于需要同步的参与者。**一个任务时,就可以动态的添加其他参与者,例如,从父任务中创建子任务。参与者继续之前,可以等待所有其他参与者完成其工作。

​ 下面的应用程序使用一个包含 2 000 000 个字符串的集合。使用多个任务遍历该集合,并统计以 a - z 开头的字符串个数。

FillData() 方法创建一个集合,并用随机字符串填充它。

 public static IEnumerable<string> FillData(int size)
        {
            var data = new List<string>(size);
            var r = new Random();
            for (int i = 0; i < size; i++)
            {
                data.Add(GetString(r));
            }
            return data;
        }

        public static string GetString(Random r)
        {
            var sb = new StringBuilder();
            for (int i = 0; i < 6; i++)
            {
                //Max 26 not include 97 => a
                sb.Append((char)(r.Next(26) + 97));
            }
            return sb.ToString();
        }

CalculationTask() 方法定义了任务执行的作业。通过参数接收一个包含4项的元组。第三个参数时对 Barrier 实例的引用。任务完成其作业时,任务就会使用 RemoveParticipant() 方法从 Barrier 类中删除它自己。

/// <summary>
/// 计算传入字符串数组每个字符串开头的数量
/// </summary>
/// <param name="jobNumber">当前任务编号</param>
/// <param name="partitionSize">处理字符串长度</param>
/// <param name="barrier">Barrier实例</param>
/// <param name="coll">要处理的字符串</param>
/// <returns></returns>
static int[] CalculationTask(int jobNumber, int partitionSize, Barrier barrier, IList<string> coll)
{
            Console.WriteLine("CalculationTask {0} Started", jobNumber);
            List<string> data = new List<string>(coll);
            int start = jobNumber * partitionSize;
            int end = start + partitionSize;
            Console.WriteLine("Task {0} : partition from {1} to {2}", Task.CurrentId, start, end);

            int[] charCount = new int[26];
            for (int j = start; j < end; j++)
            {
                char c = data[j][0];
                charCount[c - 97]++;//如果c = 'a',result[97-97]++ = result[0]++,即代表a出现了一次,int[0]的值加1
            }
            barrier.SignalAndWait();
            Console.WriteLine("Calculation completed from task {0}. {1} " + "times a, {2} times z", Task.CurrentId, charCount[0], charCount[25]);

            barrier.RemoveParticipant();
            Console.WriteLine("Task {0} removed from barrier, " + "remaining participants {1} ", Task.CurrentId, barrier.ParticipantsRemaining);

            Console.WriteLine("CalculationTask {0} Completed", jobNumber);
            return charCount;
}

​ 在 Main() 方法中创建一个 Barrier 实例。在构造函数中,可以指定参与者的数量。使用 Task.Run 创建两个任务,把遍历集合的任务分为两个部分,第第一个任务遍历 0~1000000,第二个任务遍历 1000001~2000000。启动该任务后,使用 **SignalAndWait()**方法,Main() 方法在完成时发出信号,并等待所有其他参与者或者发布完成的信号。一旦所有参与者都准备好,就提取任务结果,并使用 Zip() 扩展方法把结果进行合并。

static void Main(string[] args)
{

            const int numberTasks = 2;
            const int partitionSize = 1000000;
            Task<int[]>[] tasks = new Task<int[]>[numberTasks];
            //Barrier barrier = new Barrier(numberTasks + 1);
            var data = new List<string>(FillData(numberTasks * partitionSize));
            //指定Barrier  参与者数量
            var barrier = new Barrier(numberTasks, (c) =>
            {
                Console.WriteLine("phase {0} completed", c.CurrentPhaseNumber + 1);
            });
            for (int i = 0; i < numberTasks; i++)
            {
                Console.WriteLine("Main - start CalculationTask {0}", i);
                int jobNumber = i;
                tasks[i] = Task.Run(() => CalculationTask(jobNumber, partitionSize, barrier, data));
            }
            barrier.SignalAndWait();
            var resultCollection = tasks[0].Result.Zip(tasks[1].Result, (c1, c2) =>
            {
                return c1 + c2;
            });

            char ch = 'a';
            int sum = 0;
            foreach (var x in resultCollection)
            {
                Console.WriteLine("{0}, count: {1}", ch++, x);
                sum += x;
            }
            Console.WriteLine("Main finished {0}", sum);
            Console.WriteLine("remaining {0}", barrier.ParticipantsRemaining);
}

运行结果如下:

Main - start CalculationTask 0
Main - start CalculationTask 1
CalculationTask 0 Started
CalculationTask 1 Started
Task 1 : partition from 0 to 1000000
Task 2 : partition from 1000000 to 2000000
phase 1 completed
Calculation completed from task 1. 38515 times a, 38411 times z
Task 1 removed from barrier, remaining participants 1
CalculationTask 0 Completed
phase 2 completed
Calculation completed from task 2. 38194 times a, 38614 times z
Task 2 removed from barrier, remaining participants 0
CalculationTask 1 Completed
a, count: 76709
b, count: 76771
c, count: 77353
d, count: 77360
e, count: 76957
f, count: 76845
g, count: 76934
h, count: 77433
i, count: 76878
j, count: 76902
k, count: 77124
l, count: 77137
m, count: 76811
n, count: 76685
o, count: 76364
p, count: 76997
q, count: 76940
r, count: 76516
s, count: 76977
t, count: 76868
u, count: 77091
v, count: 77199
w, count: 76873
x, count: 76433
y, count: 76818
z, count: 77025
Main finished 2000000
remaining 0

2.10 ReaderWriterLockSlim 类

​ 为了使用锁定机制允许锁定多个读取器(而不是一个写入器)访问某个资源,可以使用 ReaderWriterLockSlim 类。这个类提供了一个锁定功能,如果没有写入器锁定资源,就允许多个读取器访问资源,但只能有一个写入器锁定该资源。

ReaderWriterLockSlim 类的属性可获得读取阻塞或不阻塞的锁定,如 EnterReadLock()TryEnterReadLock() 方法。还可以使用 EnterWriteLock()TryEnterWriteLock() 方法获得写入锁定。如果任务先读取资源,之后写入资源,它就可以使用 EnterUpgradaleReadLock()TryEnterUpgradableReadLock() 方法获得可升级的读取锁定。有了这个锁定,就可以获得写入锁定,而无需释放读取锁定。

​ 这个类的几个属性提供了当前锁定的相关信息,如 CurrentReadCountWaitingReadCountWaitingReadCountWaitingUpgradableReadCountWaitingWriteCount

​ 下面的示例程序创建了一个包含 6 项的集合和一个 ReaderWriterLockSlim 对象。ReaderMethod 方法获得一个读取锁定,读取列表中的所有项,并把它们写到控制台中。WriterMethod方法获得一个写入锁定,以改变集合的所有值。在 Main() 方法中,启动6个线程,以调用 ReaderMethod()ReaderMethod() 方法

class Program
    {
        private static List<int> items = new List<int> { 0, 1, 2, 3, 4, 5 };
        private static ReaderWriterLockSlim rwl = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

        static void ReaderMethod(object reader)
        {
            try
            {
                rwl.EnterReadLock();
                //rwl.EnterUpgradeableReadLock();
                for (int i = 0; i < items.Count; i++)
                {
                    Console.WriteLine("{0} => Reader {1}, loop: {2}, item: {3}", DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss fff"), reader, i, items[i]);
                    Thread.Sleep(40);
                }
            }
            finally
            {
                rwl.ExitReadLock();
                //rwl.ExitUpgradeableReadLock();
            }
        }

        static void WriterMethod(object writer)
        {
            try
            {
                while(!rwl.TryEnterWriteLock(50))
                {
                    Console.WriteLine("{0} => Writer {1} waiting for the writer lock",DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss fff"), writer);
                    Console.WriteLine("{0} => Current reader count {1}", DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss fff"), rwl.CurrentReadCount);
                }
                Console.WriteLine("{0} => Writer {1} accquired the lock",DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss fff"), writer);
                for (int i = 0; i < items.Count; i++)
                {
                    items[i]++;
                    Thread.Sleep(50);
                }
                Console.WriteLine("{0} => Writer {1} finished", DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss fff"), writer);
            }
            finally
            {
                rwl.ExitWriteLock();
            }
        }
        static void Main(string[] args)
        {
            var taskFactory = new TaskFactory(TaskCreationOptions.LongRunning, TaskContinuationOptions.None);
            var tasks = new Task[6];
            
            tasks[0] = taskFactory.StartNew(WriterMethod, 1);
            Thread.Sleep(1000);//for Writer 1 accquired the lock
            tasks[1] = taskFactory.StartNew(ReaderMethod, 1);
            tasks[2] = taskFactory.StartNew(ReaderMethod, 2);

            tasks[3] = taskFactory.StartNew(WriterMethod, 2);
            tasks[4] = taskFactory.StartNew(ReaderMethod, 3);
            tasks[5] = taskFactory.StartNew(ReaderMethod, 4);

            for (int i = 0; i < tasks.Length; i++)
            {
                tasks[i].Wait();
            }
            //Task.WaitAll(tasks);
            Console.ReadLine();
        }
}

​ 运行这个程序,可以看到第一个写入器先获得锁定。第二个写入器和所有的读取器需要等待。接着读取器可以同时工作,而第二个写入器仍在等待资源。

2021-03-29 12:44:10 789 => Writer 1 accquired the lock
2021-03-29 12:44:11 204 => Writer 1 finished
2021-03-29 12:44:11 796 => Reader 1, loop: 0, item: 1
2021-03-29 12:44:11 796 => Reader 2, loop: 0, item: 1
2021-03-29 12:44:11 805 => Reader 3, loop: 0, item: 1
2021-03-29 12:44:11 839 => Reader 1, loop: 1, item: 2
2021-03-29 12:44:11 855 => Reader 3, loop: 1, item: 2
2021-03-29 12:44:11 855 => Reader 2, loop: 1, item: 2
2021-03-29 12:44:11 872 => Reader 4, loop: 0, item: 1
2021-03-29 12:44:11 872 => Writer 2 waiting for the writer lock
2021-03-29 12:44:11 873 => Current reader count 4
2021-03-29 12:44:11 887 => Reader 1, loop: 2, item: 3
2021-03-29 12:44:11 903 => Reader 3, loop: 2, item: 3
2021-03-29 12:44:11 903 => Reader 2, loop: 2, item: 3
2021-03-29 12:44:11 919 => Reader 4, loop: 1, item: 2
2021-03-29 12:44:11 935 => Reader 1, loop: 3, item: 4
2021-03-29 12:44:11 935 => Writer 2 waiting for the writer lock
2021-03-29 12:44:11 936 => Current reader count 4
2021-03-29 12:44:11 951 => Reader 3, loop: 3, item: 4
2021-03-29 12:44:11 951 => Reader 2, loop: 3, item: 4
2021-03-29 12:44:11 967 => Reader 4, loop: 2, item: 3
2021-03-29 12:44:11 983 => Reader 1, loop: 4, item: 5
2021-03-29 12:44:11 998 => Writer 2 waiting for the writer lock
2021-03-29 12:44:11 998 => Reader 3, loop: 4, item: 5
2021-03-29 12:44:11 998 => Reader 2, loop: 4, item: 5
2021-03-29 12:44:12 000 => Current reader count 4
2021-03-29 12:44:12 013 => Reader 4, loop: 3, item: 4
2021-03-29 12:44:12 030 => Reader 1, loop: 5, item: 6
2021-03-29 12:44:12 045 => Reader 3, loop: 5, item: 6
2021-03-29 12:44:12 046 => Reader 2, loop: 5, item: 6
2021-03-29 12:44:12 053 => Writer 2 waiting for the writer lock
2021-03-29 12:44:12 053 => Current reader count 4
2021-03-29 12:44:12 056 => Reader 4, loop: 4, item: 5
2021-03-29 12:44:12 102 => Reader 4, loop: 5, item: 6
2021-03-29 12:44:12 120 => Writer 2 waiting for the writer lock
2021-03-29 12:44:12 121 => Current reader count 1
2021-03-29 12:44:12 152 => Writer 2 accquired the lock
2021-03-29 12:44:12 532 => Writer 2 finished