Go中协程基础小记
协程基础
在Go中,线程被程为goroutine,也有人称其为线程的线程(a thread of thread)。每个线程都有自己的单独的变量如:
PC程序计数器、函数调用栈Stack、一套寄存器Registers。
在Go中协程对应的primitive原语有:
- start/go:启动/运行一个线程
- exit:线程退出,一般从某个函数退出/结束执行后,会自动隐式退出
- stop:停止一个线程,比如向一个没有读者的channel写数据,那么channel阻塞,go可能会运行时暂时停止这个线程
- resume:恢复原本停止的线程重新执行,需要恢复程序计数器(program counter)、栈指针(stack pointer)、寄存器(register)状态,让处理器继续运行该线程。
为什么需要多协程
依靠多协程达到并发的效果:
- I/O concurrency:I/O并发。
- multi-core parallelism:多核并行,提高整体吞吐量。充分利用CPU。
- convinience:方便,经常有需要异步执行or定时执行的任务,可以通过线程完成。
多协程面临的问题
- race conditions:多线程会引入竞态条件的场景
- avoid sharing:避免共享内存以防止竞态条件场景的产生(Go有一个竞态检测器race detector,能够辅助识别代码中的一些竞态条件场景)
- use locks:让一系列指令变成原子操作
- coordination:同步协调问题,比如一个线程的执行依赖另一个线程的执行结果等
- channels:通道允许同时通信和协调
- condition variables:配合互斥锁使用
- deadlock:死锁问题,比如在go中简单的死锁场景,一个写线程往channel写数据,但是永远没有读线程从channel读数据,那么写线程被永久阻塞,即死锁,go会抓住这种场景,抛出运行时错误runtime error。
go中还存在一个协程泄漏的问题:参考下面代码示例第四个:channel的使用。
代码演示
存在共享变量的资源竞争
// 定义一个工具类,不具有实际意义。
package main
import "math/rand"
func requestVote() bool {
intn := rand.Intn(10)
if intn <= 5 {
return true
}
return false
}
// 基础代码
package main
func main() {
count := 0
finished := 0
for i := 0; i < 10; i++ {
// 匿名函数,创建共 10 个线程
go func() {
vote := requestVote() // 一个内部sleep随机时间,最后返回true的函数,模拟投票
if vote {
count++
}
finished++
}()
}
for count < 5 && finished != 10 {
// wait
}
if count >= 5 {
println("received 5+ votes!")
} else {
println("lost")
}
}
使用go自带的资源竞争分析工具:
go run -race xxx.go
执行结果如下:
==================
WARNING: DATA RACE
Write at 0x00c00001c118 by goroutine 9:
main.main.func1()
/home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:20 +0x77
Previous read at 0x00c00001c118 by main goroutine:
main.main()
/home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:25 +0x147
Goroutine 9 (running) created at:
main.main()
/home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:17 +0x71
==================
==================
WARNING: DATA RACE
Write at 0x00c00001c128 by goroutine 9:
main.main.func1()
/home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:22 +0xa4
Previous read at 0x00c00001c128 by main goroutine:
main.main()
/home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:25 +0x164
Goroutine 9 (running) created at:
main.main()
/home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:17 +0x71
==================
received 5+ votes!
==================
WARNING: DATA RACE
Read at 0x00c00001c128 by goroutine 12:
main.main.func1()
/home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:22 +0x91
Previous write at 0x00c00001c128 by goroutine 11:
main.main.func1()
/home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:22 +0xa4
Goroutine 12 (running) created at:
main.main()
/home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:17 +0x71
Goroutine 11 (finished) created at:
main.main()
/home/wt/Backend/go/goprojects/src/golearndetail/concurrency/learn01/voteselect.go:17 +0x71
==================
Found 3 data race(s)
exit status 66
通过上述代码运行结果,可以得知存在多个协程的情况下对共享变量count、finished存在资源竞争。
Mutex解决资源竞争
代码示例:
package main
import (
"sync"
)
func main() {
count := 0
finished := 0
var mu sync.Mutex
for i := 0; i < 10; i++ {
// 匿名函数,创建共 10 个线程
go func() {
vote := requestVote() // 一个内部sleep随机时间,最后返回true的函数,模拟投票
// 临界区加锁
mu.Lock()
// 推迟到基本block结束后执行,这里即函数执行结束后 自动执行解锁操作。利用defer语言,一般在声明加锁后,立即defer声明推迟解锁
defer mu.Unlock()
if vote {
count++
}
finished++
}()
}
for {
mu.Lock()
if count > 5 || finished == 10 {
// 不能在此处解锁,下面仍然需要对count变量进行判断
//mu.Unlock()
break
}
mu.Unlock()
// wait
}
if count >= 5 {
println("received 5+ votes!")
} else {
println("lost")
}
mu.Unlock()
}
运行结果:
wt@wt:~/Backend/go/goprojects/src/golearndetail/concurrency/learn01$ go run -race voteselect_mutex.go utils.go
received 5+ votes!
使用Mutex后,及时加锁以及解锁,不存在资源竞争。
Mutex+Cond解决资源竞争和CPU空转
代码示例:
package main
import "sync"
func main() {
count := 0
finished := 0
var mu sync.Mutex
//cond := sync.NewCond(&mu)
cond := sync.Cond{L: &mu}
for i := 0; i < 10; i++ {
// 匿名函数,创建共 10 个线程
go func() {
vote := requestVote() // 一个内部sleep随机时间,最后返回true的函数,模拟投票
// 临界区加锁
mu.Lock()
// 推迟到基本block结束后执行,这里即函数执行结束后 自动执行解锁操作。利用defer语言,一般在声明加锁后,立即defer声明推迟解锁
defer mu.Unlock()
if vote {
count++
}
finished++
cond.Broadcast()
}()
}
mu.Lock()
for count < 5 || finished != 10 {
// 如果条件不满足,则在制定的条件变量上wait。内部原子地进入sleep状态,并释放与条件变量关联的锁。当条件变量得到满足时,这里内部重新获取到条件变量关联的锁,函数返回。
cond.Wait()
// 使用cond.Wait的目的防止CPU空转,使用time.sleep()无法控制合适的休眠时间
}
if count >= 5 {
println("received 5+ votes!")
} else {
println("lost")
}
mu.Unlock()
}
执行结果:
wt@wt:~/Backend/go/goprojects/src/golearndetail/concurrency/learn01$ go run -race voteselect_mutex_cond.go utils.go
received 5+ votes!
chan解决资源竞争
代码示例:
package main
func main() {
count := 0
finished := 0
ch := make(chan bool)
for i := 0; i < 10; i++ {
// 匿名函数,创建共 10 个线程
go func() {
ch <- requestVote()
}()
}
// 这里实现并不完美,如果count >= 5了,主线程不会再监听channel,导致其他还在运行的子线程会阻塞在往channel写数据的步骤。
// 但是这里主线程退出后子线程们也会被销毁,影响不大。但如果是在一个长期运行的大型工程中,这里就存在泄露线程leaking threads
for count < 5 && finished != 10 {
// 主线程在这里等待
v := <-ch
if v {
count++
}
finished++
}
if count >= 5 {
println("received 5+ votes!")
} else {
println("lost")
}
}
上述代码中涉及到一个线程泄漏的问题:
在Go语言中,"泄露线程leaking threads"通常指的是goroutine泄漏。Goroutine是Go语言中实现并发的轻量级线程,它们应该是临时的和短暂的。然而,如果一个goroutine因为某些原因无法正常结束,它就会一直运行,从而导致资源无法释放,这就是所谓的goroutine泄漏。
上面代码如果长期运行,由于非缓冲通道没有数据接收者,会导致部分goroutine一直阻塞在向其写数据的操作中,造成线程泄漏。
对于非缓冲通道的理解可以参考我的另一篇博文:
https://blog.csdn.net/weixin_45863010/article/details/142618550?spm=1001.2014.3001.5501
执行结果:
wt@wt:~/Backend/go/goprojects/src/golearndetail/concurrency/learn01$ go run -race voteselect_channel.go utils.go
received 5+ votes!