【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 == 1 {
 sendCond.Wait()   //如果有情报,就等待
}
mailbox = 1   //放入情报
lock.Unlock()  //写完
recvCond.Signal()

  获取情报

lock.RLock()  // 读操作
for mailbox == 0 {
 recvCond.Wait()  //没有情报
}
mailbox = 0  //取走情报
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为此等待,那么该通知就会被遗弃

原文地址:https://www.cnblogs.com/yuxiaoba/p/9805099.html