.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 }

-----------------------------------------------------------------------------------------

时间仓促,水平有限,如有不当之处,欢迎指正。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏有趣的django

python开发面试问题

python语法以及其他基础部分 可变与不可变类型;  浅拷贝与深拷贝的实现方式、区别;deepcopy如果你来设计,如何实现;  __new__() 与 __...

4158
来自专栏Java架构沉思录

优雅实现延时任务之zookeeper篇

在《优雅实现延时任务之Redis篇》一文中提到,实现延时任务的关键点,是要存储任务的描述和任务的执行时间,还要能根据任务执行时间进行排序,那么我们可不可以使用z...

993
来自专栏博客园

C#异步使用要点(翻译)

在使用异步方法中最好不要使用void当做返回值,无返回值也应使用Task作为返回值,因为使用void作为返回值具有以下缺点

1315
来自专栏逸鹏说道

分享一个自制的 .net线程池1

扯淡 由于项目需求,需要开发一些程序去爬取一些网站的信息,算是小爬虫程序吧。爬网页这东西是要经过网络传输,如果程序运行起来串行执行请求爬取,会很慢,我想没人会这...

3216
来自专栏蘑菇先生的技术笔记

探索c#之Async、Await剖析

2608
来自专栏Linyb极客之路

分布式之缓存击穿

因此,如果黑客每次故意查询一个在缓存内必然不存在的数据,导致每次请求都要去存储层去查询,这样缓存就失去了意义。如果在大流量下数据库可能挂掉。这就是缓存击穿。 场...

1163
来自专栏Spark生态圈

[spark] Shuffle Write解析 (Sort Based Shuffle)

从 Spark 2.0 开始移除了Hash Based Shuffle,想要了解可参考Shuffle 过程,本文将讲解 Sort Based Shuffle。

942
来自专栏一名合格java开发的自我修养

Java IO 装饰者模式

  装饰模式以对客户端透明的方式动态地给一个对象附加上更多的责任。换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。

752
来自专栏desperate633

TCP/IP之CIDR与路由聚合CIDR路由聚合( route aggregation)

(CIDR: Classless InterDomain Routing)无类域间路由

1234
来自专栏智能大石头

线程池ThreadPool及Task调度机制分析

近1年,偶尔发生应用系统启动时某些操作超时的问题,特别在使用4核心Surface以后。笔记本和台式机比较少遇到,服务器则基本上没有遇到过。

650

扫码关注云+社区