【Go】并发编程
Go语言宣扬用通讯的方式共享数据。
Go语言以独特的并发编程模型傲视群雄,与并发编程关系最紧密的代码包就是sync包,意思是同步。同步的用途有两个,一个是避免多个线程在同一时刻操作同一个数据块,另一个是协调多个线程,以避免它们在同一时刻执行同一块代码。由于这一的数据库和代码块的背后都隐含着一种或多种资源,所以可以把它们看成是共享资源,同步就是控制多个线程对共享资源的访问。
一个线程在想要访问某一个共享资源时,需要先申请对该资源的访问权限,并且只有在申请成功之后,访问才能真正开始,而当线程对共享资源的访问结束时,它还必须归还对该资源的访问权限,若要再次访问仍需申请。多个并发运行的线程对一个共享资源的访问是完全串行的。
在Go语言中,最常用的同步工具当属互斥量(mutex)。sync包中的Mutex就是与其对应的类型,该类型的值可以被称为互斥量或互斥锁。一个互斥锁可以被用来保护一个临界区或者一组临界区,可以通过它来保证,在同一时刻只有一个goroutin处于该临界区之内。每当有goroutine想进入临界区时,都需要先对它进行锁定,并且每个goroutine离开临界区时,都要及时对它进行解锁。锁定操作可以通过调用互斥锁的Lock方法实现,解锁操作可以调用互斥锁的Unlock方法
mu.Lock()
_, err := writer.Write([]byte(data))
if err != nil {
log.Printf("error: %s [%d]", err, id)
}
mu.Unlock()
使用互斥锁的注意事项有:
1.不要重复锁定互斥锁
对一个已经被锁定的互斥锁进行锁定,会立即阻塞当前的goroutine
当Go语言运行时系统发现所有的用户级goroutine都处于等待状态(死锁),就会自行抛出一个带有如下信息的panic:
fatal error: all goroutines are asleep - deadlock!
这种由Go语言运行时系统自行抛出的panic属于致命错误,都是无法被恢复的,调用recover函数对它们起不到任何作用,即一旦产生死锁,程序必然奔溃+
避免这种情况的发生,最简单有效的方式就是让每一个互斥锁都只保护一个临界区
2.不要忘记解锁互斥锁,必要时使用defer语句
忘记解锁会使其他goroutine无法进入到该互斥锁保护的临界区,这轻则会导致一些程序功能的失效,重则会造成死锁和程序奔溃。
3.不要对尚未锁定或者已解锁的互斥锁解锁
解锁为锁定的锁会立即引发panic,应该总是抱着,对每一个锁定操作,都要有且只有一个对应的解锁操作。
4.不要在多个函数之间直接传递互斥锁
Go语言中的互斥锁是开箱即用的,一旦声明了一个sync。Mutex类型的变量,就可以直接使用它。但该类型是一个结构体类型,属于值类型的一种,把它传给一个函数、将它从函数中返回、把它赋值给其他变量、让它进入某个通道都会导致它的副本的产生。并且原值和它的副本,以及多个副本之间都是完全独立的,它们都是不同的互斥锁。如果把一个互斥锁作为参数值传给了一个函数,那么在这个函数中对传入的锁的所有操作,都不会对存在于该函数之外的那个原锁产生任何影响。
读写锁和互斥锁有哪些异同?
读写锁是读/写互斥锁的简称。在Go语言中,读写锁由sync.RWMutex类型的值代表,也是开箱即用的。读写锁把对共享资源的读操作和写操作区别对待了,它可以对这两种操作施加不同程度的保护。
一个读写锁实际上包含了两个锁,即:读锁和写锁。sync.RWMutex类型中的Lock方法和Unlock方法分别用于对写锁进行锁定和解锁,而它的RLock方法和RUnlock方法则分别用于对读锁进行锁定和解锁
另外,对于同一个读写锁来说有如下规则:
- 在写锁已被锁定的情况下再试图锁定写锁,会阻塞当前的goroutine
- 在写锁已被锁定的情况试图锁定读锁,会阻塞当前goroutine
- 在读锁已被锁定的情况下锁定写锁,会阻塞当前goroutine
- 在读锁已被锁定的情况下再试图锁定读锁,不会阻塞当前的goroutine
条件变量(conditional variable)
条件变量并不是被用来保护临界区和共享资源的,它是用于协调想要访问共享资源的那些线程的。当共享资源的状态发生变化时,它可以被用来通知被互斥锁阻塞的线程。
条件变量提供三个方法:等待通知(wait)、单发通知(signal)和广播通知(broadcast)。在等待通知的时候需要在条件变量基于的那个互斥锁的保护下进行。在进行单发或者广播通知时,需要在对应互斥锁解锁之后做这两种操作。
举个栗子,两个人在执行秘密任务,需要在不直接联系和见面的前提下进行,一个人需要向信箱里放置情报,另一个人需要从信箱里获取情报,这个信箱就如同一个共享资源。
var mailbox uint8 // 信箱,值为0表示情报,值为1表示有情报
var lock sync.RWMutex // 读写锁
//sync.Cond类型不是开箱即用,需要利用sync.NewCond来创建。
sendCond := sync.NewCond(&lock) //*sync.Cond类型
recvCond := sync.NewCond(lock.RLocker()) //*sync.Cond类型
条件变量是基于互斥锁的,因此这里的sync.Locker类型的参数值不可或缺。
sync.Locker是一个接口,在它声明中只包含两个方法的定义,Lock()和UnLock。sync.Mutex和sync.RWMutex类型都拥有Lock方法和Unlock方法,只不过它们都是指针方法。因此这两个类型的指针类型才算sync.Locker接口的实现类型。
这里在为sendCond做初始化时,把基于lock变量的指针值传给了sync.NewCond函数。因为lock变量的Lock方法和Unlock方法分别用于对写锁的锁定和解锁,它们与sendCond变量的含义是对应的。sendCond变量是专门为放置情报而准备的条件变量,向信箱中放置情报。
recvCond变量代表的是专门为获取情报而准备的条件变量。与sendCond不同,lock变量中用于对读锁进行锁定和解锁的方法是RLock和RUnlock,它们与sync.Locker接口中定义的方法并不匹配。需要调用sync.RWMutex类型的RLocker方法实现这一需求。lock.RLocker()得来的值所拥有的Lock方法和UnLock方法,在其内部会分别调用lock变量的RLock和RUnlock方法,即前两个方法仅仅是后两个方法的代理。
定义好了变量,那放置情报并通知另外一个人应该怎么做呢
lock.Lock() // 持有信箱上的锁,写操作
for mailbox == {
sendCond.Wait() //如果有情报,就等待
}
mailbox = //放入情报
lock.Unlock() //写完
recvCond.Signal()
获取情报
lock.RLock() // 读操作
for mailbox == {
recvCond.Wait() //没有情报
}
mailbox = //取走情报
lock.RUnlock() //读完
sendCond.Signal()
条件变量的Wait方法做了什么?
1、把调用它的goroutine(当前的goroutine)加入到当前条件变量的通知队列中
2、解锁当前条件变量基于的那个互斥锁
3、让当前的goroutine处于等待状态,等到通知到来时再决定是否唤醒它,这时这个goroutine就会阻塞在调用这个Wait方法的那行代码上
4、如果通知到来并且决定唤醒这个goroutine,那么就在唤醒它之后重新锁定当前条件变量基于的互斥锁。自此以后,当前的goroutine就会继续执行后面的代码
为什么先要锁定条件变量基于的互斥锁,才能调用它的Wait方法?
那因为条件变量的Wait方法在阻塞当前的goroutine之前会解锁它基于的互斥锁,所以在调用该Wait方法之前,必须先锁定那个互斥锁,否则在调用这个Wait方法时,会引发一个不可恢复的panic
为什么要用for语句包裹调用其Wait方法的表达式,用if语句不行吗?
显然,if语句只会对共享资源的状态检查一次,for语句可以做多次检查,直到这个状态改变为止。
那为什么要做多次检查呢?
主要是为了保险起见。如果一个goroutine因收到通知而被唤醒,但却发现共享资源的状态依然不符合它的要求,那么就应该再次调用条件变量的Wait方法,并继续等待下次通知的到来。
那什么时候会出现上述的情况呢?
1)有多个goroutine在等待共享资源的同一种状态。虽然等待的goroutine很多,但每次成功的goroutine却可能只有一个。成功的goroutine最终解锁互斥锁之后,其他的goroutine会先后进入临界区,但它们会发现共享资源状态依然不是它们想要的。
2)共享资源状态可能有的状态不是两个,如mailbox变量可能值不只有0和1,还有2,3,4。但每次改变后的结果只可能有一个,所以单一的结果一定不可能满足所有goroutine的条件,那些未被满足的goroutine需要继续等待。
3)在一些多CPU核心的计算机系统中,即使没有收到条件变量的通知,调用其Wait方法的goroutine也是有可能被唤醒的。这是硬件层面决定的。
条件变量的Signal方法和Broadcast方法有何异同?
条件变量的Signal方法和Broadcast方法都是用来发送通知的,不同的是,前者的通知只会唤醒一个因此而等待的goroutine,而后者的通知却会唤醒所有为此等待的goroutine。
条件变量的Wait方法总会把当前的goroutine添加到队列的队尾,而它的Signal方法总会从通知队列的队首开始查找可被唤醒的goroutine,所以,因Signal方法的通知而被唤醒的goroutine一般都是最早等待的那个。
条件变量的Signal方法和Broadcast方法不需要在互斥锁保护下执行。
条件变量的通知有即时性。即如果发生通知的时候没有goroutine为此等待,那么该通知就会被遗弃
【Go】并发编程的更多相关文章
- [ 高并发]Java高并发编程系列第二篇--线程同步
高并发,听起来高大上的一个词汇,在身处于互联网潮的社会大趋势下,高并发赋予了更多的传奇色彩.首先,我们可以看到很多招聘中,会提到有高并发项目者优先.高并发,意味着,你的前雇主,有很大的业务层面的需求, ...
- 伪共享(false sharing),并发编程无声的性能杀手
在并发编程过程中,我们大部分的焦点都放在如何控制共享变量的访问控制上(代码层面),但是很少人会关注系统硬件及 JVM 底层相关的影响因素.前段时间学习了一个牛X的高性能异步处理框架 Disruptor ...
- 【Java并发编程实战】----- AQS(四):CLH同步队列
在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形.其主要从两方面进行了改造:节点的结构与节点等待机制.在结构上引入了头 ...
- 【Java并发编程实战】----- AQS(三):阻塞、唤醒:LockSupport
在上篇博客([Java并发编程实战]----- AQS(二):获取锁.释放锁)中提到,当一个线程加入到CLH队列中时,如果不是头节点是需要判断该节点是否需要挂起:在释放锁后,需要唤醒该线程的继任节点 ...
- 【Java并发编程实战】----- AQS(二):获取锁、释放锁
上篇博客稍微介绍了一下AQS,下面我们来关注下AQS的所获取和锁释放. AQS锁获取 AQS包含如下几个方法: acquire(int arg):以独占模式获取对象,忽略中断. acquireInte ...
- 【Java并发编程实战】-----“J.U.C”:CLH队列锁
在前面介绍的几篇博客中总是提到CLH队列,在AQS中CLH队列是维护一组线程的严格按照FIFO的队列.他能够确保无饥饿,严格的先来先服务的公平性.下图是CLH队列节点的示意图: 在CLH队列的节点QN ...
- 【Java并发编程实战】-----“J.U.C”:Exchanger
前面介绍了三个同步辅助类:CyclicBarrier.Barrier.Phaser,这篇博客介绍最后一个:Exchanger.JDK API是这样介绍的:可以在对中对元素进行配对和交换的线程的同步点. ...
- 【Java并发编程实战】-----“J.U.C”:CountDownlatch
上篇博文([Java并发编程实战]-----"J.U.C":CyclicBarrier)LZ介绍了CyclicBarrier.CyclicBarrier所描述的是"允许一 ...
- 【Java并发编程实战】-----“J.U.C”:CyclicBarrier
在上篇博客([Java并发编程实战]-----"J.U.C":Semaphore)中,LZ介绍了Semaphore,下面LZ介绍CyclicBarrier.在JDK API中是这么 ...
- 【Java并发编程实战】-----“J.U.C”:ReentrantReadWriteLock
ReentrantLock实现了标准的互斥操作,也就是说在某一时刻只有有一个线程持有锁.ReentrantLock采用这种独占的保守锁直接,在一定程度上减低了吞吐量.在这种情况下任何的"读/ ...
随机推荐
- Android笔记--Bitmap
Android | Bitmap解析 Android中Bitmap是对图像的一种抽象.通过他可以对相应的图像进行剪裁,旋转,压缩,缩放等操作.这里循序渐进的一步步了解Bitmap的相关内容. 先了解B ...
- Azure资源管理工具Azure PowerShell介绍
什么是 Azure PowerShell? Azure PowerShell 是一组模块,提供用于通过 Windows PowerShell 管理 Azure 的 cmdlet.你可以使用 cmdle ...
- 悦读FM客户端应用源码
<ignore_js_op> <ignore_js_op><ignore_js_op> 正如悦读FM所表达的[当好的文字遇上好的声音],悦读FM提供了一个很好的文章 ...
- COFF文件格式
链接器 目录 一 COFF-Common Object File Format-通用对象文件格式... 3 COFF的文件格式与结构体... 4 文件头... 5 numberOfSections(区 ...
- Python 学习日志9月18日
今天早晨学习了<Head First HTML and CSS>,第10章“div and span”. 看完并且做了练习也算是对div和span扫了个盲,需要在实践练习中加强理解与掌握. ...
- android 焦点 ListView 点击事件获取失败
1. 在ListView 中, 创建一个app_item.xml 布局文件 在布局文件中有如下的代码: <CheckBox android:id="@+id/cb_t ...
- vector的基本用法
#include<iostream> #include<vector> #include<algorithm> using namespace std; int m ...
- vue2.0的变化
1. 在每个组件模板,不在支持片段代码 组件中模板: 之前: <template> <h3>我是组件</h3><strong>我是加粗标签</st ...
- 【C语言项目】贪吃蛇游戏(下)
目录 00. 目录 07. 游戏逻辑 7.5 按下ESC键结束游戏 7.6 判断是否撞到墙 7.7 判断是否咬到自己 08. 游戏失败界面设计 8.1 游戏失败界面边框设计 8.2 撞墙失败界面 8. ...
- 多线程threadvar 变量设定
Delphi管理多线程之线程局部存储:threadvar 尽管多线程能够解决许多问题,但是同时它又给我们带来了很多的问题.其中主要的问题就是:对全局变量或句柄这样的全局资源如何访问?另外,当必须确保一 ...