线程同步的情景之一
从本篇文章开始,我将陆续介绍多线程中会遇到的三种情况。
情景一:此茅坑有主了
大锤:“我擦,居然一个茅坑有两个人在用。”
大锤:“啊,忍不住了,一起挤挤吧~~~”
叫兽:“舒坦了,先走了。”
叫兽按下了冲水开关.... "哗啦啦....."
大锤:“你妹啊,冲什么水啊,冲得我一身 shit ”
解决方案:为了解决这种混乱的情况,管理员给茅坑加了道门,一次只允许一个人使用,其他人只能在外面等待。而且只要有人占着,就算不拉屎,其他人也只能乖乖排队。
问题抽象:当某一资源可能同时被多个线程读取和修改时,资源的状态将变得难以预料。
线程同步方案:Interlocked、lock、Moniter、SpinLock、ReadWriteLockSlim、Mutex
方案特性:除所有者外,其他人无条件等待;先到先得(谁先进茅坑,谁先用,没有先后顺序)
各方案间的区别(关于如何使用每种方案,很多文章和书籍都有介绍,就不再一一赘述了。)
这些方案从它们各自的实现方式可分为三种:用户模式构造、内核模式构造 和 混合模式构造。
应该尽量使用用户模式构造,它们的速度要显著快于内核模式的构造。这是因为它们使用了特殊 CPU 指令来协调线程。这意味着协调是在硬件中发生的(所以才这么快)。它们有一个缺点:只有 Windows 操作系统内核才能停止一个线程的运行(以避免浪费 CPU 时间)。所以,一个线程想要取得一个资源但又暂时取不到,它会一直在用户模式中运行。这可能浪费大量 CPU 时间。
内核模式的构造是由 Windows 操作系统自身提供的。所以,它们要求你在应用程序的线程中调用在操作系统内核中实现的函数。将线程从用户模式切换为内核模式(或相反)会招致巨大的性能损 失,这正是为什么应该避免使用内核模式构造的原因。然后,它们有一个重要的优点:一个线程使用一个内核模式的构造获取一个由其它线程拥有的资源 时,Windows会阻塞线程,使它不再浪费 CPU 时间。然后,当资源变得可用时,Windows 会恢复线程,允许它访问资源。
---- 《CLR via C# (第 3 版)》 P706
上面这段话摘自《CLR via C#》,各别用词稍微调整了下以便于理解。简单来说,用户模式会通过在 CPU 中不断的执行某些指令来达到阻塞线程的效果(想像一下一直执行 while(true); 的样子),而内核模式则是实实在在的把线程的执行给停止了,CPU 不会再去调度这个线程。混合模式,就不用说了,是两者的结合。
那什么时候该用什么模式的构造呢?对于短时间的阻塞,选择用户模式;长时间的阻塞,选择内核模式;阻塞时间不定的,选择混合模式。
用户模式(user-mode)
Interlocked 保证的是原子性,其原子操作包括 “递增”、“递减”、“相加”、“交换” 。之所以把它也归入情景一,是因为它通过原子操作确保一个资源在 “读取后,写入前” 不会有其它线程中断它的执行,从而保证了资源的独占使用。
优点:速度最快,且单次操作阻塞时间短。
缺点:可执行的操作有限。
SpinLock 自旋锁,在 .Net 4.0 的时候引入。自旋的意思就是自个儿在原地旋转,以此来占用 CPU 时间。说白了就是类似 “while(状态是否可用); ”,如果状态不可用,则一直循环,直到状态可用为止。可以用 Interlocked 来实现 SpinLock 的效果:
//参考 Clr via C#
struct MySpinLock
{
int _lock;
public void Enter()
{
//第一个线程进来的时候,Exchange 返回0,while 退出。其它线程进来,都返回1
while (Interlocked.Exchange(ref _lock, 1) == 1) ;
}
public void Exit()
{
Interlocked.Exchange(ref _lock, 0);
}
}
优点:速度快,可以用于各种操作。
缺点:如果操作需要很长时间,将会严重浪费 CPU 时间。在单核的处理器中使用该方式,可能造成死锁。因为如果加锁的线程优先级低于阻塞的线程,那可能很长一段时间都无法被调度到CPU上,这样就无法解锁。
内核模式(kernal-mode)
Mutex 可以跨进程保证资源的独占使用,通过 WaitOne 来获取锁,ReleaseMutex 释放锁(使用哪个线程执行的 WaitOne,只能由该线程 ReleaseMutex)。它与后面要讲到的 “Event” 都来自于同一个父类 WaitHandle。这是一个抽象类,包装了 Windows 操作系统的内核对象句柄。
主要用于:限制应用程序只能启动一次。如 Sql Server、360安全卫士。
代码示例:
[STAThread]
static void Main()
{
bool loaded = false;
Mutex mutex = new Mutex(false, "SINGILE", out loaded);
if (loaded)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
else
{
Application.Exit();
}
}
优点:允许递归使用,可以跨进程使用
缺点:速度最慢(不仅是因为会在内核模式与用户模式间进行切换,造成性能的损失;也因为相对于 Event,它提供了递归使用等高级的功能,这导致它比其它结构都要复杂)
混合模式(hybrid-mode)
Moniter 方式通过调用静态方法 Enter、Exit 来实现对共享资源或代码段的独占使用,是 .Net 领域中问世最早的一种线程同步机制。我们都知道每个引用类型在堆中都会包含两个特殊的字段:同步块索引 和 类型对象指针。而使用 Moniter.Enter 实际就会去操作同步块索引,让它指向堆中的同步块数组;Mointer.Exit 则会重新将同步块索引置为 -1。
优点:速度还行,介于内核模式和用户模式之间;支持递归使用。
缺点:会把所有操作(读或写)该资源的线程都阻塞,而当系统中读线程的数量远远多于写线程的时候,很有可能出现同一时刻只有多个读线程,这个时候阻塞的行为就显得多余了。
Lock 是 C# 的语法糖,通过查看 IL 代码可以知道,它最终将被解释为 Moniter.Enter 和 Moniter.Exit。下面是 C# 4.0 代码的 IL。
通过上面的 IL,可以明确的看到 Moniter.Exit 被放置在 finally 块中,这样保证了锁最终将被正确释放(避免了可能发生的死锁)。但有一点值得注意的是,如果代码块中抛出了异常,尽管可以保证锁被释放,但无法保证其中的共享资源仍旧是正确的。
优点:使用简单;保证锁肯定会被释放;速度同 Moniter 。
缺点:同 Moniter。
ReadWriteLockSlim 与 Mointer 不同,它通过 EnterReadLock、EnterWriteLock、ExitReadLock、ExitWriteLock 来区别对待读线程还是写线程。所以对于读线程加读锁,而写线程加写锁,这样当当前时刻不存在写线程的时候,所有读线程都可以并发的访问资源。
优点:读、写锁分离。当不存在写线程的时候,速度要明显快于 Mointer。而当有写线程的时候,速度稍慢于 Mointer。
上面的方式各有优缺点,就算是经验丰富的程序猿也不一定能保证线程一定是安全的。所以只要有可能还是建议大家尽量不使用、少使用共享资源,或者让共享资源变成只读。
总结
情景一中所说的所有方法都是围绕一个目的 ------ “解决对共享资源的争用问题”。当在实际开发过程中,如果碰到了共享资源(静态变量、类型的成员变量、文件等)或需要独占使用的代码段时,请考虑采用上述方式中的任何一种来保证线程安全。
作者:stg609
本文转自:http://www.cnblogs.com/stg609/p/4050473.html