美文网首页
二、线程同步

二、线程同步

作者: pingwazi | 来源:发表于2019-12-24 14:27 被阅读0次

    回到主目录
    1、阻塞同步
    1.1、Join
    1.2、自旋+阻塞
    2、锁同步
    2.1、lock
    2.2、Monitor
    2.3、死锁
    2.4、锁嵌套
    3、信号同步
    3.1、SemaphoreSlim
    3.2、AutoResetEvent
    3.3、ManualResetEventSlim
    3.4、CountdownEvent
    3.5、等待句柄和线程池
    3.6、WaitAny、WaitAll与SignalAndWait
    4、非阻塞同步
    4.1、Interlocked

    线程同步是实现线程安全的一种方式,支持多线程操作的编程语言都会涉及到线程线程同步的概念。实现线程同步的方式有很多种,这个章节就来介绍C#中实现线程间同步的集中方式。

    1、阻塞同步

    何为阻塞?就是线程在运行期间由于某些原因被暂停,比如调用Sleep、Join等方法,被阻塞的线程不会再消耗CPU资源(但任然会占用内存空间),并且在被阻塞的同时会出让CPU资源,直到解除阻塞的条件被满足为止。通过让自己或者其他线程阻塞可以实现简单的线程同步。

    1.1、Join

    阻塞当前线程,直到所等待线程运行结束,当前线程才会继续执行。

    class Program
        {
            static void Main(string[] args)
            {
                int i = 0;
                Thread thread = new Thread(() => { Thread.Sleep(1000 * 2); i++; });
                thread.Start();
                thread.Join();//阻塞当前线程,直到thread运行完成
                i++;
                Console.WriteLine(i);
                Console.WriteLine("按任意键退出");
                Console.ReadLine();
            }
        }
    

    1.2、自旋+阻塞

    在某些情况下,如果解除阻塞的条件能够很快得到满足的话,那么使用自旋代替阻塞会效率高一些,因为它避免了上写文切换的开销。但如果存粹的只使用自旋,非常浪费CPU的时间,因为对于CLR和操作系统而言,它们认为现在正在执行一个非常重要的任务,就会给它分配相应的资源。因此,通常情况下使用自旋+阻塞会更加高效一些。

        class Program
        {
             static void Main(string[] args)
           {
               int i = 0;
               Thread thread =new Thread(() =>{ Thread.Sleep(1000 * 2); i++; });
               thread.Start();
               while(i<= 0) {
                   Thread.Sleep(10);
                   Thread.MemoryBarrier();
               }
               i++;
               Console.WriteLine(i);
               Console.WriteLine("按任意键退出");
               Console.ReadLine();
           }
        }
    

    2、锁同步

    使用锁来协调线程的行为是日常开发中最为常见的方式,锁有一个特性,那就是对于同一个同步对象,在同一时间只能有一个线程获得这个对象的锁,如果存在竞争的话,其他线程就得等待获得锁的线程释放锁之后才能获得。在使用锁的时候,第一步是选取同步对象,同步对象必须对要进行协调的每个线程都是可见的。第二步是在线程中将指定的代码段放到锁控制的区域内。值得注意的是,一个对象并不会因为做为同步对象而导致使用受限。比如lock(x){/* 被锁控制的代码段 */},x.ToString()仍然是可以正常使用的,不会受到任何影响。

    2.1、lock

    lock关键字是由C#语法所支持的,它能够对一个同步对象加锁和释放锁。

    class Program
        {
            /// <summary>
            /// 同步对象
            /// </summary>
            static readonly object _lockObj = new object();
            /// <summary>
            /// 线程局部的随机数对象
            /// </summary>
            static ThreadLocal<Random> _threadRandom=new ThreadLocal<Random>(()=>new Random(Guid.NewGuid().GetHashCode()));
            static void Main(string[] args)
            {
                int i = 0;
                Thread thread1 = new Thread(() =>
                {
                    lock (_lockObj)
                    {
                        int j = i;
                    Thread.Sleep(_threadRandom.Value.Next(1000,1000*10));
                        i = j + 1;
                    }
    
                });
                thread1.Start();
                lock (_lockObj)
                {
                    int k = i;
                    Thread.Sleep(_threadRandom.Value.Next(1000,1000*10));
                    i = k + 1;
                }
                thread1.Join();
                Console.WriteLine(i);
                Console.WriteLine("按任意键退出");
                Console.ReadKey();
            }
        }
    

    2.2、Monitor

    lock关键字实际上是由C#提供的语法糖,lock关键字最终会有编译器翻译为Monitor的实现。Monitor类提供了两个静态方法Enter()和Exit(),两个方法都接受一个同步对象,前者是获取这个通过对象的锁,后者是释放这个同步对象的锁。在C#4.0之前的版本中,编译器将lock关键字翻译为 Monitor.Enter(_lockObj);try{/* 需要同步的代码 */}finally{ Monitor.Exit(_lockObj);},这种模式实际上是存在锁泄露的问题,比如在Enter方法中获得锁之后抛出了异常,或者Enter()与try之间发上了异常(其他线程调用了Abort或者内存溢出),都会导致已经获得到锁无法被释放。在C#4.0中,lock关键字被翻译为另外一种模式

    class Program
        {
            static readonly object _lockObj=new object();
            static ThreadLocal<Random> _threadRandom = new ThreadLocal<Random>(() => new Random(Guid.NewGuid().GetHashCode()));
            static void Main(string[] args)
            {
                int i = 0;
                Thread thread1 = new Thread(() =>
                {
                    bool lockTaken1 = false;
                    try
                    {
                        Monitor.Enter(_lockObj, ref lockTaken1);//lockTaken当前仅当enter方法抛出异常并且没有获得锁的时候为false
                        int j = i;
                        Thread.Sleep(_threadRandom.Value.Next(1000,1000*10));
                        i = j + 1;
                    }
                    finally
                    {
                        if (lockTaken1)
                        {
                            Monitor.Exit(_lockObj);
                        }
                    }
                });
                thread1.Start();
                bool lockTaken2 = false;
                try
                {
                    Monitor.Enter(_lockObj, ref lockTaken2);//lockTaken当前仅当enter方法抛出异常并且没有获得锁的时候为false
                    int j = i;
                    Thread.Sleep(_threadRandom.Value.Next(1000,1000*10));
                    i = j + 1;
                }
                finally
                {
                    if (lockTaken2)
                    {
                        Monitor.Exit(_lockObj);
                    }
                }
                thread1.Join();
                Console.WriteLine(i);
                Console.WriteLine("按任意键退出");
                Console.ReadLine();
            }
        }
    

    2.3 死锁

    当两个线程等待的资源都被对方占用时,就会造成死锁

    class Program
        {
            static readonly object _firstLockObj = new object();
            static readonly object _secondLockObj = new object();
            static void Main(string[] args)
            {
                Thread thread1 = new Thread((name) =>
                 {
                     lock (_firstLockObj)
                     {
                         Console.WriteLine($"{name}获得了第一个锁");
                         Thread.Sleep(1000);
                         Console.WriteLine($"{name}准备获取第二个锁");
                         lock (_secondLockObj)
                         {
                             Console.WriteLine($"{name}先后获得了第一个锁和第二个锁。");
                         }
                     }
                 });
                Thread thread2 = new Thread((name) =>
                 {
                     lock (_secondLockObj)
                     {
                         Console.WriteLine($"{name}获得了第二个锁");
                         Thread.Sleep(1000);
                         Console.WriteLine($"{name}准备获取第一个锁");
                         lock (_firstLockObj)
                         {
                             Console.WriteLine($"{name}先后获得了第二个锁和第一个锁。");
                         }
                     }
                 });
                thread1.Start("第一个线程");
                thread2.Start("第一个线程");
                thread1.Join();
                thread2.Join();
                Console.WriteLine("程序执行结束!按任意键退出...");
                Console.ReadLine();
            }
        }
    

    2.4 嵌套锁

    lock和Monitor都支持对同一个同步对象进行嵌套加锁,内层对同一个同步对象加锁时并不会因为外层已经加锁而无法获得锁,相反时可以正常获得的,对同一个同步对象的嵌套锁只要释放锁的次数与加锁的次数保持一致即可。

     class Program
        {
            static object _lockObj = new object();
            static void Main(string[] args)
            {
                lock (_lockObj)
                {
                    lock (_lockObj)
                    {
    
                    }
                }
            }
        }
    

    3、信号同步

    C#中提供了多种用于线程间通信的信号构造,通过它们就能够协调线程之间的行为,进而实现线程同步最终目的。

    3.1、SemaphoreSlim

    通常用于限制对某一段代码段并发量,SemaphoreSlim是Semaphore的轻量级版本,在性能上较Semaphore也有所提升,但是SemaphoreSlim不能跨进程使用。在实例化SemaphoreSlim的时候需要指定能同时运行的线程数量,一旦同时运行(调用WaitOne的次数)的线程数量超过了指定的值,超过的线程排队等待,直到之前通过的线程运行结束,并释放状态(调用Release方法)。举一个生活中常见的例子,有一群人需要到公园里面的一个凳子上坐一会儿,但是这个凳子同时只能做3个人,这样的话,没有坐在凳子上的人就需要排队等待,直到坐在凳子上的人离开。

    class Program
       {
           static SemaphoreSlim _semaphoreSlim =new SemaphoreSlim(3);
           static void Main(string[] args)
           {
               for(int i = 0; i<5; i++)
               {
                   //刚开始只能够有三个线程通过_semaphoreSlim.Wait();,其他线程均在此处阻塞
                   //直到前面通过的线程调用Release为止
                   Work(i);
               }
               Console.ReadLine();
           }
           static void Work(int i)
           {
               ThreadPool.QueueUserWorkItem((state) =>
               {
                   Console.WriteLine($"{i}准备运行");
                   _semaphoreSlim.Wait();
                   Console.WriteLine($"{i}开始运行");
                   Thread.Sleep(1000*10);//休眠十秒,模拟长时间运行
                   Console.WriteLine($"{i}运行结束");
                   _semaphoreSlim.Release();
               },i);
           }
       }
    

    3.2、AutoResetEvent

    顾名思议,这是一个自动复位的信号量。AutoResetEvent好比是一个火车站的检票闸机,一张票只能过一个人,并且人过去后闸机门自动关闭。调用Set方法,就好比在闸机中插入一张票;调用WaitOne方法,就好比在闸机前等待门打开的人。如果票插入了,但是一直没有人通过,这个门会一直打开。如果在此时没有人等待,但插入了大于一张的票,那么生效只有一张票,其余的票都将作废。
    在实例化AutoResetEvent对象的时候,需要指定AutoResetEvent的初始状态(也就是闸机门是否打开)。

     class Program
        {
            static AutoResetEvent _autoResetEvent = new AutoResetEvent(false);
            static void Main(string[] args)
            {
                for (int i = 0; i < 5; i++)
                {
                    //同时执行5个任务
                    //但这5个任务都会被阻塞掉,直到调用Set方法为止
                    Work(i);
                }
                foreach (int i in Enumerable.Range(0, 5))
                {
                    Console.WriteLine("是否打开一次闸机门y/n");
                    string option = Console.ReadLine();
                    if (option.Equals("y", StringComparison.OrdinalIgnoreCase))
                    {
                        _autoResetEvent.Set();
                    }
                }
                Console.ReadLine();
            }
            static void Work(int i)
            {
                Task.Factory.StartNew((state) =>
                {
                    _autoResetEvent.WaitOne();
                    Console.WriteLine($"{state}开始运行啦");
                    Thread.Sleep(1000);
                    Console.WriteLine($"{state}运行结束啦");
                }, i);
            }
        }
    

    3.3、ManualResetEvent

    与AutoResetEvent不同的是ManualReset是手动复位的,它就好比是一扇大门,门开了(调用Set方法),在门外等待(调用WaitOne方法的线程)的所有人都可以通过这个门。门关了(调用Reset方法),在门外的人就得等待。在实例化的ManualResetEventSlim的时候,同样需要指定初始状态(也就是门是否打开)。

    class Program
        {
            static ThreadLocal<Random> _threadRandom = new ThreadLocal<Random>(() => new Random(Guid.NewGuid().GetHashCode()));//ThreadLocal是用于存储每个线程特别的变量。
            static ManualResetEvent _manualResetEvent = new ManualResetEvent(false);
            static void Main(string[] args)
            {
                for (int i = 0; i < 5; i++)
                {
                    //同时执行5个任务
                    //但这5个任务都会被阻塞掉,直到调用Set方法为止
                    Work(i);
                }
                foreach (int i in Enumerable.Range(0, 5))
                {
                    Console.WriteLine("开门/关门:y/n");
                    string option = Console.ReadLine();
                    if (option.Equals("y", StringComparison.OrdinalIgnoreCase))
                    {
                        _manualResetEvent.Set();
                    }
                    else
                    {
                        _manualResetEvent.Reset();
                    }
                }
                Console.ReadKey();
            }
            static void Work(int i)
            {
                Task.Factory.StartNew((state) =>
                {
                    Thread.Sleep(_threadRandom.Value.Next(1, 1000 * 20));
                    Console.WriteLine($"{state}等待开门");
                    _manualResetEvent.WaitOne();
                    Console.WriteLine($"{state}运行结束啦");
                }, i);
            }
        }
    

    3.4、CountdownEvent

    这就好比是一辆车,车上座位是固定的(初始化的时候指定的),每来一个人(调用一次Signal),车上座位就会少一个,当人满的时候,车就可以开走了(调用Wait的地方就可以继续运行了)。

    class Program
        {
            static ThreadLocal<Random> _threadRandom = new ThreadLocal<Random>(() => new Random(Guid.NewGuid().GetHashCode()));//ThreadLocal是用于存储每个线程特别的变量。
            static readonly int _waitCount = 10;
            static CountdownEvent _countdownEvent = new CountdownEvent(_waitCount);
    
            static void Main(string[] args)
            {
                Console.WriteLine("等待客人");
                WaitPersons();
                _countdownEvent.Wait();
                Console.WriteLine("客人已满!发车");
                Console.ReadKey();
            }
            static void WaitPersons()
            {
                for (int i = 0; i < _waitCount; i++)
                {
                    Task.Factory.StartNew(() =>
                    {
                        Thread.Sleep(_threadRandom.Value.Next(1, 1000 * 10));
                        _countdownEvent.Signal();
                        Console.WriteLine($"来了一个人");
                    });
                }
    
            }
        }
    

    3.5、等待句柄与线程池结合

    如果应用中有都会等待某一个信号的话,那么可能会造成大量的资源浪费,为了解决这个问题C#提供了一种将等待句柄与线程池结合的处理方式。

    class Program
        {
            static ManualResetEvent _manualResetEvent = new ManualResetEvent(false);
    
            static void Main(string[] args)
            {
                Work();
                Console.ReadKey();
            }
            static void Work()
            {
                for (int i = 0; i < 200; i++)
                {
                    new Thread(WaitAutoResetSignal).Start(i);
                }
            }
            static void WaitAutoResetSignal(object state)
            {
                //_manualResetEvent.WaitOne();
                //Console.WriteLine($"{state}任务运行结束");
                ThreadPool.RegisterWaitForSingleObject(_manualResetEvent, ThreadPoolWaitCallBack, state, -1, true);
            }
            static void ThreadPoolWaitCallBack(object state, bool isTimeOut)
            {
                Console.WriteLine($"{state}任务运行结束");
            }
        }
    

    3.6、WaitAny、WaitAll和SignalAndWait

    WaitHandle还提供了WaitAny、WaitAll和SignalAndWait,这三种方法的作用都能根据字面意思得出,其中有一点需要说明的是exitContext参数,这个参数里面所说的“同步上下文” 指的是CLR中的自动锁机制的同步域,一个线程在同步域中,其他线程都将不能进入该同步域。这个参数很少去设置它,保持默认的就ok,当你真正需要设置它的时候,你也就知道怎么设置了。

    4、Interlocked

    如果一条指令在底层的处理器就就已经是不可分割的话,那么它就可以阻止任何竞争的发生。在32位环境中,长度小于等于32位的类型,进行简单的读或者写是原子的。在64位环境中,长度小于等于64位的类型,进行简单的读或者写诗原子的。但如果一条语句结合了多条读写操作的语句,那它必然不是原子的。但为了实现这些操作的原子性,C#提供了Interlocked来实现。

    相关文章

      网友评论

          本文标题:二、线程同步

          本文链接:https://www.haomeiwen.com/subject/axzjnctx.html