.Net多线程编程—同步机制
1.简介
新的轻量级同步原语:Barrier,CountdownEvent,ManualResetEventSlim,SemaphoreSlim,SpinLock,SpinWait。轻量级同步原语只能用在一个进程内。而相应的那些重量级版本支持跨进程的同步。
2.Barrier
主要成员
1)public Barrier(int participantCount, Action<Barrier> postPhaseAction);构造 函数,participantCount:参与的线程个数(参与者的个数), postPhaseAction每个阶段后执行的操作。
2) public void SignalAndWait();发出信号,表示参与者已达到屏障并等待所有其他参与者也达到屏障。
3) public bool SignalAndWait(int millisecondsTimeout); 如果所有参与者都已在指定时间内达到屏障,则为 true;否则为 false。
4) public int ParticipantCount { get; } 获取屏障中参与者的总数。
5) public long CurrentPhaseNumber { get; internal set; }获取屏障的当前阶段编号。
6)public int ParticipantsRemaining { get; }获取屏障中尚未在当前阶段发出信号的参与者的数量。每当新阶段开始时,这个值等于ParticipantCount ,每当有参与者调用这个属性时,其减一。
注意:
1) 每当屏障(Barrier实例)接收到来自所有参与者的信号之后,屏障就会递增其阶段数,运行构造函数中指定的动作,并且解除阻塞每一个参与者。
2)Barrier使用完要调用Dispose()方法释放资源
3.CountdownEvent
主要成员:
1) public int InitialCount { get; } 获取设置事件时最初的信号数。
1) public CountdownEvent(int initialCount);
2) public bool Signal();向 CountdownEvent 注册信号,同时减小CurrentCount的值。
3) public void Reset(int count);将 System.Threading.CountdownEvent.InitialCount 属性重新设置为指定值。
注意:
一定要确保每个参与工作的线程都调用了Signal,如果有至少一个没有调用,那么任务会永久阻塞。所以一般在finally块中调用Signal是个好习惯。
4.ManualResetEvent与ManualResetEventSlim
ManualResetEvent:可实现跨进程或AppDomain的同步。
主要成员:
1)public bool Reset();将事件状态设置为非终止状态,导致线程阻止,返回值指示操作是否成功。
2)public bool Set();将事件状态设置为终止状态,允许一个或多个等待线程继续,返回值指示操作是否成功。
ManualResetEventSlim:不可应用于跨进程的同步。
主要成员:
1) public bool IsSet { get; }获取是否设置了事件。
2) public void Reset();将事件状态设置为非终止状态,从而导致线程受阻,返回值指示操作是否成功。
3)public bool Set();将事件状态设置为终止状态,允许一个或多个等待线程继续,返回值指示操作是否成功。
4)public void Wait();阻止当前线程,直到设置了当前 ManualResetEventSlim 为止。
5) public void Dispose();释放资源。
6)public ManualResetEventSlim(bool initialState, int spinCount);
5.Semaphore与SemaphoreSlim
Semaphore:可实现跨进程或AppDomain的同步,可使用WaitHandle操作递减信号量的计数。
主要成员:
1)public Semaphore(int initialCount, int maximumCount);
2)public int Release();退出信号量并返回前一个计数。
3)public virtual bool WaitOne(); 阻止当前线程,直到当前 System.Threading.WaitHandle 收到信号。 如果当前实例收到信号,则为 true。 如果当前实例永远收不到信号,则 System.Threading.WaitHandle.WaitOne(System.Int32,System.Boolean)永不返回。
注意:
使用完Semaphore立即调用Dispose()方法释放资源。
SemaphoreSlim:不可实现跨进程或AppDomain的同步,不可使用WaitHandle操作递减信号量的计数。
主要成员:
1)public SemaphoreSlim(int initialCount, int maxCount);
2)public int CurrentCount { get; } 获取将允许进入 System.Threading.SemaphoreSlim 的线程的数量。
3)public int Release();退出 System.Threading.SemaphoreSlim 一次。
4)public void Wait();阻止当前线程,直至它可进入 System.Threading.SemaphoreSlim 为止。
5)public WaitHandle AvailableWaitHandle { get; }返回一个可用于在信号量上等待的 System.Threading.WaitHandle。
注意:
使用完SemaphoreSlim立即调用Dispose()方法释放资源。
6.SpinLock:自旋锁,对SpinWait的包装
主要成员:
1)public void Enter(ref bool lockTaken); 采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 lockTaken 以确定是否已获取锁。
2)public void Exit(bool useMemoryBarrier);释放锁
说明:
1)不要将SpinLock声明为只读字段。
2)确保每次任务结束后都释放锁。
7.SpinWait:基于自旋的等待
主要成员:
1)public static void SpinUntil(Func<bool> condition);在指定条件得到满足之前自旋。
2)public static bool SpinUntil(Func<bool> condition, int millisecondsTimeout);在指定条件得到满足或指定超时过期之前自旋。
说明:
1)适用情形:等待某个条件满足需要的时间很短,并且不希望发生昂贵的上下文切换。
2)内存开销非常小。其是一个结构体,不会产生不必要的内存开销。
3)如果自旋时间过长,SpinWait会让出底层线程的时间片并触发上下文切换。
8.Look:互斥锁
说明:
1)通过使用lock关键字可以获得一个对象的互斥锁。
2)使用lock,这会调用System.Threading.Monitor.Enter(object obj, ref bool lockTaken)和System.Threading.Monitor.Exit(object obj)方法。
3)不要对值类型使用Lock
4)避免锁定类的外部对象,避免跨成员或类的边界获得和释放一个锁,避免获得锁的时候调用未知代码。
9.Monitor
主要成员:
1)public static void Enter(object obj, ref bool lockTaken);获取指定对象上的排他锁,并自动设置一个值,指示是否获取了该锁。
2)public static void Exit(object obj);释放指定对象上的排他锁。
3)public static void TryEnter(object obj, int millisecondsTimeout, ref bool lockTaken);在指定的毫秒数中,尝试获取指定对象上的排他锁,并自动设置一个值,指示是否得到了该锁。
说明:
1)不要对值类型使用Monitor。
2)避免锁定类的外部对象,避免跨成员或类的边界获得和释放一个锁,避免获得锁的时候调用未知代码。
10.volatile修饰符
作用:
当共享变量被不同的线程访问和更新且没有锁和原子操作的时候,最新的值总能在共享变量中表现出来。
注意:
1)可以将这个修饰符用于类和struct的字段,但不能声明使用volatile关键字的局部变量。
2)Volatile可修饰的类型为:整型,bool,带有整型的枚举,引用类型,推到为引用类型的泛型类型,不安全上下文中的指针类型以及表示指针或者句柄的平台相关类型。
11.Interlocked:为多任务或线程共享的变量提供原子操作
主要成员:
1)public static int Increment(ref int location);以原子操作的形式递增指定变量的值并存储结果。
2)public static int Add(ref int location1, int value);对两个 32 位整数进行求和并用和替换第一个整数,上述操作作为一个原子操作完成。
3)public static float CompareExchange(ref float location1, float value, float comparand); 比较两个单精度浮点数是否相等,如果相等,则替换其中一个值。
4)public static int Decrement(ref int location);以原子操作的形式递减指定变量的值并存储结果。
注意:
最大的好处:开销低,效率高。
12 使用模式
1)Barrier
1 public static void BarrierTest1() 2 { 3 //构造函数的参数participantCount表示参与者的数量。 4 //注意:父线程也是一个参与者,所以两个任务,但是Barrier的participantCount为3 5 //注意:无法保证任务1和任务2完成的先后顺序。 6 //Barrier(int participantCount, Action<Barrier> postPhaseAction);也可使 用此方法 7 //当所有参与者都已到达屏障后,执行要处理的任务,即对两个任务产生的数据统一处理的过程可放在此处执行。 8 using (Barrier bar = new Barrier(3)) 9 { 10 Task.Factory.StartNew(() => 11 { 12 13 //具体业务 14 15 //当业务完成时,执行下面这行代码;发出信号,表明任务已完成,并等待其他参与者 16 bar.SignalAndWait(); 17 18 }); 19 20 Task.Factory.StartNew(() => 21 { 22 23 //具体业务 24 25 //当业务完成时,执行下面这行代码;发出信号,表明任务已完成,并等待其他参与者 26 bar.SignalAndWait(); 27 28 }); 29 30 //保证上面两个任务都能完成才执行bar.SignalAndWait();这一句之后的代码 31 bar.SignalAndWait(); 32 //当上述两个任务完成后,对两个任务产生的数据进行统一处理。 33 34 } 35 36 } 37 38 public static void BarrierTest2() 39 { 40 //构造函数的参数participantCount表示参与者的数量。 41 using (Barrier bar = new Barrier(3)) 42 { 43 Task.Factory.StartNew(() => 44 { 45 46 //具体业务 47 48 //当业务完成时,执行下面这行代码;移除一个参与者 49 //注意:bar.SignalAndWait();与bar.RemoveParticipant();可以混用 50 bar.RemoveParticipant(); 51 52 }); 53 54 Task.Factory.StartNew(() => 55 { 56 57 //具体业务 58 59 //当业务完成时,执行下面这行代码;移除一个参与者 60 bar.RemoveParticipant(); 61 62 }); 63 64 bar.SignalAndWait(); 65 //当上述两个任务完成后,对两个任务产生的数据进行统一处理。 66 } 67 }
2)CountdownEvent
1 public static void CountdownEventTest() 2 { 3 //注意初始化信号数等于并行的任务数 4 int initialCount = N; 5 using (CountdownEvent cd = new CountdownEvent(initialCount)) 6 { 7 //多个并行任务,完成一个减少一个信号 8 for (int i = 0; i < N; i++) 9 { 10 Task.Factory.StartNew(() => 11 { 12 try 13 { 14 //真正的业务 15 } 16 finally 17 { 18 //确保不论何种情况都能减少信号量,防止死循环 19 cd.Signal(); 20 } 21 }); 22 } 23 24 //等待上述多个任务执行完毕 25 cd.Wait(); 26 } 27 }
3)ManualResetEvent与ManualResetEventSlim
1 public static void ManualResetEventTest() 2 { 3 ManualResetEvent mre = new ManualResetEvent(false); 4 ManualResetEvent mre1 = new ManualResetEvent(false); 5 6 try 7 { 8 Task.Factory.StartNew(() => 9 { 10 //业务 11 mre.Set(); 12 }); 13 14 Task.Factory.StartNew(() => 15 { 16 mre.WaitOne(); 17 18 //使用任务1的数据 19 20 mre1.Set(); 21 22 }); 23 24 //等待任务全部执行完 25 mre1.WaitOne(); 26 } 27 finally 28 { 29 mre.Dispose(); 30 mre1.Dispose(); 31 } 32 } 33 //注意:本示例并不是一个最佳实践,目的在于演示ManualResetEventSlim 34 //当没有更好的协调机制时,可考虑使用本示例 35 public static void ManualResetEventSlimTest1() 36 { 37 ManualResetEventSlim mreslim = new ManualResetEventSlim(); 38 ManualResetEventSlim mreslim1 = new ManualResetEventSlim(); 39 try 40 { 41 Task.Factory.StartNew(() => 42 { 43 mreslim.Set(); 44 45 //业务 46 47 mreslim.Reset(); 48 49 }); 50 51 Task.Factory.StartNew(() => 52 { 53 //当mreslim.Set()被调用时,mreslim.Wait()立即返回,即解除阻塞。 54 mreslim.Wait(); 55 //直到mreslim.Reset()被调用,循环才会结束 56 while (mreslim.IsSet) 57 { 58 //业务 59 } 60 mreslim1.Set(); 61 }); 62 63 //等待第二个任务完成 64 mreslim1.Wait(); 65 } 66 finally 67 { 68 mreslim.Dispose(); 69 mreslim1.Dispose(); 70 } 71 }
4)Semaphore与SemaphoreSlim
1 public static void SemaphoreSlimTest() 2 { 3 int initialCount = 10;//可以是其他值 4 List<string> list = new List<string>(); 5 var tasks = new Task[initialCount]; 6 //如果使用Semaphore,实例化的时候,那么最少要传递两个参数,信号量的初始请求数和信号量的最大请求数 7 using(SemaphoreSlim ssl = new SemaphoreSlim(initialCount)) 8 { 9 for (int i = 0; i < initialCount; i++) 10 { 11 int j = i; 12 tasks[j] = Task.Factory.StartNew(() => 13 { 14 try 15 { 16 //等待,直到进入SemaphoreSlim为止 17 //如果使用Semaphore,应调用WaitOne 18 ssl.Wait(); 19 //直到进入SemaphoreSlim才会执行下面的代码 20 list.Add(""+j);//可将这部分替换成真实的业务代码 21 } 22 finally 23 { 24 ssl.Release(); 25 } 26 }); 27 } 28 //注意一定要在using块的最后阻塞线程,直到所有的线程均处理完任务 29 //如果没有等待任务全部完成的语句,会导致SemaphoreSlim资源被提前释放。 30 Task.WaitAll(tasks); 31 } 32 }
5)SpinLock
1 public static void SpinLockTest() 2 { 3 bool lockTaken = false; 4 SpinLock sl = new SpinLock(true); 5 try 6 { 7 //获得锁 8 //如果不能获得锁,将会等待并不断检测锁是否可用 9 //获得锁后,lockTaken为true,此行代码之后的部分才会开始运行 10 sl.Enter(ref lockTaken); 11 12 //或使用含有超时机制的TryEnter方法 13 //sl.TryEnter(1000,ref lockTaken); 14 //然后抛出超时异常 15 //if (!lockTaken) 16 //{ 17 // throw new TimeoutException("超时异常"); 18 //} 19 20 //真正的业务。。。 21 22 } 23 finally 24 { 25 if (lockTaken) 26 { 27 //释放锁 28 //SpinLock没有使用内存屏障,因此设成false 29 sl.Exit(false); 30 } 31 } 32 }
6)SpinWait
1 public static void SpinWaitTest() 2 { 3 bool isTrue = false; 4 //任务一,处理业务,成功将isTrue设置为true 5 Task.Factory.StartNew(() => 6 { 7 //处理业务,返回结果result指示是否成功 8 bool result = ...; 9 if (result) 10 { 11 isTrue = true; 12 } 13 }); 14 15 //可设定等待时间,如果超时,则向下执行 16 Task.Factory.StartNew(() => { 17 SpinWait.SpinUntil(()=>isTrue,10000); 18 //真正的业务 19 }); 20 }
7) Look
1 下面两段代码是等价的。 2 lock (Object) 3 { 4 //do something 5 } 6 7 //等价代码 8 bool lockTaken = false; 9 try 10 { 11 Monitor.Enter(object,lockTaken); 12 //do something 13 } 14 finally 15 { 16 if(lockTaken) 17 { 18 Monitor.Exit(object); 19 } 20 }
8)Interlocked
1 public static void InterlockedTest() 2 { 3 Task[] tasks = new Task[10]; 4 long j = 0; 5 for(int i=0;i<10;i++) 6 { 7 int t = i; 8 tasks[t] = Task.Factory.StartNew(()=> 9 { 10 //以安全的方式递增j 11 Interlocked.Increment(ref j); 12 }); 13 } 14 Task.WaitAll(tasks); 15 }
-----------------------------------------------------------------------------------------
转载与引用请注明出处。
时间仓促,水平有限,如有不当之处,欢迎指正。