线程同步的情景之一

 

  从本篇文章开始,我将陆续介绍多线程中会遇到的三种状况。 数组

 

 

  情景一:此茅坑有主了安全

 

  大锤:“我擦,竟然一个茅坑有两我的在用。”多线程

  大锤:“啊,忍不住了,一块儿挤挤吧~~~”并发

  叫兽:“舒坦了,先走了。”函数

  叫兽按下了冲水开关.... "哗啦啦....."post

  大锤:“你妹啊,冲什么水啊,冲得我一身 shit ”性能

 

解决方案:为了解决这种混乱的状况,管理员给茅坑加了道门,一次只容许一我的使用,其余人只能在外面等待。并且只要有人占着,就算不拉屎,其余人也只能乖乖排队。spa

问题抽象:当某一资源可能同时被多个线程读取和修改时,资源的状态将变得难以预料。操作系统

线程同步方案: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。

 

  上面的方式各有优缺点,就算是经验丰富的程序猿也不必定能保证线程必定是安全的。因此只要有可能仍是建议你们尽可能不使用、少使用共享资源,或者让共享资源变成只读

 

总结

  情景一中所说的全部方法都是围绕一个目的 ------ “解决对共享资源的争用问题”。当在实际开发过程当中,若是碰到了共享资源(静态变量、类型的成员变量、文件等)或须要独占使用的代码段时,请考虑采用上述方式中的任何一种来保证线程安全。

 

  本文来自《C# 基础回顾: 线程同步的情景之一

相关文章
相关标签/搜索