欢迎光临
我们一直在努力

Go 学习笔记03:goroutine、channel、WaitGroup 与 Mutex 并发入门

上一篇主要整理了 Go 中函数、struct、指针和 interface 的基础用法。

这一篇继续记录 Go 中非常有特色的一部分:并发编程。

本篇主要整理目前已经学习过的几个知识点:

goroutine
channel
缓冲 channel
close
WaitGroup
defer
Mutex
select
timeout

这些内容以后在 Go 后端、网络编程以及并发任务处理中都会经常遇到。


一、goroutine 是什么

goroutine 可以先简单理解成:

让一个函数并发执行。

普通调用:

func work() {
fmt.Println("working")
}

func main() {
work()
}

这里 work() 会按照正常顺序执行。

如果加上:

go work()

就表示:

启动一个新的 goroutine 来执行 work()。

例如:

package main

import "fmt"

func work() {
fmt.Println("working")
}

func main() {
go work()
}

需要注意:

如果 main() 太快结束,新的 goroutine 可能还没来得及执行,整个程序就已经退出了。

所以 goroutine 启动之后,通常还需要配合其他机制进行等待或同步。


二、channel 是什么

channel 可以理解成:

goroutine 之间传递数据的通道。

创建一个传递整数的 channel:

ch := make(chan int)

发送数据:

ch <- 10

接收数据:

x := <-ch

例如:

package main

import "fmt"

func main() {
ch := make(chan int)

go func() {
ch <- 20
}()

x := <-ch

fmt.Println(x)
}

输出:

20

这里:

ch <- 20

表示把 20 放进 channel。

而:

x := <-ch

表示从 channel 取出一个值。


三、channel 还可以用于等待

如果:

x := <-ch

执行时 channel 里暂时没有数据,那么当前 goroutine 会在这里等待。

直到另一个 goroutine执行:

ch <- 100

程序才会继续。

所以 channel 不仅能传递数据,还能起到一定的同步作用。


四、无缓冲 channel

下面这种:

ch := make(chan int)

叫做无缓冲 channel。

它的特点是:

发送和接收通常需要配合进行。

例如:

ch := make(chan int)

ch <- 10

如果没有其他 goroutine 来接收数据,那么:

ch <- 10

可能会一直阻塞。

正确示例:

ch := make(chan int)

go func() {
ch <- 10
}()

fmt.Println(<-ch)

输出:

10


五、有缓冲 channel

还可以创建有缓冲的 channel:

ch := make(chan int, 2)

这里的:

2

表示 channel 最多可以暂时存放 2 个整数。

例如:

ch := make(chan int, 2)

ch <- 10
ch <- 20

fmt.Println(<-ch)
fmt.Println(<-ch)

输出:

10
20

可以先理解为:

[10, 20]

先放进去的值先被取出来。

如果缓冲区已经满了,再继续发送数据,就会阻塞,直到有人先取走数据。


六、关闭 channel

可以使用:

close(ch)

关闭 channel。

需要注意:

关闭 channel 并不会立即清空里面已经存在的数据。

例如:

ch := make(chan int, 2)

ch <- 10
ch <- 20

close(ch)

fmt.Println(<-ch)
fmt.Println(<-ch)

仍然可以得到:

10
20

close(ch) 更准确的意思是:

告诉接收方,以后不会再发送新的数据了。


七、判断 channel 是否已经读取完

可以这样接收:

value, ok := <-ch

其中:

value -> 读到的数据
ok -> 是否还能正常读取

例如:

ch := make(chan int, 1)

ch <- 8
close(ch)

value, ok := <-ch

此时:

value = 8
ok = true

如果数据已经全部读取完,再读取一次:

value = 0
ok = false


八、使用 range 读取 channel

channel 也可以配合 range:

for value := range ch {
fmt.Println(value)
}

例如:

ch := make(chan int, 3)

ch <- 1
ch <- 2
ch <- 3

