C# 线程同步的几种方法

有时候必须访问变量、实例、方法、属性或者结构体,而这些并没有准备好用于并发访问,或者有时候需要执行部分代码,而这些代码必须单独运行,这是不得不通过将任务分解的方式让它们独立运行。
当任务和线程要访问共享的数据和资源的时候,您必须添加显示的同步,或者使用原子操作或锁。

下面举个例子:
我们吃饭用手机点菜的时候,多个人同时点菜,在最后结账的时候,如果大家都争着买单,那如果没有同步信息,就会造成多个人都买单成功。这就是线程同步的问题之一。

下面介绍几种线程同步的方法:

1、锁 SpinLock 、Mutex、Monitor、lock

SpinLock
自旋锁是指当一个线程在获取锁对象的时候,如果锁已经被其它线程获取,那么这个线程将会循环等待,不断的去获取锁,直到获取到了锁。
优点:避免了线程上下文切换。性能较高。
缺点:如果长时间等待,将消耗大量的CPU资源。而且多个等待中的线程,并不是等待时间越长就先获取到锁,可能会一直等待下去。

主要的方法:
在这里插入图片描述

        static SpinLock SpinLock = new SpinLock();
        static bool hasPay = false;
        static void Pay(int num)
        {
               bool locked = false;
                SpinLock.Enter(ref locked);//获取锁
                if (!hasPay)
                {
                    Console.WriteLine($"编号{num}:已经买单了");
                    hasPay = true;
                }
                else
                    Console.WriteLine($"编号{num}:买单失败");
                //检测是否释放锁,没有的话进行释放
                if (locked)
                    SpinLock.Exit(locked);         
        }
         static void Main(string[] args)
        {
            for(int i=1;i<10;i++)
            {
                Task.Factory.StartNew( obj=> { Pay((int)obj); },i);
            }        
            Console.ReadLine();
        }    

Mutex:
互斥锁是一个互斥的同步对象,意味着同一时间有且仅有一个线程可以获取它,互斥锁可适用于一个共享资源每次只能被一个线程访问的情况,如果线程获取互斥体,则需要获取该互斥体的第二个线程将挂起,直到第一个线程释放该互斥体。

在 Mutex 类中,WaitOne() 方法用于等待资源被释放, ReleaseMutex() 方法用于释放资源。WaitOne() 方法在等待 ReleaseMutex() 方法执行后才会结束。
在这里插入图片描述在这里插入图片描述

        static Mutex mutex = new Mutex();//互斥锁      
        static bool hasPay = false;
        static void Pay(int num)
        {
            if (mutex.WaitOne())
            {
                try
                {
                    if(!hasPay)
                    {
                        Console.WriteLine($"编号{num}:已经买单了");
                        hasPay = true;
                    }
                    else
                        Console.WriteLine($"编号{num}:买单失败");
                }
                finally
                {                    
                   mutex.ReleaseMutex();
                }
            }
        }  
         static void Main(string[] args)
        {
            for(int i=1;i<10;i++)
            {
                Task.Factory.StartNew( obj=> { Pay((int)obj); },i);
            }        
            Console.ReadLine();
        }    

lock:
1、lock锁定的是一个引用类型,值类型不能被lock
2、避免lock一个string,因为程序中任何跟锁定的string的字符串是一样的都会被锁定。

static void Pay(int num)
        {
           lock(lockobj)
            {
                    if(!hasPay)
                    {
                        Console.WriteLine($"编号{num}:已经买单了");
                        hasPay = true;
                    }
                    else
                        Console.WriteLine($"编号{num}:买单失败");
            }
        }

Monitor:
Monitor类提供了与lock类似的功能,不过与lock不同的是,它能更好的控制同步块,当调用了Monitor的Enter(Object o)方法时,会获取o的独占权,直到调用Exit(Object o)方法时,才会释放对o的独占权。
可以使用 TryEnter() 方法可以给它传送一个超时值,决定等待获得对象锁的最长时间,该方法能在指定的毫秒数内结束线程,这样能避免线程之间的死锁现象。
主要方法:
在这里插入图片描述

        static void Pay(int num)
        {
                try
                {
                    Monitor.Enter(lockobj);
                    if(!hasPay)
                    {
                        Console.WriteLine($"编号{num}:已经买单了");
                        hasPay = true;
                    }
                    else
                        Console.WriteLine($"编号{num}:买单失败");
                }
                finally
                {
                    Monitor.Exit(lockobj);
                }           
        }

ia
其实以上三种的方式都差不多,输出结果也是一样的。

