Go 并发编程实战:从 Goroutine 到 Channel 与锁
Go 协程(Goroutine)
什么是协程(Goroutine)
Goroutine 是 Go 语言提供的轻量级并发执行单位。
简单理解:
Goroutine 就是一条由 Go 运行时管理的“轻量线程”。它可以让多个任务同时执行。
-
golang 中的主线程:(可以理解为线程/也可以理解为进程),在一个 Golang 程序的主线程上可以起多个协程。Golang 中多协程可以实现并行或者并发。
-
Golang 中每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少。OS 线程(操作系统线程)一般都有固定的栈内存(通常为 2MB 左右),一个 goroutine (协程) 占用内存非常小,只有 2KB 左右,多协程goroutine 切换调度开销方面远比线程要少。
-
由 Go 调度,不由操作系统直接调度
商场 (计算机)
|
├── 店铺A (进程A)
│ ├── 正式员工1 (线程1) -- 由【商场总管理处】(内核态)调度
│ ├── 正式员工2 (线程2) -- 由【商场总管理处】(内核态)调度
│ └── 一群实习生 (协程) -- 由【店长】(用户态调度器)管理,跑在几个正式员工身上
|
├── 店铺B (进程B)
│ └── ..
Goroutine 的使用以及 sync.WaitGroup
并行执行需求:
在主线程(可以理解成进程)中,开启一个 goroutine, 该协程每隔 50 毫秒秒输出 “你好 golang” ,在主线程中也每隔 50 毫秒输出"你好 golang", 输出 10 次后,退出程序,要求主线程和goroutine 同时执行。
package main
import (
"fmt"
"time"
)
func test() {
for i := 1; i <= 4; i++ {
fmt.Println("test () hello " i)
time.Sleep(time.Millisecond*50)
}
}
func main() {
go test()
for i := 0; i < 4; i++ {
fmt.Println("main() hello", i)
time.Sleep(time.Millisecond * 50)
}
fmt.Println("main结束")
}
main() hello 0
test() hello world 0
test() hello world 1
main() hello 1
main() hello 2
test() hello world 2
test() hello world 3
main() hello 3
main() hello 4
test() hello world 4
main结束
有时候意主线程执行完毕后即使协程没有执行完毕,程序会退出,所以我们需要对上面代码进行改造。
sync.WaitGroup 可以实现主线程等待协程执行完毕
package main
import (
"fmt"
"time"
"sync"
)
var wg sync.WaitGroup //1、定义全局的 WaitGroup
func test() {
for i := 0; i < 4; i++ {
fmt.Println("test() hello world", i)
time.Sleep(time.Millisecond * 50)
}
wg.Done() // 4、goroutine 结束就登记-1
}
func main() {
wg.Add(1) //2、启动一个 goroutine 就登记+1
go test()
for i := 0; i < 2; i++ {
fmt.Println("main() hello", i)
time.Sleep(time.Millisecond)
}
wg.Wait() // 3、等待所有登记的 goroutine 都结束
fmt.Println("main结束")
}
输出
main() hello 0
test() hello world 0
main() hello 1
test() hello world 1
test() hello world 2
test() hello world 3
main结束
wg.Done() 是 Go 并发中的 WaitGroup 计数减少操作。
为什么 Done() 一般放 defer?
- 原因:防止中途 return 或 panic 导致忘记 Done。
推荐写法:
func work(){
defer wg.Done()
fmt.Println("工作中")
}
| 方法 | 作用 |
|---|---|
Add(数量) |
增加等待任务数 |
Done() |
任务完成,数量-1 |
Wait() |
阻塞等待数量变0 |
启动多个 Goroutine
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup //定义全局的 WaitGroup
func test() {
defer wg.Done() // goroutine 结束就登记-1
fmt.Println("hello", i)
}
func main() {
for i := 0; i < 5; i++ {
wg.Add(1)// 启动一个 goroutine 就登记+1
go test(i)
}
wg.Wait() // 等待所有登记的 goroutine 都结束
fmt.Println("main结束")
}
hello 4//多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为 10 //个 goroutine是并发执行的,而 goroutine 的调度是随机的。
hello 3
hello 1
hello 2
hello 0
main结束
参数传递
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Println(n)
}(i)//把当前 i 的值复制给 n。
}
wg.Wait()
}//顺序不一定是 0 1 2 3 4。因为 goroutine 调度由 Go 运行时决定
错误代码,不传参
匿名函数没有复制外部变量,而是保存了对外部变量的引用(地址)
所有 goroutine 使用的是同一个变量:i
循环结束:i = 5
然后 goroutine 才执行
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i)//Go 闭包捕获的是变量,不是变量当时的值。
}()
}
time.Sleep(time.Second)//可能输出 5 5 5 5 5
用协程创建一个形参为空,返回值为空的函数
go func(){
defer fmt.Println("A.defer")
func(){
defer fmt.Println("B.defer")
runtime.Goexit()
//整个 goroutine 要退出。Go 会执行:当前 goroutine 中所有 defer
}()
fmt.Println("A")
}()
B.defer
A.defer
runtime.Goexit() 是什么?
官方定义 Goexit terminates the goroutine that calls it.
意思:终止当前 goroutine。
不是退出函数。是直接杀掉整个协程。
| return | runtime.Goexit | |
|---|---|---|
| 退出范围 | 当前函数 | 整个goroutine |
| 执行defer | 执行 | 执行 |
| 后续代码 | 当前函数后面不执行 | 整个goroutine都不执行 |
| 能退出协程 | ❌ | ✅ |
go channel
1.什么是 Channel?
Channel(通道)是 Go 用来实现 goroutine 之间通信 的机制。
Golang 的并发模型是 CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。
channel是一种类型,一种引用类型
var 变量名称 chan 元素类型
//未初始化的通道类型变量其默认零值是nil
var ch1 chan int
var ch2 chan bool// 声明一个传递布尔型的管道
var ch3 chan []int
2.创建channel
make(chan 数据类型)// 初始化
ch:=make(chan int)// 只能传 ch<-10,不能传ch<-"hello"
- 无缓冲channel
特点:发送必须等待接收。
ch := make(chan int)
ch <- 10
fmt.Println("结束")//程序卡住,无缓冲 channel 没有存储空间
- 缓冲channel
ch:=make(chan int,3)
ch<-10
ch<-3
ch<-5 //不会阻塞
//如果
ch<-4 //会阻塞
缓冲channel读取
package main
import "fmt"
func main(){
ch := make(chan int,3)
ch <- 10
ch <- 20
ch <- 30
fmt.Println(<-ch) // 10
fmt.Println(<-ch) // 20
fmt.Println(<-ch) // 30
}
channel 发送,接收与关闭
ch:=make(chan int)
ch <- 15
value := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值,忽略结果
close(ch) //不再发送数据
关闭后的通道有以下特点:
- 对一个关闭的通道再发送值就会导致 panic。
- 对一个关闭的通道进行接收会一直获取值直到通道为空。
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
- 关闭一个已经关闭的通道会导致 panic。
多返回值模式
value,ok:=<-ch
// value:从通道中取出的值,如果通道被关闭则返回对应类型的零值。
// ok:通道ch关闭时返回 false,否则返回 true。
func f2(ch chan int) {
for {
v, ok := <-ch
if !ok {
fmt.Println("通道已关闭")
break
}
//换成for range
// for v:=range ch {
// fmt.Println(v)
// }
fmt.Printf("v:%#v ok:%#v\n", v, ok)
}
}
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)
f2(ch)
}
无缓冲channel
func recv(ch chan int) {
ret := <-ch
fmt.Println("接收成功", ret)
}
func main() {
ch := make(chan int)
go recv(ch) // 创建一个 goroutine 从通道接收值
ch <- 15
fmt.Println("发送成功")
}
main goroutine
创建channel
启动 recv goroutine
recv:
等待接收 <-ch
main:
ch <- 15
↓
channel传递
↓
recv 收到15
这是无缓冲channel,没有储存空间
如果 :
func main(){
ch := make(chan int)
ch <- 15
go ch1(ch)
}
main:
ch <- 15
等待接收者
↓
永远等待
↓
deadlock//如果是有缓冲可以这样写
3.单向通道
<- chan int // 只接收通道,只能接收不能发送
chan <- int // 只发送通道,只能发送不能接收
// Producer2 返回一个接收通道
func Producer2() <-chan int {
ch := make(chan int, 2)
// 创建一个新的goroutine执行发送数据的任务
go func() {
for i := 0; i < 10; i++ {
if i%2 == 1 {
ch <- i
}
}
close(ch) // 任务完成后关闭通道
}()
return ch
}
// Consumer2 参数为接收通道
func Consumer2(ch <-chan int) int {
sum := 0
for v := range ch {
sum += v
}
return sum
}
func main() {
ch2 := Producer2()
res2 := Consumer2(ch2)
fmt.Println(res2) // 25
}
完整生产者消费者例子
func producer(ch chan<-int){
for i:=0;i<5;i++{
ch<-i
}
close(ch)//谁生产谁关闭
}
func consumer(ch <-chan int){
for v:=range ch{
fmt.Println("收到",v)
}
}
func main(){
ch:=make(chan int)
go producer(ch)
consumer(ch)
}
收到: 0
收到: 1
收到: 2
收到: 3
收到: 4
channel方向转换规则
双向
chan int
↓
单向发送
chan<- int
双向
chan int
↓
单向接收
<-chan int
不能单转双
4.Select多路复用
Select 的使用方式类似于之前学到的 switch 语句,它也有一系列 case 分支和一个默认的分支。每个 case 分支会对应一个通道的通信(接收或发送)过程。select 会一直等待,直到其中的某个 case 的通信操作完成时,就会执行该 case 分支对应的语句。
select{
case <-ch1:
//...
case date:=<-ch2 :
//...
case ch3<-10 :
//...
default :
// 默认操作
}
Select 语句具有以下特点
- 可处理一个或多个 channel 的发送/接收操作。
- 如果多个 case 同时满足,select 会随机选择一个执行。
- 对于没有 case 的 select 会一直阻塞,可用于阻塞 main 函数,防止退出。
ch1:=make(chan int, 5)
ch2:=make(chan string, 5)
for i:=0;i<5;i++{
ch1<-i
ch2<-"hello"+fmt.Sprintf("%d", i)
}
for {
select{
case v:=<-ch1:
fmt.Println("ch1", v)
case v:=<-ch2:
fmt.Println("ch2", v)
default:
fmt.Println("没有数据了")
return
}
}
//输出
ch2 hello0
ch1 0
ch2 hello1
ch1 1
ch1 2
ch1 3
ch2 hello2
ch2 hello3
ch2 hello4
ch1 4
没有数据了
打印10以内的偶数
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case ch <- i:
case v := <-ch:
fmt.Println(v)
}
}
//
0
2
4
6
8
关闭channel
for {
select {
case v, ok := <-ch1:
if !ok {
ch1关闭处理
return/标记
}
处理数据
case v, ok := <-ch2:
if !ok {
ch2关闭处理
return/标记
}
处理数据
}
}
ch1 := make(chan int, 5)
ch2 := make(chan string, 5)
for i := 0; i < 5; i++ {
ch1 <- i
ch2 <- "hello" + fmt.Sprintf("%d", i)
}
//关闭
close(ch1)
close(ch2)
for {
select {
case v, ok := <-ch1:
if !ok {
fmt.Println("ch1关闭了")
return
}
fmt.Println("ch1", v)
case v, ok := <-ch2:
if !ok {
fmt.Println("ch2关闭了")
return
}
fmt.Println("ch2", v)
}
}
recover解决panic
func saayhello() {
for i := 0; i < 5; i++ {
fmt.Println("hello world", i)
time.Sleep(time.Millisecond * 50)
}
}
func hello() {
defer func() {
if err := recover(); err != nil {
fmt.Println("hello()函数发生了错误", err)
}
}()
var mymap map[int]string
mymap[0] = "hello"
}
// 调用
go hello()
go saayhello()
for i := 0; i < 5; i++ {
fmt.Println("main() hello world", i)
}
time.Sleep(time.Second)
互斥锁
写锁(Lock()):
-
只允许一个 goroutine 持有写锁。
-
写操作是独占的,在写操作进行时,所有其他的读操作和写操作都会被阻塞,直到写锁释放。
var count int
var mutex sync.Mutex
var wg sync.WaitGroup
func test() {
for i := 0; i < 10000; i++ {
mutex.Lock()
count++
mutex.Unlock()
}
wg.Done()
}
//调用
wg.Add(2)
go test()
go test()
wg.Wait()
fmt.Println(count)// 20000
读写互斥锁
Go 提供的读写锁(sync.RWMutex)是一种允许多个 读操作并发,但 写操作互斥 的锁
sync.RWMutex 的操作
-
**RLock() 和 RUnlock():**用于加读锁和释放读锁。当一个 goroutine 获取读锁后,其他 goroutine 也可以同时获取读锁(只要没有写锁存在)。当读锁释放时,其他 goroutine 才能获取写锁。
-
**Lock() 和 Unlock():**用于加写锁和释放写锁。当一个 goroutine 获取写锁时,所有读操作和写操作都会被阻塞,直到写锁被释放。
var (
count int
wg sync.WaitGroup
rw sync.RWMutex
)
func Read() {
defer wg.Done()
rw.RLock()
defer rw.RUnlock()
fmt.Println(count)
}
func Write(n int) {
defer wg.Done()
rw.Lock()
defer rw.Unlock()
count = n
fmt.Println("修改数据:", count)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1)
go Read()
}
wg.Add(1)
go Write(5)
wg.Wait()
}
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)