多线程总结之旅(6):线程同步之临界区

   为何使用线程同步?
  现在的应用程序愈来愈复杂,咱们经常须要多线程技术来提升咱们应用程序的响应速度,建立一个线程是不能提升程序的执行效率的,因此要建立多个线程。每一个线程都由本身的线程ID,当前指令指针(PC),寄存器集合和堆栈组成,但代码区是共享的,即不一样的线程能够执行一样的函数。因此在并发环境中,多个线程同时对同一个内存地址进行 写入,因为CPU时间调度上的问题,写入数据会被屡次的覆盖,会形成共享数据损坏,因此就要使线程同步。 (若是多个线程同时对共享数据只进行只读访问是不须要进行同步的)
 
  线程同步带来的问题?
  

  在并发的环境里,“线程同步锁”能够保护共享数据,可是也会存在一些问题:html

  1)   实现比较繁琐,并且容易错漏。你必须标识出可能由多个线程访问的全部共享数据。而后,必须为其获取和释放一个线程同步琐,而且保证已经正确为全部共享资源添加了锁定代码。数据库

  2)   因为临界区没法并发运行,进入临界区就须要等待,加锁带来效率的下降。express

  3)   在复杂的状况下,很容易形成死锁,并发实体之间无止境的互相等待。多线程

  4)   优先级倒置形成实时系统不能正常工做。优先级低的进程拿到高优先级进程须要的锁,结果是高/低优先级的进程都没法运行,中等优先级的进程可能在狂跑。并发

  5)   当线程池中一个线程被阻塞时,可能形成线程池根据CPU使用状况误判建立更多的线程以便执行其余任务,然而新建立的线程也可能因请求的共享资源而被阻塞,恶性循环,徒增线程上下文切换的次数,而且下降了程序的伸缩性。(这一点很重要)dom

 

  .NET下线程同步的方法?
 
  线程同步:即当有一个线程在对内存进行操做时,其余线程都不能够对这个内存地址进行操做,直到该线程完成操做, 其余线程才能对该内存地址进行操做,而其余线程又处于等待状态,目前实现线程同步的方法有不少,其中包括 临界区、互斥量、事件、信号量四种方式。
 
  我先介绍一下这四种方法的概念,主要是品味他们实现线程同步的不一样之处在哪里?
  

  1.临界区:经过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。(临界区能够认为是操做共享资源的一段代码函数

  2.互斥量:为协调共同对一个共享资源的单独访问而设计的。ui

  3.信号量:为控制一个具备有限数量用户资源而设计。this

  4.事 件:用来通知线程有一些事件已发生,从而启动后继任务的开始spa

   
 
  若是没有品味出来东东,咱们逐一详细介绍四种方式。。。。。。。
 
 
1、临界区:
  适用范围:它只能同步一个进程中的线程,不能跨进程同步。通常用它来作单个进程内的代码快同步,效率比较高。
  经过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只容许一个线程对共享资源进行访问,若是有多个线程试图访问公共资源,那么在有一个线程进入后,其余试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其余线程才能够抢占。
  实现临界区的方式包括 Lock、Monitor类、ReaderWriterLock类以及标志量
 
  (1)Lock
    (1.1)lock的使用形式:lock(expression) embedded-statement,   即lock   (   表达式   )   嵌入语句
        注意:lock 语句的表达式必须表示一个引用类型的值。永远不会为 lock 语句中的表达式执行隐式装箱转换,所以,若是该表达式表示的是一个值类型的值,则会致使一个编译时错误。
     (1.2)lock的实质。
        lock (x) 等价于如下代码(其中x是引用类型)
        
        system.threading.monitor.enter(x);
        try {
             ...
          }
        finally 
          {           system.threading.monitor.exit(x);           }

 

     (1.3)lock什么对象。
 
       lock什么对象呢?
      (a)lock引用类型
      (b) 某些系统类提供专门用于锁定的成员。例如,array 类型提供 syncroot。许多集合类型也提供 syncroot。

      (c)自定义类推荐用私有的只读静态对象,好比:private static readonly object obj = new object();为何要设置成只读的呢?这时由于若是在lock代码段中改变obj的值,其它线程就畅通无阻了,由于互斥锁的对象变了,object.referenceequals必然返回false。(推荐的方式

 
       为何只能lock引用类型?

      (a)为何不能lock值类型,好比lock(1)呢?lock本质上monitor.enter,monitor.enter会使值类型装箱,每次lock的是装箱后的对象。lock实际上是相似编译器的语法糖,所以编译器直接限制住不能lock值类型。

      (b)退一万步说,就算能编译器容许你lock(1),可是object.referenceequals(1,1)始终返回false(由于每次装箱后都是不一样对象),也就是说每次都会判断成未申请互斥锁,这样在同一时间,别的线程照样可以访问里面的代码,达不到同步的效果。同理lock((object)1)也不行。

      (c)那么lock("xxx")字符串呢?msdn上的原话是:锁定字符串尤为危险,由于字符串被公共语言运行库 (clr)“暂留”。 这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了全部运行的应用程序域的全部线程中的该文本。所以,只要在应用程序进程中的任何位置处具备相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的全部实例。

      (d)一般,最好避免锁定 public 类型或锁定不受应用程序控制的对象实例。例如,若是该实例能够被公开访问,则 lock(this) 可能会有问题,由于不受控制的代码也可能会锁定该对象。这可能致使死锁,即两个或更多个线程等待释放同一对象。出于一样的缘由,锁定公共数据类型(相比于对象)也可能致使问题。并且lock(this)只对当前对象有效,若是多个对象之间就达不到同步的效果。

      (e)lock(typeof(class))与锁定字符串同样,范围太广了。

    (1.4)示例

/*
该实例是一个线程中lock用法的经典实例,使获得的balance不会为负数
同时初始化十个线程,启动十个,但因为加锁,可以启动调用WithDraw方法的可能只能是其中几个
*/
using System;

namespace ThreadTest29
{
    class Account
    {
        private Object thisLock = new object();//设置锁对象
        int balance;
        Random r = new Random();

        public Account(int initial)
        {
            balance = initial;
        }

        int WithDraw(int amount)
        {
            if (balance < 0)
            {
                throw new Exception("负的Balance.");
            }
            //确保只有一个线程使用资源,一个进入临界状态,使用对象互斥锁,10个启动了的线程不能所有执行该方法
            lock (thisLock)
            {
                if (balance >= amount)
                {
                    Console.WriteLine("----------------------------:" + System.Threading.Thread.CurrentThread.Name + "---------------");

                    Console.WriteLine("调用Withdrawal以前的Balance:" + balance);
                    Console.WriteLine("把Amount输入 Withdrawal     :-" + amount);
                    //若是没有加对象互斥锁,则可能10个线程都执行下面的减法,加减法所耗时间片断很是小,可能多个线程同时执行,出现负数。
                    balance = balance - amount;
                    Console.WriteLine("调用Withdrawal以后的Balance :" + balance);
                    return amount;
                }
                else
                {
                    //最终结果
                    return 0;
                }
            }
        }
        public void DoTransactions()
        {
            for (int i = 0; i < 100; i++)
            {
                //生成balance的被减数amount的随机数
                WithDraw(r.Next(1, 100));
            }
        }
    }

    class Test
    {
        static void Main(string[] args)
        {
            //初始化10个线程
            System.Threading.Thread[] threads = new System.Threading.Thread[10];
            //把balance初始化设定为1000
            Account acc = new Account(1000);
            for (int i = 0; i < 10; i++)
            {
                System.Threading.Thread t = new System.Threading.Thread(new System.Threading.ThreadStart(acc.DoTransactions));
                threads[i] = t;
                threads[i].Name = "Thread" + i.ToString();
            }
            for (int i = 0; i < 10; i++)
            {
                threads[i].Start();
            }
            Console.ReadKey();
        }
    }
}

   (2)Monitor类

    这个算是实现锁机制的纯正类,在锁定的临界区中只容许让一个线程访问,其余线程排队等待。主要整理为2组方法。

    (2.1)Monitor.Enter和Monitor.Exit      

      微软很照护咱们,给了咱们语法糖Lock,对的,语言糖确实减小了咱们没必要要的劳动而且让代码更可观,可是若是咱们要精细的     控制,则必须使用原生类,这里要注意一个问题就是“锁住什么”的问题,通常状况下咱们锁住的都是静态对象,咱们知道静态对象属于类级别,当有不少线程共同访问的时候,那个静态对象对多个线程来讲是一个,不像实例字段会被认为是多个。Monitor 锁定对象是引用类型,而非值类型,该对象用来定义锁的范围,与lock同样,毕竟lock是monitor的语法糖。

class Program
    {
        static void Main(string[] args)
        {
            for (int i = 0; i < 10; i++)
            {
                Thread t = new Thread(Run);

                t.Start();
            }
        }

        //资源
        static object obj = new object();

        static int count = 0;

        static void Run()
        {
            Thread.Sleep(10);

            //进入临界区
            Monitor.Enter(obj);

            Console.WriteLine("当前数字:{0}", ++count);

            //退出临界区
            Monitor.Exit(obj);
        }
    }

 

    (2.2)Monitor.Wait和Monitor.Pulse  

  首先这两个方法是成对出现,一般使用在Enter,Exit之间。 Wait: 暂时的释放资源锁,而后该线程进入”等待队列“中,那么天然别的线程就能获取到资源锁。 Pulse:  唤醒“等待队列”中的线程,那么当时被Wait的线程就从新获取到了锁。 

这里咱们是否注意到了两点:①   可能A线程进入到临界区后,须要B线程作一些初始化操做,而后A线程继续干剩下的事情。②   用上面的两个方法,咱们能够实现线程间的彼此通讯。

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace Test
{
    public class Program
    {
        public static void Main(string[] args)
        {
            LockObj obj = new LockObj();

            //注意,这里使用的是同一个资源对象obj
            Jack jack = new Jack(obj);
            John john = new John(obj);

            Thread t1 = new Thread(new ThreadStart(jack.Run));
            Thread t2 = new Thread(new ThreadStart(john.Run));

            t1.Start();
            t1.Name = "Jack";

            t2.Start();
            t2.Name = "John";

            Console.ReadLine();
        }
    }

    //锁定对象
    public class LockObj { }

    public class Jack
    {
        private LockObj obj;

        public Jack(LockObj obj)
        {
            this.obj = obj;
        }

        public void Run()
        {
            Monitor.Enter(this.obj);

            Console.WriteLine("{0}:我已进入茅厕。", Thread.CurrentThread.Name);

            Console.WriteLine("{0}:擦,太臭了,我仍是撤!", Thread.CurrentThread.Name);

            //暂时的释放锁资源
            Monitor.Wait(this.obj);

            Console.WriteLine("{0}:兄弟说的对,我仍是进去吧。", Thread.CurrentThread.Name);

            //唤醒等待队列中的线程
            Monitor.Pulse(this.obj);

            Console.WriteLine("{0}:拉完了,真舒服。", Thread.CurrentThread.Name);

            Monitor.Exit(this.obj);
        }
    }

    public class John
    {
        private LockObj obj;

        public John(LockObj obj)
        {
            this.obj = obj;
        }

        public void Run()
        {
            Monitor.Enter(this.obj);

            Console.WriteLine("{0}:直奔茅厕,兄弟,你仍是进来吧,当心憋坏了!",
                               Thread.CurrentThread.Name);

            //唤醒等待队列中的线程
            Monitor.Pulse(this.obj);

            Console.WriteLine("{0}:哗啦啦....", Thread.CurrentThread.Name);

            //暂时的释放锁资源
            Monitor.Wait(this.obj);

            Console.WriteLine("{0}:拉完了,真舒服。", Thread.CurrentThread.Name);

            Monitor.Exit(this.obj);
        }
    }
}

   (3)ReaderWriteLock类

   先前也知道,Monitor实现的是在读写两种状况的临界区中只可让一个线程访问,那么若是业务中存在”读取密集型“操做,就比如数据库同样,读取的操做永远比写入的操做多。针对这种状况,咱们使用Monitor的话很吃亏,不过不要紧,ReadWriterLock就很牛X,由于实现了”写入串行“,”读取并行“。

ReaderWriteLock中主要用3组方法:

<1>  AcquireWriterLock: 获取写入锁。

          ReleaseWriterLock:释放写入锁。

<2>  AcquireReaderLock: 获取读锁。

          ReleaseReaderLock:释放读锁。

<3>  UpgradeToWriterLock:将读锁转为写锁。

         DowngradeFromWriterLock:将写锁还原为读锁。

 下面就实现一个写操做,三个读操做,要知道这三个读操做是并发的。

namespace Test
{
    class Program
    {
        static List<int> list = new List<int>();

        static ReaderWriterLock rw = new System.Threading.ReaderWriterLock();

        static void Main(string[] args)
        {
            Thread t1 = new Thread(AutoAddFunc);

            Thread t2 = new Thread(AutoReadFunc);

            t1.Start();

            t2.Start();

            Console.Read();
        }

        /// <summary>
/// 模拟3s插入一次
/// </summary>
/// <param name="num"></param>
        public static void AutoAddFunc()
        {
            //3000ms插入一次
            Timer timer1 = new Timer(new TimerCallback(Add), null, 0, 3000);
        }

        public static void AutoReadFunc()
        {
            //1000ms自动读取一次
            Timer timer1 = new Timer(new TimerCallback(Read), null, 0, 1000);
            Timer timer2 = new Timer(new TimerCallback(Read), null, 0, 1000);
            Timer timer3 = new Timer(new TimerCallback(Read), null, 0, 1000);
        }

        public static void Add(object obj)
        {
            var num = new Random().Next(0, 1000);

            //写锁
            rw.AcquireWriterLock(TimeSpan.FromSeconds(30));

            list.Add(num);

            Console.WriteLine("我是线程{0},我插入的数据是{1}。", Thread.CurrentThread.ManagedThreadId, num);

            //释放锁
            rw.ReleaseWriterLock();
        }

        public static void Read(object obj)
        {
            //读锁
            rw.AcquireReaderLock(TimeSpan.FromSeconds(30));

            Console.WriteLine("我是线程{0},我读取的集合为:{1}",
                              Thread.CurrentThread.ManagedThreadId, string.Join(",", list));
            //释放锁
            rw.ReleaseReaderLock();
        }
    }
}

  (4)标志量: 顾名思义,标志量就是声明一个布尔型变量,用来标示某些方法的执行状态。

 

 

 

 

下一篇博文中来介绍互斥量实现线程同步。。。。

相关文章
相关标签/搜索