同时使用Monitor和lock可以使lock释放当前资源,使当前线程阻塞:

        static void Pay(int num)
        {
           lock(lockobj)
            {
                try
                {
                    //将当前拥有锁的线程进入等待队列,再次阻塞,直到再次获得资源
                     Monitor.Wait(lockobj);
                    if (!hasPay)
                    {
                        Console.WriteLine($"编号{num}:已经买单了");
                        hasPay = true;
                    }
                    else
                        Console.WriteLine($"编号{num}:买单失败");
                  
                }
                finally
                {                    
                    //给等待队列的线程进入就绪队列
                    Monitor.Pulse(lockobj);                  
                }
            }
        }
        static void VipPay()
        {
            lock (lockobj)
            {
                try
                {
                    //将等待队列中的线程进入就绪
                    Monitor.Pulse(lockobj);
                    Console.WriteLine("Vip插队买单");
                    hasPay = true;
                }
                finally
                {
                    //让其他的等待队列的线程进入就绪队列
                    Monitor.Pulse(lockobj);
 
                }
            }
        }
        static void Main(string[] args)
        {
                Task.Factory.StartNew( obj=> { Pay((int)obj); },1);
                Thread.Sleep(50);
                Task.Factory.StartNew(VipPay);
                Console.ReadLine();
        }

运行结果:
在这里插入图片描述
lock和Monitor的区别
一、lock的底层本身是Monitor来实现的,所以Monitor可以实现lock的所有功能。
二、Monitor有TryEnter的功能,可以防止出现死锁的问题,lock没有。

Mutex和前面两者的区别:

Mutex消耗较大的资源,不适合频繁的操作,会降低操作的效率。
Mutex本身是可以系统级别的,所以是可以跨越进程的。比如我们要实现一个软件不能同时打开两次,那么Mutex是可以实现的,而lock和monitor是无法实现的

2、使用SemaphoreSlim和Semaphore

信号量分为两种类型:本地信号量和命名系统信号量。 本地信号灯对应用程序而言是本地的,系统信号量在整个操作系统中均可见,适用于进程间同步。 SemaphoreSlim 是不使用 Windows 内核信号量的 Semaphore 类的轻型替代项。 与 Semaphore 类不同,SemaphoreSlim 类不支持已命名的系统信号量。 只能将其用作本地信号量。 SemaphoreSlim 类是用于在单个应用内进行同步的建议信号量。

限制同时访问同一个资源的线程数量,实例化信号量时,可以指定可同时进入信号量的最大线程数。 还可以指定可同时进入信号量的初始线程数,每次线程进入信号量时,计数就会减少,每次线程释放信号量时都会递增。
以下例子是限制每桌只有三个位,同时吃饭的人只有三个,多余的则需要等位。在初始化的时候设定最多的并发数,调用wait方法限制,当吃饭完后则调用release,发出信号给其他线程。
在这里插入图片描述

        static SemaphoreSlim semaphore = new SemaphoreSlim(3);
        static void Eat(int num)
        {
            Console.WriteLine($"编号{num}顾客等位");
            semaphore.Wait();
            Console.WriteLine($"有位了!编号{num}的顾客请用餐");
            Thread.Sleep(100);
            Console.WriteLine($"编号{num}吃完走了");
            semaphore.Release();
        }
        static void Main(string[] args)
        {
            for(int i=1;i<6;i++)
            {
                Task.Factory.StartNew( obj=> { Eat((int)obj); },i);
            }
            Console.ReadLine();
        }

在这里插入图片描述

3、AutoResetEvent和ManualResetEvent

Reset ():将事件状态设置为非终止状态,导致线程阻止;如果该操作成功,则返回true;否则,返回false。
Set ():将事件状态设置为终止状态。
WaitOne(): 阻止当前线程,直到收到信号。
WaitOne(TimeSpan, Boolean) :阻止当前线程,直到当前实例收到信号,使用 TimeSpan 度量时间间隔并指定是否在等待之前退出同步域。

下面先演示一下AutoResetEvent ,在这段代码中,每次走一个人都进行了set一次,发信号给下一个人。

        static AutoResetEvent evenResetEvent = new AutoResetEvent(false);
        static ManualResetEvent manualResetEvent = new ManualResetEvent(false);

        static void Eat(int num)
        {
            Console.WriteLine($"编号{num}顾客等位");
            evenResetEvent.WaitOne();
            Console.WriteLine($"有位了!编号{num}的顾客请用餐");
            Console.WriteLine($"编号{num}吃完走了");
            //走一个set一次,发信号给下一个人
            evenResetEvent.Set();
        }
        static void Main(string[] args)
        {
            for(int i=1;i<5;i++)
            {
                Task.Factory.StartNew( obj=> { Eat((int)obj); },i);
                Thread.Sleep(50);
            }
            evenResetEvent.Set();
            //前面的线程运行完成后,再执行下面这段线程,会发现线程被阻塞
            Thread.Sleep(1000);
            evenResetEvent.WaitOne();
            Task.Factory.StartNew(obj => { Eat((int)obj); }, 10);
            Console.ReadLine();
        }

