并发是 golang 的优势之一,使用关键字 go 可以很方便的开启一个协程. go 语言中,常常用 go、chan、select 及 sync 库完成并发操作,处理同步、异步、阻塞、非阻塞任务.
1. 概要
go 语言的并发编程,以下是需要了解的基础知识点,也是本文主要介绍的内容. 可以对照看看这些是否已经可以熟练运用了.
阻塞: 阻塞是进程(也可以是线程、协程)的状态之一(新建、就绪、运行、阻塞、终止). 指的是当数据未准备就绪,这个进程(线程、协程)一直等待,这就是阻塞.
非阻塞: 当数据为准备就绪,该进程(线程、协程)不等待可以继续执行,这就是非阻塞.
同步: 在发起一个调用时,在没有得到结果之前,这个调用就不返回,这个调用过程一直在等待. 这是同步.
异步: 在发起调用后,就立刻返回了,这次调用过程就结束了. 等到有结果了被调用方主动通知调用者结果. 这是异步.
go(协程): 通过关键字 go 即可创建一个协程.
chan : golang 中用于并发的通道,用于协程的通信.
- 有缓冲通道
- 无缓冲通道
- 单向通道
select: golang 提供的多路复用机制.
close(): golang 的内置函数, 可以关闭一个通道.
sync: golang 标准库之一,提供了锁.
定时器: golang 标准库 time 提供的重要功能, 提供了定时器功能,可用于超时处理.
- Timer
- Ticker
2. go 并发编程
go 的并发编程采用的 CSP (Communicating Sequential Process) 模型,主要基于协程 goroutine 和通道 channel .
2.1 协程 go
在 go 语言中,并发编程使用关键字 go 即可快速启动一个并发运行的 goroutine. 如下:
go 函数名 (参数列表)
go f(a int, b int, c int){
fmt.Println(a+b+c)
}(1,2,3)
2.2 channel 通道
golang 提供了通道类型 chan,用于在并发操作时的通信,它本身就是并发安全的. 通过 chan 可以创建无缓冲、缓冲通道,满足不同需求. 写法如下:
make(chan int)
make(chan int, 10)
<- chan
chan <-
无缓冲通道: 要求接受和发送数据的 goroutine 同时准备好,否则将会阻塞.
有缓冲通道: 给予通道一个容量值,只要有值便可以接受数据,有空间便可以发送数据,可以不阻塞的完成.
单向通道: 默认情况通道是双向的,可以接受及发送数据. 也可以创建单向通道,只能收或者发数据. 如下是单向接受通道
var ch chan
<- float64
2.3 select
select: 可以监听 channel 上的输入/输出操作, 类似于 select、epoll、poll 使得通道支持多路复用. select 是专门通道 channel 设计的. 它可以结合通道实现超时处理、判断缓冲通道是否阻塞、退出信号量处理,如下:
// 1. 超时机制
select {
case <-ch:
case <-timeout:
fmt.Println("timeout 01")}// 2. 退出信号量处理
select {
case <- quitChan:
return
default:
}// 3. 判断缓冲通道是否已满
ch := make(chan int, 5)
ch <- 1
select {
case ch <- 2:
fmt.Println("channel value is", <-ch)
default:
fmt.Println("channel blocking")
}
2.4 内置函数 close()
close() 函数用于关闭通道 channel 的,close 之后的 channel 还可以读取数据,close() 函数由以下几点使用要点:
- 只能关闭双向通道或者发送通道
- 它应该由发送者使用,而不应该由接受者调用
- 当通道关闭后,接受者都不再阻塞,关闭通道后,依然可以从通道中读取值
- 所有元素读取完后,将返回通道元素的零值,并且读取检测值也是 false
示例:
ch := make(chan int, 1)
ch <- 3
close(ch) // 关闭ch
v, ok := <- ch // 3,true
v2,ok := <- ch // 0,false
3. 阻塞、同步与异步
3.1 阻塞与非阻塞
进程状态
阻塞: 阻塞是进程(也可以是线程、协程)的状态之一(新建、就绪、运行、阻塞、终止). 指的是当数据未准备就绪,这个进程(线程、协程)一直等待,这就是阻塞.
非阻塞: 当数据为准备就绪,该进程(线程、协程)不等待可以继续执行,这就是非阻塞.
3.2 同步与异步
同步: 在发起一个调用时,在没有得到结果之前,这个调用就不返回,这个调用过程一直在等待. 这是同步.
异步: 在发起调用后,就立刻返回了,这次调用过程就结束了. 等到有结果了被调用方主动通知调用者结果. 这是异步.
3.3 四种组合
同步、异步、阻塞、非阻塞可以组合成四种并发方式:
- 同步阻塞调用:得不到结果不返回,线程进入阻塞态等待。
- 同步非阻塞调用:得不到结果不返回,线程不阻塞一直在CPU运行。
- 异步阻塞调用:去到别的线程,让别的线程阻塞起来等待结果,自己不阻塞。
- 异步非阻塞调用:去到别的线程,别的线程一直在运行,直到得出结果。
4. golang 标准库的锁与定时器
4.1 锁与 sync 库
并发编程中,为了确保并发安全,可以使用锁机制. golang 提供了标准库 sync ,它实现了并发需要的各种锁. 包括:
- Mutex: 互斥锁,有俩个方法 Lock() 和 Unlock(), 它只能同时被一个 goroutine 锁定,其它锁再次尝试锁定将被阻塞,直到解锁.
- RWMutex: 读写锁,有四个方法,Lock()写锁定、Unlock()写解锁、RLock()读锁定、RUnlock()读解锁,读锁定和写锁定只能同时存在一个.
只能有一个协程处于写锁定状态,但是可以有多个协程处于读锁定状态. 即写的时候不可读,读的时候不可写. 只能同时有一个写操作确保数据一致性.
而可以多个协程同时读数据,确保读操作的并发性能.
此外在 go 的并发编程中,还会常用到 sync 的以下内容:
- sync.Map: 并发安全的字典 map
- sync.WaitGroup: 用来等待一组协程的结束,常常用来阻塞主线程.
- sync.Once: 用于控制函数只能被使用一次,
- sync.Cond: 条件同步变量. 可以通过 Wait()方法阻塞协程,通过 Signal()、Broadcast() 方法唤醒协程.
- sync.Pool: 一组临时对象的集合,是并发安全的. 它主要是用于存储分配但还未被使用的值,避免频繁的重新分配内存,减少 gc 的压力.
4.2 time 库的定时器
golang 的标准库 time 中提供了定时器功能,并提供通道 channel 变量进行定时通知. time 库中提供了两种定时器:
time.Timer: 定时器 timer 在创建指定时间后,向通道 time.Timer.C 发送数据. 之后需要使用 Reset 设定定时器时间.
time.Ticker: 周期性定时器. 会按照初设定的时间重复计时.
示例:
// timer
for {
<- timer.C
timer.Reset(time.Second) // 重设后才有效
}
// ticker
for {
<- ticker.C // 周期性有效
}
5. 结语
5.1 思考题
- golang 中 select 的多个 case 同时成立,那么选择的是哪一个?
- golang 中除了使用 sync 锁,还可以如何保证并发安全? atomic 是什么?
- sync.Map 对键的类型有什么要求么?
- 如何避免死锁? golang 中如何检测死锁?
5.2 参考资料
- Golang 并发编程 [https://www.cnblogs.com/konghui/p/10703615.html#close]
- 深入理解并发/并行,阻塞/非阻塞,同步/异步[https://cloud.tencent.com/developer/article/1339622]