zoukankan      html  css  js  c++  java
  • 线程同步手记

    一、前言

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

    二、什么是线程同步

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

       

            假如这个盒子一次只能放一个东西,并且接力赛又要保持顺畅,该是怎样的情景?
            首先对于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结束后,其他线程才可以访问,这保证了访问的正确性。但是,如果有多个线程对同一个资源进行写操作,在独占锁解开前,其他线程只能被临时暂停,这使得程序的效率大打折扣。所以应该慎用锁,只有必要时才使用。
     
     
     
     
    希望以上分享对你有所帮助,感谢您的捧场。
    作者: 张飞洪[厦门]
    QQ群: 共享交流群

    打赏支持

  • 相关阅读:
    django实现github第三方本地登录
    django扩展User模型(model),profile
    WSGI
    Linux查看配置文件中未被注释的有效配置行
    virsh 命令
    qemu-img命令
    python logging
    oslo.messaging
    集成kafka的身份认证(SASL/PLAIN)到kubernets环境中
    Helm基础知识
  • 原文地址:https://www.cnblogs.com/jackyfei/p/synchronization.html
Copyright © 2011-2022 走看看