在这里插入图片描述

演示一下ManualResetEvent,这段代码则创建线程后,在主线程set一次后不再进行set。

 static ManualResetEvent manualResetEvent = new ManualResetEvent(false);
        static int nummm = 0;
        static void Eat(int num)
        {
            Console.WriteLine($"编号{num}顾客等位");
            manualResetEvent.WaitOne();
            Console.WriteLine($"有位了!编号{num}的顾客请用餐");
            Console.WriteLine($"编号{num}吃完走了");
            //这里没有每次都set,只是在主线程那里set了一次
           // evenResetEvent.Set();
        }
        static void Main(string[] args)
        {
            for(int i=1;i<5;i++)
            {
                Task.Factory.StartNew( obj=> { Eat((int)obj); },i);
                Thread.Sleep(50);
            }
            manualResetEvent.Set();
             //前面的线程运行完成后,再执行下面这段线程,会发现线程依然正常运行。
            Thread.Sleep(1000);
            manualResetEvent.WaitOne();
            Task.Factory.StartNew(obj => { Eat((int)obj); }, 10);
            Console.ReadLine();
        }

在这里插入图片描述

根据两段代码的运行结果,可以得出他们的不同点:
AutoResetEvent 收到 Set 后 , 一次只能执行一个线程,其它线程继续 WaitOne 。
ManualResetEvent 收到 Set 后,所有处理 WaitOne 状态线程均继续执行,但是如果set之后不进行reset重置信号的话的话,下次进入的线程就算调用了waitOne也不会阻塞,上一个set的信号会继续生效。

4、Barrier

官方解释:使多个任务能够采用并行方式依据某种算法在多个阶段中协同工作,简单来说就是需要所有的信号都到达后都才能进行下一步。每次执行完一个动作后,信号个数都会重置为,下次还是依旧等待初始化设置的信号个数。
在这里插入图片描述
如上图所示,有四个任务,每个任务里面做各自的事情,当有一个屏障发出信号后就会开始等待,等待到4个全部到达,就会解除等待,同时进行下一步。

主要方法:

在这里插入图片描述
在这里插入图片描述

  static Barrier Barrier = null;
        static void Eat(int num)
        {
            Console.WriteLine($"编号{num}顾客到了");
            //发出一个信号,等到4个信号均到达才往下进行
            Barrier.SignalAndWait();
            
            Console.WriteLine($"到齐了上菜,可以用餐了");
            Barrier.SignalAndWait();
            
            Console.WriteLine($"编号{num}饱了,呃......");
            Barrier.SignalAndWait();

        }
        static void Main(string[] args)
        {
            Barrier = new Barrier(4);
            for (int i=1;i<5;i++)
            {
                Task.Factory.StartNew( obj=> { Eat((int)obj); },i);
                Thread.Sleep(50);
            }
            //等待所有线程执行完毕
            Task.WaitAll();
            Console.WriteLine($"都吃完溜了");
            Console.ReadLine();
        }

在这里插入图片描述

5、CountdownEvent

指在被发信号一定次数后,它会解除对等待线程的阻塞,设置等待的次数,待所有线程信号到达后进行下一步。但是每次信号到达后可以重置信号数量,每次调用wait后将不再阻塞,直到调用Reset()函数。
CountdownEvent可以通过TryAddCount()和AddCount()函数来增加函数Signal() 需被调用的次数,但只有当CountDownEvent处于未就绪态时才会成功。
在这里插入图片描述

  static CountdownEvent countdownEvent = null;
        static void Eat(int num)
        {
            Console.WriteLine($"编号{num}顾客到了");
           
            Console.WriteLine($"编号{num}饱了,呃......");
            countdownEvent.Signal();
        }
        static void Main(string[] args)
        {
            countdownEvent = new CountdownEvent(2);
           // Barrier = new Barrier(4);
            for (int i=1;i<3;i++)
            {
                Task.Factory.StartNew( obj=> { Eat((int)obj); },i);
                Thread.Sleep(50);
            }
            //在此阻塞,全部信号到达后可以进行下一步
            countdownEvent.Wait();
             Console.WriteLine($"都吃完溜了");
            //可以重置信号数量为4个,下次调用wait的时候则需要等待4个信号
            //countdownEvent.Reset(4);
            Console.ReadLine();
        }

在这里插入图片描述

  • 8
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值