close(ch)

for value := range ch {
fmt.Println(value)
}

输出:

1
2
3

这个循环会一直读取,直到:

channel 已经关闭,并且里面的数据全部读取完。

所以如果使用:

for value := range ch

一般要注意 channel 是否会被关闭,否则循环可能一直等待。


九、WaitGroup 是什么

如果启动了多个 goroutine,有时希望主程序等它们全部执行结束。

这时候可以使用:

sync.WaitGroup

例如:

var wg sync.WaitGroup

常见三个操作:

wg.Add(1)
wg.Done()
wg.Wait()

可以理解为:

Add(n) -> 登记 n 个任务
Done() -> 当前任务完成
Wait() -> 等待所有任务完成

例如:

package main

import (
"fmt"
"sync"
)

func main() {
var wg sync.WaitGroup

wg.Add(2)

go func() {
fmt.Println("任务1")
wg.Done()
}()

go func() {
fmt.Println("任务2")
wg.Done()
}()

wg.Wait()

fmt.Println("全部完成")
}

任务1 和 任务2 的先后顺序不一定固定。

但:

全部完成

一定会在两个任务都结束之后输出。


十、defer 的基础用法

defer 可以先理解成:

把某个操作延迟到当前函数结束之前执行。

例如:

func test() {
defer fmt.Println("B")

fmt.Println("A")
}

输出:

A
B

因为:

defer fmt.Println("B")

不会马上执行,而是在函数结束之前执行。


十一、多个 defer 的执行顺序

如果有多个 defer:

func test() {
defer fmt.Println("A")
defer fmt.Println("B")
defer fmt.Println("C")

fmt.Println("D")
}

输出:

D
C
B
A

多个 defer 是:

后进先出。

也就是最后登记的 defer 最先执行。


十二、defer 和 WaitGroup 配合

实际代码里常写:

go func() {
defer wg.Done()

fmt.Println("working")
}()

这样可以保证当前 goroutine 结束时执行:

wg.Done()

相比把 wg.Done() 手动放在函数最后,更不容易忘记。


十三、Mutex 是什么

多个 goroutine 同时修改同一个变量时,可能出现数据竞争。

例如:

count++

看起来只有一行,但实际可以理解成:

读取 count
加 1
写回 count

如果多个 goroutine 同时进行,就可能导致结果不准确。

这时候可以使用:

sync.Mutex

也就是互斥锁。

基本写法:

mu.Lock()

count++

mu.Unlock()

可以理解为:

Lock() -> 我现在开始修改,其他 goroutine 先等一下
Unlock() -> 我修改完成了,其他 goroutine 可以继续


十四、WaitGroup 和 Mutex 综合使用

例如启动 5 个 goroutine,每个都让 count 加 1:

package main

import (
"fmt"
"sync"
)

func main() {
var wg sync.WaitGroup
var mu sync.Mutex

count := 0

for i := 0; i < 5; i++ {
wg.Add(1)

go func() {
defer wg.Done()

mu.Lock()
count++
mu.Unlock()
}()
}

wg.Wait()

fmt.Println(count)
}

最终输出:

5

这里:

WaitGroup

负责等待 5 个 goroutine 全部完成。

而:

Mutex

负责保证 count++ 不会被多个 goroutine 同时修改。


十五、goroutine 把结果通过 channel 返回

例如:

func square(n int, ch chan int) {
ch <- n * n
}

调用:

ch := make(chan int)

go square(5, ch)

result := <-ch

fmt.Println(result)

输出:

25

这里:

square()

在新的 goroutine 中进行计算。

然后通过:

ch <- n * n

把结果传回主 goroutine。


十六、并发任务完成顺序不固定

例如:

go square(2, ch)
go square(3, ch)

a := <-ch
b := <-ch

最终得到的两个结果一定是:

4
9

但:

a = 4, b = 9

和:

a = 9, b = 4

都有可能。

原因是:

goroutine 的启动顺序,不代表它们一定按照这个顺序完成。

这是学习并发时比较重要的一点。


十七、select 是什么

select 用来处理多个 channel。

例如:

select {
case msg1 := <-ch1:
fmt.Println(msg1)

case msg2 := <-ch2:
fmt.Println(msg2)
}

可以理解为:

同时等待多个 channel,哪个先准备好,就先处理哪个。

如果 ch1 先有数据,就执行第一个 case。

如果 ch2 先有数据,就执行第二个。


十八、select 中的 default

例如:

select {
case value := <-ch:
fmt.Println(value)

default:
fmt.Println("no data")
}

如果当前 channel 没有数据,那么不会一直等待,而是直接执行:

no data

所以:

default

可以理解为:

如果当前没有任何 case 可以立即执行,就执行 default。

需要注意:

default 不是最后一定会执行。

只要有其他 case 可以执行,就不会进入 default。


十九、select + timeout 超时控制

实际开发中,经常不能一直等待某个任务。

这时可以使用:

time.After()

例如:

select {
case msg := <-ch:
fmt.Println(msg)

case <-time.After(2 * time.Second):
fmt.Println("timeout")
}

意思是:

等待 channel 的数据,但最多等待 2 秒。

如果 2 秒内收到数据,就处理数据。

如果 2 秒仍然没有收到,就输出:

timeout


二十、超时控制完整例子

package main

import (
"fmt"
"time"
)

func worker(ch chan string) {
time.Sleep(1 * time.Second)

ch <- "任务完成"
}

func main() {
ch := make(chan string)

go worker(ch)

select {
case msg := <-ch:
fmt.Println(msg)

case <-time.After(2 * time.Second):
fmt.Println("timeout")
}
}

因为任务只需要等待 1 秒,而超时时间是 2 秒,所以最终输出:

任务完成

如果把:

time.Sleep(1 * time.Second)

改成:

time.Sleep(3 * time.Second)

那么超时时间会先到,最终输出:

timeout


二十一、目前容易混淆的几个地方

1. goroutine 和 channel 不是同一个东西

goroutine 是:

执行任务的并发单元。

channel 是:

goroutine 之间传递数据的通道。

可以简单理解成:

goroutine -> 干活的人
channel -> 传递数据的通道


2. goroutine 不是进程

一开始容易把:

go func()

理解成“创建新进程”。

实际上更准确的说法是:

启动新的 goroutine。

goroutine 是 Go 运行时管理的轻量级并发任务,并不是操作系统进程。


3. 启动顺序不等于完成顺序

例如:

go task1()
go task2()

并不能保证:

task1 一定先完成
task2 一定后完成

这也是并发程序需要特别注意的地方。


4. WaitGroup 和 Mutex 作用不同

WaitGroup:

等待多个 goroutine 全部完成。

Mutex:

防止多个 goroutine 同时修改共享数据。

它们解决的是两个不同的问题。


二十二、学习总结

这一阶段主要学习了 Go 的并发基础。

目前我对几个核心概念的理解是:

goroutine
-> 并发执行任务

channel
-> goroutine 之间传递数据

WaitGroup
-> 等待多个 goroutine 完成

Mutex
-> 保护共享数据,避免数据竞争

defer
-> 函数结束前执行指定操作

select
-> 同时处理多个 channel

time.After
-> 实现简单的超时控制

Go 的并发部分比前面的基础语法更抽象,目前还只是掌握了基本概念和简单用法。

后面还需要通过实际项目继续巩固。

下一阶段准备开始学习 Go 的工程基础,包括:

package
go mod
项目目录结构
JSON
文件读写
go test

之后再进入 HTTP、Gin 和 MySQL,开始真正编写 Go 后端项目。

赞(0)
未经允许不得转载:171主机测评 » Go 学习笔记03:goroutine、channel、WaitGroup 与 Mutex 并发入门
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址