Jackyfei

线程同步手记

一、前言

        线程同步其实很简单,但是往往被老师教的很复杂。这是之前上课受的伤。脑袋瓜当人人家的跑马场,被蹂躏一番,最后老师留下的是先入为主的错误,以至于后面不停的干扰我的理解,纠起错来,真是不知道浪费了多少精力。

二、什么是线程同步

        一直想要找一个良好的方式来表达什么是线程同步。
 
        先看一个模拟线程同步的图:

 

   

 

        假如这个盒子一次只能放一个东西,并且接力赛又要保持顺畅,该是怎样的情景?
        首先对于Reader来说,取货的时候,箱子必须有货,如果没有货,要在旁边等候;
        其次对于Writer来说,存货的时候,箱子必须为空,如果不为空,也要在旁边等候;
        两个人要步调一致,并且配合默契,才能顺利的搬运东西。反过来,如果Reader执行了好几次,Writer才执行一次,或者Write执行了好几次,Reader才执行一次,最后都不能很好的保持步调的一致。
        把两个人看成是两个线程,这时候线程要同步,也必须要满足上面的要求。两个线程要协同一致的工作,才能完成一项任务。

三、代码演示:

        
      
    public class ThreadSyn
     {
        //缓存区,假设一次只能缓存一个字符
        private static char buffer;
        //线程1:写操作
        public Thread thread1 = new Thread(()=>
                                        {
                                            string str = "横看成岭侧成峰,远近高低各不同。不识庐山真面目,只缘身在此山中。";
                                           for (int i = 0; i < 32; i++)
                                           {
                                               buffer = str[i];
                                               Thread.Sleep(26);
                                           }
 
                                       });
 
        //线程2:读操作
        public Thread thread2 = new Thread(() =>
                                        { 
                                            for (int i = 0; i < 32; i++)
                                            {
                                                char ch = buffer;
                                                Console.WriteLine(ch);
                                                Thread.Sleep(36);
                                            }
                                        });
        }
 
         public class Program
       {
          static void Main(string[] args)
          {
              ThreadSyn threadSyn=new ThreadSyn();
              threadSyn.thread1.Start();
              threadSyn.thread2.Start();
 
              Console.Read();
          } 
    }

 

 
        运行效果图:

        

  

 

四、原因和方案:

        此时线程还是没有协同工作。因为如果写一个,读一个,再写一个,再读一个,那么这首诗应该是一首完整的显示。但是效果图的诗句却是紊乱的。
        如何才能解决真正的同步,.net为我们提供了一系列的同步类。包括:互锁(Interlocked),管程(Monitor)和互斥体(Mutex).
     
  4.1下面用互锁来解决上面的问题。
       
   public class ThreadSyn
     {
        //缓存区
        private static char _buffer;
        //标示盒子,即缓冲区使用的空间,盒子初始化为0
        private static long _box = 0;
        //线程1:写操作
        public Thread thread1 = new Thread(()=>
                                        {
                                           string str = "横看成岭侧成峰,远近高低各不同。不识庐山真面目,只缘身在此山中。";
                                           for (int i = 0; i < 32; i++)
                                           {
                                               //写入之前检查缓冲区
                                               //如果缓冲区已满,就进行等待,直到缓冲区的数据被进程读取为止
                                               while(Interlocked.Read(ref _box) == 1)
                                               { 
                                                   Thread.Sleep(10);
                                               }
                                             
                                               //向缓冲区写数据
                                               _buffer = str[i];
                                               //写完数据,标记缓冲区已满
                                               Interlocked.Increment(ref _box);
                                           }
                                       });
 
        //线程2:读操作
        public Thread thread2 = new Thread(() =>
                                        {
                                            for (int j = 0; j < 32; j++)
                                            {
                                                //写入之前检查缓冲区
                                                //如果缓冲区为空,就进行等待,直到缓冲区的数据被进程填充为止
                                                while(Interlocked.Read(ref _box) == 0)
                                                {
                                                    Thread.Sleep(10);
                                                }
                                               
                                                //向缓冲区读数据
                                                char ch = _buffer;
                                                Console.Write(ch);
                                                //读完数据,标记缓冲区已空
                                                Interlocked.Decrement(ref _box);
                                            }
                                        });
    }

    运行效果图:

    

    

        InterLocked提供了单个指令的操作,因此他提供了性能非常高的同步。
    

  

 

