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.不要重复锁定互斥锁工具
对一个已经被锁定的互斥锁进行锁定,会当即阻塞当前的goroutineui
当Go语言运行时系统发现全部的用户级goroutine都处于等待状态(死锁),就会自行抛出一个带有以下信息的panic:spa
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方法则分别用于对读锁进行锁定和解锁
另外,对于同一个读写锁来讲有以下规则:
条件变量并非被用来保护临界区和共享资源的,它是用于协调想要访问共享资源的那些线程的。当共享资源的状态发生变化时,它能够被用来通知被互斥锁阻塞的线程。
条件变量提供三个方法:等待通知(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 == 1 { sendCond.Wait() //若是有情报,就等待 } mailbox = 1 //放入情报 lock.Unlock() //写完 recvCond.Signal()
获取情报
lock.RLock() // 读操做 for mailbox == 0 { recvCond.Wait() //没有情报 } mailbox = 0 //取走情报 lock.RUnlock() //读完 sendCond.Signal()
一、把调用它的goroutine(当前的goroutine)加入到当前条件变量的通知队列中
二、解锁当前条件变量基于的那个互斥锁
三、让当前的goroutine处于等待状态,等到通知到来时再决定是否唤醒它,这时这个goroutine就会阻塞在调用这个Wait方法的那行代码上
四、若是通知到来而且决定唤醒这个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方法都是用来发送通知的,不一样的是,前者的通知只会唤醒一个所以而等待的goroutine,然后者的通知却会唤醒全部为此等待的goroutine。
条件变量的Wait方法总会把当前的goroutine添加到队列的队尾,而它的Signal方法总会从通知队列的队首开始查找可被唤醒的goroutine,因此,因Signal方法的通知而被唤醒的goroutine通常都是最先等待的那个。
条件变量的Signal方法和Broadcast方法不须要在互斥锁保护下执行。
条件变量的通知有即时性。即若是发生通知的时候没有goroutine为此等待,那么该通知就会被遗弃