8.1 并发的含义
- 并发:逻辑上具备同时处理多个任务的能力
- 并行:物理在同一时刻执行多个并发任务
我们通常会说程序是并发设计的,也就是说,它允许多个任务同时执行,但实际上并不一定真的在同一时刻发生,在单核处理器上,它们能以间隔方式切换执行,而并行则依赖多核处理器等物理设备,让多个任务真正在同一时刻执行,它代表了当前程序运行状态。简单点说,并行是并发设计的理想模式。
多线程或多进程是并行的基本条件,但单线程也可用协程做到并发。尽管协程在单个线程上通过主动切换来实现多任务并发,但它也有自己的优势。除了将因阻塞而浪费的时间找回以外,还免去了线程切换的开销,有着不错的执行效率。协程上运行的多个任务本质上依旧是串行的,加上可自主调度,所以并不需要做同步处理。
很难说哪种方式更好一些,它们有各自适用的场景。通常情况下,用多进程来实现分布式和负载平衡,减轻单进程垃圾回收压力;用多线程抢夺更多的处理器资源;用协程来提高处理器时间片利用率。
简单讲goroutine归纳为协程并不合适。运行时会创建多个线程来执行并发任务,且任务单元可被调度到其他线程并行执行。这更像是多线程和协程的综合体,能最大限度提升执行效率,发挥多核处理能力。
只需在函数调用前添加gi关键字即可创建并发任务。
go println("hello")
go func(s string) {
println(s)
}("teest")
关键字go并非执行并发操作,而是创建一个并发单元。新建任务被放置在系统队列中,等待调度器安排合适系统线程去获取执行权。当前流程不会阻塞,不会等待该任务启动,且运行时也不保证并发任务的执行次序。
每个任务单元除保存函数指针、调用参数外,还会分配执行所需的栈内存空间。相比系统默认MB级别的线程栈,gotoutine自定义栈初始仅须2KB,,所以才能创建成千上万的并发任务。自定义栈采取按需分配策略,在需要时进行扩容,最大能到GB规模
与defer一样,goroutinue也会因“延迟执行”而立即计算并复制执行参数。
var c int
func counter() int {
c++
return c
}
func main() {
a := 100
go func(x, y int) {
time.Sleep(time.Second)
println("go:", x, y)
}(a, counter()) //立即计算并复制参数
a += 100
println("main:,", a, counter())
time.Sleep(time.Second * 3)
}
结果
main:, 200 2
go: 100 1
Wait
进程退出时不会等待并发任务结束,可用通道(channel)阻塞,然后发出退出信号
func main() {
exit := make(chan struct{})
go func() {
time.Sleep(time.Second)
println("goroutinue done..")
close(exit)
}()
println("main..")
<-exit
println("main exit..")
}
除关闭通道外,写入数据也可解除阻塞。channel的更多信息。
如果要等到多个任务结束,推荐使用sync.WaitGroup.通过设定计数器,让每个gorounitune在退出前递减,直至归零时解除阻塞。
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
time.Sleep(time.Second)
println("goroutine", id, "done..")
}(i)
}
println("main..")
wg.Wait()
println("main exit..")
}
结果
main..
goroutine 9 done..
goroutine 7 done..
goroutine 1 done..
goroutine 8 done..
goroutine 5 done..
goroutine 3 done..
goroutine 6 done..
goroutine 0 done..
goroutine 2 done..
goroutine 4 done..
main exit..
尽管WaitGroup.Add 实现了原子操作,但建议在goroutine外累加计数器,以免Add尚未执行,Wait已经退出。
func main() {
var wg sync.WaitGroup
go func() {
wg.Add(1)
println("hi")
}()
wg.Wait()
println("exit..")
}
可在多处使用Wait阻塞,它们都能接收到通知。
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
wg.Wait()
println("wait exit")
}()
go func() {
time.Sleep(time.Second)
println("done.")
wg.Done()
}()
wg.Wait()
println("main exit..")
}
结果
done.
wait exit
main exit..
GOMAXPROCS
运行时可能会创建很多线程,但任何时候仅有限的几个线程参与并发任务执行。该数量默认与处理器核数相等,可用runtime.GOMAXPROCS(或环境变量修改)。
如果参数小于1,GOMAXPROCS仅返回当前设置值,不做任何修改
import (
"math"
"runtime"
"sync"
)
func count() {
x := 0
for i := 0; i < math.MaxUint32; i++ {
x += 1
}
println(x)
}
func test(n int) {
for i := 0; i < n; i++ {
count()
}
}
func test2(n int) {
var wg sync.WaitGroup
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
count()
wg.Done()
}()
}
wg.Wait()
}
func main() {
n := runtime.GOMAXPROCS(0)
//test(n)
test2(n)
}
Local Storage
与线程不同,goroutine任务无法设置优先级,无法获取编号,没有局部存储(TLS),甚至连返回值都会被抛弃。但除优先级外,其他都很容易实现。
func main() {
var wg sync.WaitGroup
var gs [5]struct {
id int
result int
}
for i := 0; i < len(gs); i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
gs[id].id = id
gs[id].result = (id + 1) * 100
}(i)
}
wg.Wait()
fmt.Printf("%#v\n", gs)
}
结果
[{id:0 result:100} {id:1 result:200} {id:2 result:300} {id:3 result:400} {id:4 result:500}]
Gosched
暂停,释放掉线程去执行其他任务。当前任务被放回队列,等待下次调度时回复执行。
func main() {
runtime.GOMAXPROCS(1)
exit := make(chan struct{})
go func() {
defer close(exit)
go func() {
println("b")
}()
for i := 0; i < 4; i++ {
println("a", i)
if i == 1 {
runtime.Gosched()
}
}
}()
<-exit
}
结果
a 0
a 1
a 2
a 3
或
a 0
a 1
b
a 2
a 3
结果不稳定
该函数很少被使用,因为运行时会主动向长时间运行的任务发出抢调度。只是当前版本实现的算法稍显粗糙,不能保证调度总能成功,所以主动切换还有适用场合。
Goexit
Goexit 立即终止当前任务,运行时确保所有已注册延迟调用被执行。该函数不会影响其他并发任务,不会引发panic,自然也就无法捕获
func main() {
exit := make(chan struct{})
go func() {
defer close(exit)
defer println("a")
func() {
defer func() {
println("b", recover() == nil)
}()
func() {
println("c")
runtime.Goexit()
println("c done")
}()
println("b done")
}()
println("a done")
}()
<-exit
println("main exit")
}
结果
c
b true
a
main exit
如果在main.main里调用Goexit,它会等待其他任务结束,然后让进程直接崩溃
func main() {
for i := 0; i < 2; i++ {
go func(x int) {
for n := 0; n < 2; n++ {
fmt.Printf("%c:%d\n", 'a'+x, n)
time.Sleep(time.Millisecond)
}
}(i)
}
runtime.Goexit()
println("main exit")
}
结果
b:0
a:0
b:1
a:1
fatal error: no goroutines (main called runtime.Goexit) - deadlock!
无论身处哪一层,Goexit都能立即终止整个调用堆栈,这与return仅退出当前函数不同。标准函数库os.Exit 可终止进程,但不会执行延迟调用。
8.2 通道
相比Erlang,Go并未严格的并发安全。允许全局变量、指针、引用类型这些非安全内存共享操作,就需要开发人员自行维护数据一致性和完整性。Go鼓励使用CSP通道,以通信代替内存共享,实现并发安全。
通过消息来避免竞态的模型除了CSP,还有Actor。但两者有较大区别。
作为CSP核心,通道(channel)是显示的,要求操作双方必须知道数据类型和具体通道,并不关心另一端操作者身份和数量。可如果另一端未准备妥当,或消息未能及时处理,会阻塞当前端。
相比起来,Actor是透明额。它不在乎数据类型及通道,只要知道接收者信箱即可。默认就是异步方式,发送方对消息是否被接收和处理并不关心。
从底层实现上来说,通道只是一个队列。同步模式下,发送和接收双方配对,然后直接复制数据给对方。如果配对失败,则置入等待队列,直到另一方出现才被唤醒。异步模式抢夺的则是数据缓冲槽。发送方要求有空槽可写入,而接收方则要求有缓冲数据可读。需求不符时,同样加入等待队列,直到有另一方写入数据或腾出空槽后被唤醒。
除传递消息外,通道还被用作事件通知。
func main() {
done := make(chan struct{})
c := make(chan string)
go func() {
s := <-c
println(s)
close(done)
}()
c <- "hi"
<-done
}
同步模式必须有配对操作的goroutine出现,否则会一直堵塞。而异步模式才缓冲区未满或数据未读完前,不会阻塞。
func main() {
c := make(chan int,3) // 创建3个缓冲槽的异步通道
c <- 1
c <- 2
println(<-c)
println(<-c)
}
多数时候,异步通道有助于提升性能,减少排队阻塞。
缓冲区大小仅是内部属性,不属于类型的组成部分。另外通道本身就是指针,可用相等操作符判断是否为同一对象或nil
func main() {
var a, b chan int = make(chan int, 3), make(chan int, 2)
var c chan bool
println(a == b)
println(c == nil)
fmt.Printf("%p,%d\n", a, unsafe.Sizeof(a))
}
result:
false
true
0xc00007e080,8
虽然可传递指针来避免数据复制,但须额外注意数据并发安全。
内置函数cap和len返回缓冲区大小和当前已缓冲数量;而对于同步通道则都返回0,据此可判断通道是异步还是同步。
func main() {
a, b := make(chan int), make(chan int, 3)
b <- 1
b <- 2
println("a:", len(a), cap(a)) //a: 0 0
println("b:", len(b), cap(b)) //b: 2 3
}
收发
除使用简单的发送和接收操作符外,还可使用ok-idom或range模式处理数据
func main() {
done := make(chan struct{})
c := make(chan int)
go func() {
defer close(done)
for {
x, ok := <-c
if !ok {
return
}
println(x)
}
}()
for i := 0; i < 100; i++ {
c <- i
}
close(c)
<-done
}
对于循环接收数据,range模式更简洁一些;
func main() {
done := make(chan struct{})
c := make(chan int)
go func() {
defer close(done)
for x := range c {
println(x)
}
}()
for i := 0; i < 100; i++ {
c <- i
}
close(c)
<-done
}
及时用close函数关闭通道引发结束通知,否则可能会导致死锁。
通知可以是群体性的。也未必就是通知结束,可以是任何需要表达的事件。
func main() {
var wg sync.WaitGroup
ready := make(chan struct{})
wg.Add(3)
for i := 0; i < 3; i++ {
go func(id int) {
defer wg.Done()
println(id, ":ready.")
<-ready
println(id, "running...")
}(i)
}
time.Sleep(time.Second)
println("Ready?Go!")
close(ready)
wg.Wait()
}
result:
0 :ready.
2 :ready.
1 :ready.
Ready?Go!
1 running...
0 running...
2 running...
一次性事件用close效率更好,可传递不同数据标志实现。还可使用sync.Cond实现单播或广播事件。
对于closed或nil通道,发送和接收操作都有相应规则:
- 向已关闭通道发送数据,引发panic。
- 从已关闭接收数据,返回已缓冲数据或零值。
- 无论收发,nil通道都会阻塞。
func main() {
c := make(chan int, 3)
c <- 10
c <- 20
close(c)
// for i := 0; i < cap(c)+1; i++ {
// x, ok := <-c
// println(i, ":", ok, x)
// }
for x := range c {
println(x)
}
}
操作得对异步通道进行,同步通道关闭后就不能读取数据了
单向
通道默认是单向的,并不区分发送和接收端。但某些时候,我们可可以限制收发的方向来获得更严谨的逻辑操作。
尽管可使用make创建单向通道,但那没有任何意义。通常使用类型转换来获取单向通道,并分别赋值给操作双方。
func main() {
var wg sync.WaitGroup
wg.Add(2)
c := make(chan int)
var send chan<- int = c
var recv <-chan int = c
go func() {
defer wg.Done()
for x := range recv {
println(x)
}
}()
go func() {
defer wg.Done()
defer close(c)
for i := 0; i < 100; i++ {
send <- i
}
time.Sleep(time.Second)
}()
wg.Wait()
}
不能在单向通道上做逆向操作
func main() {
c := make(chan int)
var send chan<- int = c
var recv <-chan int = c
<-send //<-send (receive from send-only type chan<- int)
recv <- 1 //recv <- 1 (send to receive-only type <-chan int)
}
同样,close不能用于接收端
func main() {
c := make(chan int)
var send chan<- int = c
var recv <-chan int = c
close(recv) // close(recv) (cannot close receive-only channel)
}
无法将单向通道重新转换回去
func main() {
var a, b chan int
a = make(chan int, 2)
var recv <-chan int = a
var send chan<- int = a
b = (chan int)(recv) //cannot convert recv (type <-chan int) to type chan int
b = (chan int)(send) // cannot convert send (type chan<- int) to type chan int
}
选择
如果要同时处理多个通道,可选用select语。它会随机选择一个可用通道做收发操作。
func main() {
var wg sync.WaitGroup
wg.Add(2)
a, b := make(chan int), make(chan int)
go func() {
defer wg.Done()
time.Sleep(time.Second)
for {
var (
name string
x int
ok bool
)
select { // 随机接收
case x, ok = <-a:
name = "a"
case x, ok = <-b:
name = "b"
}
if !ok { // 没有数据时关闭
return
}
println(name, x)
}
}()
go func() {
defer wg.Done()
defer close(a)
defer close(b)
for i := 0; i < 10; i++ {
select { // 随机发送
case a <- i:
case b <- i * 10:
}
}
}()
wg.Wait()
}
结果:
a 0
b 10
a 2
b 30
b 40
a 5
b 60
a 7
b 80
b 90
如要等到全部通道消息处理结束(closed),可将已完成通道设置为nil。这样它就会被阻塞,不再被select选中。
func main() {
var wg sync.WaitGroup
wg.Add(3)
a, b := make(chan int), make(chan int)
go func() {
defer wg.Done()
time.Sleep(time.Second)
for {
select {
case x, ok := <-a:
if !ok {
a = nil
break
}
println("a",x)
case x, ok := <-b:
if !ok {
b = nil
break
}
println("b",x)
}
if a==nil && b == nil{
return
}
}
}()
go func() {
defer wg.Done()
defer close(a)
for i := 0; i < 10; i++ {
a <- i
}
}()
go func() {
defer wg.Done()
defer close(b)
for i := 0; i < 3; i++ {
b <- i * 10
}
}()
wg.Wait()
结果
b 0
b 10
a 0
b 20
a 1
a 2
a 3
a 4
a 5
a 6
a 7
a 8
a 9
即便是同一通道,也会随机选择case执行。
func main() {
var wg sync.WaitGroup
wg.Add(2)
a := make(chan int)
go func() {
defer wg.Done()
for {
var x int
var ok bool
select {
case x, ok = <-a:
println("a1", x*10)
case x, ok = <-a:
println("a2", x)
}
if !ok {
return
}
}
}()
go func() {
defer wg.Done()
defer close(a)
for i := 0; i < 10; i++ {
select {
case a <- i:
case a <- i * 10:
}
}
}()
wg.Wait()
}
结果
a2 0
a1 10
a2 20
a2 30
a1 400
a1 500
a2 60
a1 700
a2 8
a2 90
a1 0
当所有通道都不可用时,select会执行default语句。如此可避开select阻塞,但须注意处理外层循环,以免陷入空耗。
func main() {
done := make(chan struct{})
c := make(chan int)
go func() {
defer close(done)
for {
select {
case x, ok := <-c:
if !ok { // 当通道可选前不会执行
return
}
println("data:", x)
default:
println("default")
}
fmt.Println(time.Now())
time.Sleep(time.Second)
}
}()
time.Sleep(time.Second * 3)
c <- 100
close(c)
<-done
}
result
default
2019-03-08 11:08:11.6008349 +0800 CST m=+0.002444801
default
2019-03-08 11:08:12.6178948 +0800 CST m=+1.019504701
default
2019-03-08 11:08:13.6185616 +0800 CST m=+2.020171501
data: 100
2019-03-08 11:08:14.6190543 +0800 CST m=+3.020664201
也可用default处理一些默认逻辑
func main() {
done := make(chan struct{})
data := []chan int{
make(chan int, 3),
}
go func() {
defer close(done)
for i := 0; i < 10; i++ {
select {
case data[len(data)-1] <- i:
default:
data = append(data, make(chan int, 3)) // 通道满时新增通道
}
}
}()
<-done
for i := 0; i < len(data); i++ { // 显示数据
c := data[i]
close(c)
for x := range c {
println(x)
}
}
}
模式
通常使用工厂方法将goroutine通道绑定。
type receiver struct {
sync.WaitGroup
data chan int
}
func newReceiver() *receiver {
r := &receiver{
data: make(chan int),
}
r.Add(1)
go func() {
defer r.Done()
for x := range r.data {
println("recv:", x)
}
}()
return r
}
func main() {
r := newReceiver()
r.data <- 1
r.data <- 2
close(r.data)
r.Wait()
}
结果
recv: 1
recv: 2
鉴于通道本身就是一个并发安全的队列,可用作 ID generator 、Pool等用途。
type pool chan []byte
func newPool(cap int) pool {
return make(chan []byte, cap)
}
func (p pool) get() []byte {
var v []byte
select {
case v = <-p:
default:
v = make([]byte, 10)
}
return v
}
func (p pool) put(b []byte) error {
select {
case p <- b:
return nil
default:
return errors.New("faild to putback")
}
}
用通道实现信号量
func main() {
runtime.GOMAXPROCS(4)
var wg sync.WaitGroup
sem := make(chan struct{}, 2)
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
sem <- struct{}{} // 满了之后阻塞,等到前面处理完后后面才能接着处理
defer func() {<-sem}()
time.Sleep(time.Second * 2)
fmt.Println(id, time.Now())
}(i)
}
wg.Wait()
}
标准库time提供了timeout和tick channel 实现
func main() {
go func() {
for {
select {
case <-time.After(time.Second * 5): // 5秒后发送消息
fmt.Println("time out...")
os.Exit(0)
}
}
}()
go func(){
tick := time.Tick(time.Second) // 每秒发送一个消息
for {
select{
case <- tick:
fmt.Println(time.Now())
}
}
}()
<-(chan struct{})(nil) //阻塞
}
结果
2019-03-08 14:39:54.4357387 +0800 CST m=+1.017798501
2019-03-08 14:39:55.4265249 +0800 CST m=+2.008584701
2019-03-08 14:39:56.4337129 +0800 CST m=+3.015772701
2019-03-08 14:39:57.4317151 +0800 CST m=+4.013775001
time out...
捕获INT、TERM信号,顺便实现一个简易的atexit函数
var exits = &struct {
sync.RWMutex
funcs []func()
signals chan os.Signal
}{}
func atexit(f func()) {
exits.Lock()
defer exits.Unlock()
exits.funcs = append(exits.funcs, f)
}
func waitExit() {
if exits.signals == nil {
exits.signals = make(chan os.Signal)
signal.Notify(exits.signals, syscall.SIGINT, syscall.SIGTERM)
}
exits.RLock()
for _,f := range exits.funcs{
defer f()
}
exits.RUnlock()
<-exits.signals
}
func main() {
atexit(func(){println("exit1...")})
atexit(func(){println("exit2...")})
waitExit()
}
性能
将发往通道的数据打包,减少传输次数,可有效提升性能。从实现上来说,通道队列依旧使用锁同步机制,单次获得更多数据,可改善因频繁加锁造成的性能问题。
const (
max = 50000000
block = 500
bufsize = 100
)
func test() {
done := make(chan struct{})
c := make(chan int, bufsize)
go func() {
count := 0
for x := range c {
count += x
}
close(done)
}()
for i := 0; i < max; i++ {
c <- i
}
close(c)
<-done
}
func testBlock() {
done := make(chan struct{})
c := make(chan [block]int, bufsize)
go func() {
count := 0
for a := range c {
for _, x := range a {
count += x
}
}
close(done)
}()
for i := 0; i < max; i++ {
var b [block]int
for n := 0; n < block; n++ {
b[n] = i + n
if i+n == max-1 {
break
}
}
c <- b
}
close(c)
<-done
}
虽然单次消耗更多内存,但性能提升非常明显,如将数组改成切片会造成更多内存分配次数。
资源泄露
通道可能会引发goroutine leak,确切的说,是指goroutine处于 发送或接收阻塞状态,但一直未被唤醒。垃圾回收器并不收集此类资源,导致他们会在等待队列里长久休眠,形成资源泄露。
func test() {
c := make(chan int)
for i := 0; i < 10; i++ {
go func() {
<-c
}()
}
}
func main() {
test()
for {
time.Sleep(time.Second)
runtime.GC()
}
}
8.3 同步
通道并非用来取代锁的,它们有各自不同的使用场景。通道倾向解决逻辑层次的并发处理架构,而锁则用来保护局部范围内的数据安全。
标准库sync提供了互斥和读写锁,另外有原子操作等,可基本满足日常开发需求。Mutex,RWMutex的使用并不复杂,只有几个地方要注意。
将Mutex作为匿名字段,相关方法必须实现为pointer-receiver,否则会因复制导致锁机制失效。
type data struct {
sync.Mutex
}
func (d *data) test(s string) { // 如果这里是(d data)会因为d不同而失效,因为两次都是复制的d
d.Lock()
defer d.Unlock()
for i := 0; i < 5; i++ {
println(s, i)
time.Sleep(time.Second)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
var d data
go func() {
defer wg.Done()
d.test("read")
}()
go func() {
defer wg.Done()
d.test("write")
}()
wg.Wait()
}
应将Mutex锁粒度控制在最小范围内,及早释放。
Mutex不支持递归锁,即便在同一goroutinue下也会导致死锁。
var m sync.Mutex
func main() {
m.Lock()
{
m.Lock()
m.Unlock()
}
m.Unlock()
}
fatal error: all goroutines are asleep - deadlock!
在涉及并发安全类型时,千万注意此类问题。
type cache struct {
sync.Mutex
data []int
}
func (c *cache) count() int {
c.Lock()
n := len(c.data)
c.Unlock()
return n
}
func (c *cache) get() int {
c.Lock()
defer c.Unlock()
var d int
if n := c.count(); n > 0 { //count 重复锁定,导致死锁
d = c.data[0]
c.data = c.data[1:]
}
return d
}
func main() {
c := cache{
data: []int{1, 2, 3, 4},
}
println(c.get())
}
相关建议:
- 对性能要求较高时,应避免使用defer Unlock
- 读写并发时,用RWMutex性能会更好一些。
- 对单个数据进行读写保护时,可尝试用原子操作
- 执行严格测试,尽可能打开数据竞争检查