同步基元概述
线程锁
使用场景:同步对共享资源的线程访问 (尽量锁最少的资源,比如有些时候你可以计算好得出结果后再加锁给你的对象赋值)
线程优先顺序:【等待队列】->【就绪队列】->【拥有锁线程】
锁的特性:
- 互斥性 指的是一次只允许一个线程持有某个特定的锁,因此可以保证共享数据内容的一致性;
- 可见性 指的是必须确保锁被释放之前对共享数据的修改,随后获得锁的另一个线程能够知道该行为。
多个.NET同步基元派生自 System.Threading.WaitHandle 类,该类会封装本机操作系统同步句柄并将信号机制用于线程交互。
轻量同步类型不依赖于基础操作系统句柄,通常会提供更好的性能。 但是,它们不能用于进程间同步。 将这些类型用于一个应用程序中的线程同步。
其中的一些类型是派生自 WaitHandle 的类型的替代项。 例如,SemaphoreSlim 是 Semaphore 的轻量替代项。
lock 互斥锁
- 最常用的,一般情况下都建议使用lock,是提供互斥访问的首选方法
- 最好是锁只读的专有的引用类型,因为值类型需要装箱,而不是值类型或其它与代码有关的类型
避免使用this(public 的话别的地方也可能呢new),typeof,字符串(相同的字符串其引用地址可能一样); - Random rad = new Random(); lock(rad) 这个是在你自己所预知内的
- 线程1 有A锁要B锁 线程2 有B锁要A锁,可能发生死锁;
Monitor 互斥锁
- lock是其的一个封装, try {Monitor.Enter } finally {Monitor.Exit}相当于lock
- 不同点是Monitor 有一个tryEnter,试图获取该锁,失败返回false;
- 不同点是Monitor 有超时机制,还有Wait、Pulse和PulseAll方法
1.Monitor.Wait方法
当线程调用 Wait 时,它释放对象的锁并进入对象的等待队列,对象的就绪队列中的下一个线程(如果有)获取锁并拥有对对象的独占使用。Wait()就是交出锁的使用权,使线程处于阻塞状态,直到再次获得锁的使用权。
2.Monitor.Pulse方法
当前线程调用此方法以便向队列中的下一个线程发出锁的信号。接收到脉冲后,等待线程就被移动到就绪队列中。在调用 Pulse 的线程释放锁后,就绪队列中的下一个线程(不一定是接收到脉冲的线程)将获得该锁。pulse()并不会使当前线程释放锁。
当一个线程尝试着lock一个同步对象的时候,该线程就在就绪队列中排队。一旦没人拥有该同步对象,就绪队列中的线程就可以占有该同步对象。这也是我们平时最经常用的lock方法。为了其他的同步目的,占有同步对象的线程也可以暂时放弃同步对象,并把自己流放到等待队列中去,这就是Monitor.Wait;由于该线程放弃了同步对象,其他在就绪队列的排队者就可以进而拥有同步对象。比起就绪队列来说,在等待队列中排队的线程更像是二等公民:他们不能自动得到同步对象,甚至不能自动升舱到就绪队列。而Monitor.Pulse的作用就是开一次门,使得一个正在等待队列中的线程升舱到就绪队列;相应的Monitor.PulseAll则打开门放所有等待队列中的线程到就绪队列。
SpinLock 自旋互斥锁
- 用于某些场景下替换Monitor,因为Monitor隐含分配的对象比较多,垃圾回收压力山大,SpinLock中包含SpinWait,但不能简单理解为就是对spinwait的封装
- 使用条件: 等待时间短暂且极少出现争用的情况下使用,项目中需要测试性能是否提高, l_spinlock.enter(ref l_islock);
- 不要将SpinLock声明为只读字段,如果声明为只读字段,会导致每次调用都会返回一个SpinLock新副本,在多线程下,每个方法都会成功获得锁,而受到保护的临界区不会按照预期进行串行化。
Interlocked 为多个线程共享的变量提供原子操作
- 用于数值的加 减 increment 递增 decrement 递减 int32,long,double
- 用于数据的交换,不局限于数字;
eg:Exchange(T, T) 或者Exchange(Object, Object) 以原子操作的形式把后面一个值赋给前面的值并返回原始值;
int a = 1;
Console.WriteLine(Interlocked.Exchange(ref a,2));
Console.WriteLine(a);
Console.ReadKey();
// 输出结果为:1 2
string aa = "abc";
Console.WriteLine(Interlocked.Exchange(ref aa,"bb"));
Console.WriteLine(aa);
//输出结果为 abc bb
- 比较
CompareExchange(ref T, T, T) 比较第一个值跟第三个值是否相等,若是相等则用第二个值替换第一个值;
返回原始值
int cc = 1;
Console.WriteLine(Interlocked.CompareExchange(ref cc, 2,1));
Console.WriteLine(cc);
//输出 1 ,2
EventWaitHandle
- 手动锁 MaualResetEvent 当发出信号时,会保持已发出信号状态,直到调用 Reset 方法
- 自动锁 AutoResetEvent 当发出信号时,会在发布单个等待线程后自动重置为未发出信号状态
Semaphore SemaphoreSilm 信号量锁
限制同时访问某共享资源或资源池的线程数量,当信号量计数大于零时,会将信号量的状态设置为已发出信号;当信号量计数为零时,会将信号量的状态设置为未发出信号。
WaitOne 等待信号,若剩余可以使用的剩余数量大于1,则WaitOne 后面的代码继续执行;
WaitOne(10ms) 若是剩余可以使用的数量不大于1,那么等待10ms后继续执行下一步,这个时候不消耗信号量,也没有信号量可以消耗;
若是剩余可使用数量大于1 ,那么就免等待,但是需要消耗用信号量;
Release() 释放一个信号量= Release(1);
Release(int releaseCount) 一次性释放多个信号量;但若是释放后的总信号量大于允许的最大信号量就会抛异常
try
{
//最大数是4,却释放了2个,这个时候剩余信号量为3+2 = 5大于4 所以异常
Semaphore semaphore = new Semaphore(4, 4, "wp");
semaphore.WaitOne();
semaphore.Release(2);
}
catch (SemaphoreFullException ex1)
{
Console.WriteLine(ex1);
throw;
}
volatile 变量修饰符
从内存而不是缓存读取,并不是说用它修饰后的变量同时只能有一个线程访问
spinWait 自旋等待,用于替换Thread.Sleep(0);
使用条件: 如果等待某个条件满足需要的世界特别短暂,而且不希望发生昂贵的上下文切换;长时间的自旋不是很好的做法,因为自旋会阻塞更高级的线程及其相关的任务,还会阻塞垃圾回收机制。
SpinWait并没有设计为让多个任务或线程并发使用,因此多个任务或线程通过SpinWait方法进行自旋,那么每一个任务或线程都应该使用自己的SpinWait实例;
在源码中spinWait 超过某个次数后是可能调用Thread.Sleep(0),Thread.Sleep(1),是会存在优先级反转的情况的,不希望发生昂贵的上下文切换,但是有可能还是否发生上下文切换;
SpinWait的核心方是SpinOnce方法,假如我们是多核CPU,该方法在前10次【NextSpinWillYield为false】SpinOnce调用Thread.SpinWait【这个是一个win32的方法 , private static extern void SpinWaitInternal(int iterations);】,后面的调用主要是调用 Thread.Yield()【Win32API, private static extern bool YieldInternal()】,当count是14的时候第一次调用 Thread.Sleep(0),当count是29的时候第一次调用 Thread.Sleep(1),后的的规则是(count-10)%20 == 19 调用Thread.Sleep(1),否者检查(count-10)%5==4Thread.Sleep(0).
SpinUntil方法的实现句非常简单了,就是循环调用SpinOnce方法,如果条件满足 或者超时 则退出循环。
不同点:
-
Thread.Yeild
该方法是在 .Net 4.0 中推出的新方法,它对应的底层方法是 SwitchToThread。Yield 的中文翻译为 “放弃”,这里意思是主动放弃当前线程的时间片,并让操作系统调度其它就绪态的线程使用一个时间片。但是如果调用 Yield,只是把当前线程放入到就绪队列中,而不是阻塞队列。如果没有找到其它就绪态的线程,则当前线程继续运行。
优势:比 Thread.Sleep(0) 速度要快,可以让低于当前优先级的线程得以运行。可以通过返回值判断是否成功调度了其它线程。
劣势:只能调度同一个处理器的线程,不能调度其它处理器的线程。当没有其它就绪的线程,会一直占用 CPU 时间片,造成 CPU 100%占用率 -
Thread.Sleep(0)
Sleep 的意思是告诉操作系统自己要休息 n 毫秒,这段时间就让给另一个就绪的线程吧。当 n=0 的时候,意思是要放弃自己剩下的时间片,但是仍然是就绪状态,其实意思和 Yield 有点类似。但是 Sleep(0) 只允许那些优先级相等或更高的线程使用当前的CPU,其它线程只能等着挨饿了。如果没有合适的线程,那当前线程会重新使用 CPU 时间片。
优势:相比 Yield,可以调度任何处理器的线程使用时间片。
劣势:只能调度优先级相等或更高的线程,意味着优先级低的线程很难获得时间片,很可能永远都调用不到。当没有符合条件的线程,会一直占用 CPU 时间片,造成 CPU 100%占用率。 -
Thread.Sleep(1)
该方法使用 1 作为参数,这会强制当前线程放弃剩下的时间片,并休息 1 毫秒(因为不是实时操作系统,时间无法保证精确,一般可能会滞后几毫秒或一个时间片)。但因此的好处是,所有其它就绪状态的线程都有机会竞争时间片,而不用在乎优先级。
优势:可以调度任何处理器的线程使用时间片。无论有没有符合的线程,都会放弃 CPU 时间,因此 CPU 占用率较低。
劣势:相比 Thread.Sleep(0),因为至少会休息一定时间,所以速度要更慢。
Barrier 屏障
当您需要一组任务并行地运行一连串的阶段,但是每一个阶段都要等待所有其他任务都完成前一阶段之后才能开始,你一通过Barrier实例来同步这一类协同工作。
Barrier初始化后,将等待特定数量的信号到来,这个数量在Barrier初始化时指定,在所指定的信号个数已经到来后,Barrier将执行一个指定的动作,这个动作也是在Barrier初始化时指定。Barrier在执行动作过后,将会重置,这时又将等待特定数量的信号到来,再执行指定动作。信号通过成员函数SignalAndWait()来发送,执行SignalAndWait()函数的Task或者线程将会投入等待,Barrier将等待特定数量的信号到达,然后Barrier执行完指定动作后被重置,这时SignalAndWait()函数所在的Task或者线程将继续运行。在程序的运行过程中,可以通过成员函数AddParticipant()和RemoveParticpant()来增加或者减少需要等待的信号数量
CountdownEvent 不常用
CountDownEvent调用成员函数Wait()将阻塞,直至成员函数Signal() 被调用达特定的次数,这时CountDownEvent称作就绪态,对于处于就绪态的CountDownEvent,调用Wait()函数将不会再阻塞,只有手动调用Reset()函数后,调用Wait()函数将再次阻塞。CountDownEvent可以通过TryAddCount()和AddCount()函数来增加函数Signal() 需被调用的次数,但只有当CountDownEvent处于未就绪态时才会成功。否则根据调用函数的不同,将有可能抛出异常
当有新的需要同步的任务产生时,就调用AddCount增加它的计数,当有任务到达同步点是,就调用Signal函数减小它的计数,当CountdownEvent的计数为零时,就表示所有需要同步的任务已经完成,可以开始下一步任务了。
ReaderWriterLock ReaderWriterLockSlim
- 允许多线程读取和独占式写入(在任何时候都只能有一个线程处于写入模式,当线程处于写入模式时,任何其他线程都不能在任何模式下进入锁定状态)
- 并且允许具有读取访问权限的一个线程处于可升级读取模式,在该模式下,线程可以升级到写入模式,而无需放弃对资源的读取访问权限. 在任何时候,只能有一个线程处于可升级模式。任意数量的线程都可以处于读取模式,并且在其他线程处于读取模式时,可以有一个处于可升级模式的线程
虽然 ReaderWriterLockSlim 类似于 ReaderWriterLock,但不同之处在于,前者简化了递归规则以及锁状态的升级和降级规则。 ReaderWriterLockSlim 避免了许多潜在的死锁情况。 另外,ReaderWriterLockSlim 的性能显著优于 ReaderWriterLock。 建议对所有新开发的项目使用 ReaderWriterLockSlim。
进程锁
在 .NET Framework 中,由于 WaitHandle 派生自 System.MarshalByRefObject,
因此,这些类型可用于跨应用程序域边界同步线程的活动。
在 .NET Framework 和 .NET Core 中,其中的一些类型可以表示已命名的系统同步句柄,这些句柄在整个操作系统中都可见并可用于进程间同步:
Mutex(.NET Framework 和 .NET Core)、
Semaphore(.NET Framework 和 Windows 上的 .NET Core)、
EventWaitHandle(.NET Framework 和 Windows 上的 .NET Core)。
-
Mutex 跨进程锁,当然也可以实现进程内的线程同步,但消耗的资源更多,是win32封装的
授予对共享资源的独占访问权限。 如果没有任何线程拥有它,则 mutex 将处于已发出信号状态 -
Semaphore
信号量分为两种类型:本地信号量和命名系统信号量
//多个进程之间用相同的名称;
Semaphore semaphore = new Semaphore(4, 4, "wp");
semaphore.WaitOne();
semaphore.Release();
//第一个控制台应用程序里面写
static void Main(string[] args)
{
Semaphore semaphore = new Semaphore(3, 3, "wp");
int times = 0;
while (true)
{
semaphore.WaitOne();
Console.WriteLine($"我第{ ++times}次告诉你我不喜欢你");
}
Console.ReadKey();
}
//第二个里面写,运行第一个项目后,在第二个项目输入a 并回车 那么第一个程序就会进行一次;
if (Semaphore.TryOpenExisting("wp", out Semaphore result1))
{
Console.WriteLine("wp 已经被别的进程占用");
while (true)
{
if (Console.ReadLine() == "a")
result1.Release();
}
}
- EventWaitHandle
其它相关概念:
- 对象锁提供了限制访问代码块的功能,通常称为临界区
出处:http://www.cnblogs.com/maanshancss/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。所有源码遵循Apache协议,使用必须添加 from maanshancss