4.2用Monitor来解决问题

  Monitor的原理是这样的:先执行的线程,独占锁,进入临界区,执行临界区资源代码。其他线程,只能在集中在临界资源上等待被叫唤。当独占锁推出资源区,也可以继续让自己等待,等待下一次被叫唤。

     //缓存区
        private static char _buffer;
        //用于同步的对象
        private static object _objForLock = new object();

        //线程1:写操作
        public Thread thread1 = new Thread(() =>
        {
            string str = "横看成岭侧成峰,远近高低各不同。不识庐山真面目,只缘身在此山中。";
            for (int i = 0; i < 32; i++)
            {
                try
                {
                    //进入临界区,获取独占锁
                    Monitor.Enter(_objForLock);

                    //向缓冲区写数据
                    _buffer = str[i];
               
                    //写完后,唤醒在临界资源上睡眠的线程
                    Monitor.Pulse(_objForLock);

                    //让当前线程睡眠在临界资源上
                    Monitor.Wait(_objForLock);

                    //整个流程有点像轮班吃饭,
                    //第一个人先去吃饭,第二个在值班等待,第一个吃完了,唤醒第二个吃饭,自己则在等待下一次吃饭。
                }
                catch (ThreadInterruptedException ex)
                {   
                   Console.WriteLine("线程被中断……");
                }
                finally
                {
                    //退出临界区
                    Monitor.Exit(_objForLock);
                }
            }
        });

        //线程2:读操作
        public Thread thread2 = new Thread(() =>
        {
            for (int j = 0; j < 32; j++)
            {
                try
                {
                    //进入临界区,获取独占锁
                    Monitor.Enter(_objForLock); 

                    //向缓冲区读数据
                    char ch = _buffer;
                    Console.Write(ch);

                    //写完后,唤醒在临界资源上睡眠的线程
                    Monitor.Pulse(_objForLock);

                    //让当前线程睡眠在临界资源上
                    Monitor.Wait(_objForLock);

                }
                catch (ThreadInterruptedException ex)
                {   
                   Console.WriteLine("线程被中断……");
                }
                finally
                {
                    //退出临界区
                    Monitor.Exit(_objForLock);
                }
            }
        });

  不同的是Monitor只能锁定引用类型的对象,值类型会被装箱,等于生成另外一个对象,不能达到同步。为了保证推出临界区资源得到释放,使用了finally。为了方便使用,C#专门使用了lock语句。

  所以我们可以完全更简洁的重写上面的try{}finally{}中的关键代码,如下所示:

          lock (_objForLock)
                {
                    //进入临界区,获取独占锁
                    Monitor.Enter(_objForLock);

                    //向缓冲区写数据
                    _buffer = str[i];

                    //写完后,唤醒在临界资源上睡眠的线程
                    Monitor.Pulse(_objForLock);

                    //让当前线程睡眠在临界资源上
                    Monitor.Wait(_objForLock);  
                }

  

独占锁注意:

  因为独占锁,其他线程就不能再访问,只有Lock结束后,其他线程才可以访问,这保证了访问的正确性。但是,如果有多个线程对同一个资源进行写操作,在独占锁解开前,其他线程只能被临时暂停,这使得程序的效率大打折扣。所以应该慎用锁,只有必要时才使用。
 
 
 

 

 
posted @ 2012-04-11 23:18  张飞洪[厦门]  阅读(995)  评论(0编辑  收藏  举报