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

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

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

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;}elseConsole.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;}elseConsole.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;}elseConsole.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;}elseConsole.WriteLine($"编号{num}:买单失败");}finally{Monitor.Exit(lockobj);}           }


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

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

        static void Pay(int num){lock(lockobj){try{//将当前拥有锁的线程进入等待队列,再次阻塞,直到再次获得资源Monitor.Wait(lockobj);if (!hasPay){Console.WriteLine($"编号{num}:已经买单了");hasPay = true;}elseConsole.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();}

C# 线程同步的几种方法相关推荐

  1. C#线程同步的几种方法

    在网上也看过一些关于线程同步的文章,其实线程同步有好几种方法,下面我就简单的做一下归纳. 一.volatile关键字 volatile是最简单的一种同步方法,当然简单是要付出代价的.它只能在变量一级做 ...

  2. 归纳一下:C#线程同步的几种方法

    我们在编程的时候,有时会使用多线程来解决问题,比如你的程序需要在后台处理一大堆数据,但还要使用户界面处于可操作状态:或者你的程序需要访问一些外部资源如数据库或网络文件等.这些情况你都可以创建一个子线程 ...

  3. unix c线程同步的三种方法:互斥量、读写锁以及条件变-xhb8413-ChinaUnix博客

    unix c线程同步的三种方法:互斥量.读写锁以及条件变-xhb8413-ChinaUnix博客 unix c线程同步的三种方法:互斥量.读写锁以及条件变 2012-03-30 14:42:38 分类 ...

  4. linux:线程同步的5种方法

    linux:线程同步的5种方法 一.为什么要使用线程: 二.线程同步的5种方法 2.1 互斥量 2.2 读写锁 2.3 条件变量 2.4 自旋锁 2.5 屏障 一.为什么要使用线程: <1> ...

  5. C++中线程同步的四种方法(Win32平台)

    1.同步和互斥 互质是一种特殊的同步.线程同步一般指线程之间的执行存在某种程度上的相互依赖关系. 2.C++中线程同步的四种方法 (1)事件(Event); (2)信号量(semaphore); (3 ...

  6. 15.线程同步的几种方法

    一.为什么需要线程同步 线程同步通常是出现在多线程环境下的问题,对于多个线程同时访问的共享内存中的变量,如果不进行保护,就会导致一些列数据出错问题.以下图为例: 假设线程A在第一次读取变量的值为10, ...

  7. 关于线程同步的几种方法

    java允许多线程,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突. 比方说,我们在买火车票的时候,如何能确定余票数据准确而无误差,这个时候就需要 ...

  8. java线程同步的7种方法

    为何要使用同步?      java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),      将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有 ...

  9. java线程同步的五种方法

    2019独角兽企业重金招聘Python工程师标准>>> 1.同步方法 即有synchronized关键字修饰的方法. 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, ...

  10. 实现线程同步的几种方法

    在多线程程序中,会出现多个线程抢占一个资源的情况,这时间有可能会造成冲突,也就是一个线程可能还没来得及将更改的 资源保存,另一个线程的更改就开始了.可能造成数据不一致.因此引入多线程同步,也就是说多个 ...

最新文章

  1. jquery mobile 页面间的传递参数
  2. nc65数据字典 云盘_从搜索引擎到核心交易数据库,详解阿里云神龙如何支撑双11...
  3. 攻破c语言笔试与机试难点,如何攻破C语言学习、笔试与机试的难点.doc
  4. 【机器学习】周志华 读书笔记 第一章 绪论
  5. MySQL杂记(更新时间——2014.05.23)
  6. Linux系统文件与目录权限管理
  7. 科普:手机里的陀螺仪到底是什么
  8. java HashMap的keyset方法
  9. 一机双平面、TCP半连接攻击——SYN攻击详解
  10. u-boot-2012.04.01移植笔记——支持NAND启动
  11. HART协议学习记录
  12. 计算机无法打印图片,为什么电脑打印不了图片 电脑里的图片无法打印处理方法...
  13. ajax响应速度慢,jQuery Ajax请求的响应速度变化
  14. Re10:读论文 Are we really making much progress? Revisiting, benchmarking, and refining heterogeneous gr
  15. python怎么输入矩阵命令_python矩阵操作
  16. 开源中国众包第三波阿里云悬赏项目,总金额 6 万
  17. 常见的两种解空间 全排列与幂集
  18. Vite入门从手写一个乞丐版的Vite开始(下)
  19. 【数学 博弈论】JZOJ_3339 wyl8899和法法塔的游戏
  20. pytorch深度学习_用于数据科学家的深度学习的最小pytorch子集

热门文章

  1. 批量修改名称百度网盘在线处理之添加序号(四)
  2. 基于PCIE 闪存卡的 Oracle 数据
  3. axios封装nodejs前后端实现getpost文件上传
  4. [附源码]java毕业设计二手车交易系统
  5. OSG —— 笔记2 - 加载模型(附源码)
  6. 冷冻电镜(cryo-EM)三维图像重构软件Relion工作站配置推荐
  7. 理解 Python 中的 for 循环
  8. 信合考试计算机知识,2015年陕西信合考试试题答案――计算机基础知识一
  9. 基于区块链技术的溯源系统-总结
  10. 【我的创作纪念日】恒川的创作一周年