select
-
select 的作用:
通过 select 可以监听 channel 上的数据流动。
-
select 的用法:
与 switch case 语法类似。但是 case 后面必须是 IO 操作,不可以人一些判断表达式。
select { case <-chan1: // 如果 chan1 成功读到数据,则进行该 case 处理语句 case chan2 <- 1: //如果成功向 chan2 写入数据,则进行该 case 处理语句 default: // 如果上面都没有成功,则进入 default 处理流程 }
-
注意:
- 监听的 case 中,没有满足监听条件,阻塞。
- 监听的 case 中,有多个满足监听条件,任选一个执行。
- 可以使用 default 来处理所有case 都不满足监听条件的状况。通常不用(会产生忙轮询)。
- select 自身不带有循环机制,需借助外层 for 来循环监听。
- break 跳出 select 中的 case 选项。类似于 switch 中的用法。
超时处理
有时候会出现 goroutine 阻塞的情况,可以使用 select 来设置超时,通过如下的方式实现:
func main() {
c := make(chan int)
o := make(chan bool)
go func() {
for {
select {
case v := <-c:
fmt.Println(v)
case <time.After(5 * time.Second):
fmt.Println("timeout"):
o <- true
break
}
}
}()
// c <-66 // 注释掉,引发 timeout
<-o
}
锁和条件变量
什么是锁
就是某个协程(线程)在访问某个资源时先锁住,防止其他协程的访问,等访问完毕解锁后其他协程再来加锁进行访问。
死锁
死锁不是锁的一种,是一种错误使用锁导致的现象。
死锁是指两个或两个以上的进程在进行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,他们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
- 常见的场景:
- 单 go 程自己死锁
package main
import "fmt"
func main() {
ch := make(chan int)
ch <- 1
fmt.Println("send")
go func() {
<- ch
fmt.Println("received")
}()
fmt.Println("over")
}
互斥锁
每个资源都对应一个可称为 “互斥锁” 的标记,这个标记用来保证在任意时刻,只能有一个协程(线程)访问该资源。其它的协程只能等待。
互斥锁是传统并发编程对共享资源进行访问控制的主要手段。它由标准库 sync
中的 Mutex
结构体类型表示。 sync.Mutex
类型只有两个公开的指针方法,Lock 和 Unlock。Lock 锁定当前的共享资源,Unlock 进行解锁。
在使用互斥锁时,一定要注意:对资源操作完成后,一定要解锁,否则会出现流程执行异常、死锁等问题。通常借助 defer
。锁定后,立即使用 defer
语句保证互斥锁及时解锁。如下所示:
var mutex sync.Mutex
func write() {
mutex.Lock()
defer mutex.Unlock()
}
读写锁
互斥锁的本质是当一个 goroutine 访问的时候,其他 goroutine 都不能访问。这样在资源同步,避免竞争的同时也降低了程序的并发性能。程序由原来的并行执行变成了串行执行。
读写锁可以让多个读操作并发,同时读取,但是对于写操作是完全互斥的。也就是说,当一个 goroutine 进行写操作的时候,其他 goroutine 既不能进行读操作,也不能进行写操作。
读时共享,写时独占。写锁优先级比读锁高。
-
“写锁定”和“读锁定”
func (*RWMutex) Lock() func (*RWMutex) Unlock()
-
“读锁定”和“写锁定”
func (*RWMutex) RLock() func (*RWMutex) RUnlock()
条件变量
条件变量的作用并不能保证在同一时刻仅有一个协程(线程)访问某个共享的数据资源,而是在对应的共享数据的状态发生变化时,通知阻塞在某个条件上的协程(线程)。条件变量不是锁,在并发中不能达到同步的目的,因此条件变量总是与锁一块使用。
Go 标准库中的 sys.Cond
类型代表了条件变量。条件变量要与锁(互斥锁或者读写锁)一起使用。成员变量 L
代表与条件变量搭配使用的锁。
type Cond struct {
noCopy noCopy
L Locker
notify notifyList
checker copyChecker
}
- 对应的 3 个常用方法,Wait、Signal、Broadcast:
-
func (c *Cond) Wait()
该函数的作用:- 阻塞等待条件变量满足
- 释放已掌握的互斥锁,相当于
cond.Unlock()
。注意:两步为原子操作(不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束)。 - 当被唤醒,
Wait()
函数返回时,解除阻塞并重新获取互斥锁。相当于cond.Lock()
-
func (c *Cond) Signal()
单发通知,给一个正等待(阻塞)在该条件变量上的 goroutine(线程)发送通知。 -
func (c *Cond) Broadcast()
广播通知,给正在等待(阻塞)在该条件变量上的所有 goroutine(线程)发送通知。
-
- 使用流程:
- 创建条件变量:
var cond sync.Cond
- 指定条件变量用的锁:
cond.L = new(sync.Mutex)
-
cond.Lock()
给公共区加锁(互斥量) - 判断是否达到阻塞条件(缓冲区满/空) --- for 循环判断
- 访问公共区 ---读、写数据、打印
- 解锁条件变量用的锁
cond.L.Unlock()
- 唤醒阻塞在条件变量上的对端。
signal() Broadcast()
- 创建条件变量: