这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天
今天来学点语言进阶...
Goroutine (并发)
并发指的是多个任务被(一个)cpu 轮流切换执行,在 Go 语言里面主要用 goroutine (协程)来实现并发,类似于其他语言中的线程。
语法
go f(x, y, z)
具体例子
首先我们看一个例子:
package main
import (
"log"
"time"
)
func doSomething(id int) {
log.Printf("before do job:(%d) \n", id)
time.Sleep(3 * time.Second)
log.Printf("after do job:(%d) \n", id)
}
func main() {
doSomething(1)
doSomething(2)
doSomething(3)
}
输出结果为:
2023/01/16 12:13:20 before do job:(1)
2023/01/16 12:13:23 after do job:(1)
2023/01/16 12:13:23 before do job:(2)
2023/01/16 12:13:26 after do job:(2)
2023/01/16 12:13:26 before do job:(3)
2023/01/16 12:13:29 after do job:(3)
可以看到执行完结果总共耗时 9 秒,每个任务是阻塞的。
我们可以使用 goroutine 并发执行任务,从而整体加快速度,下面是使用 goroutine 改进的代码:
package main
import (
"log"
"time"
)
func doSomething(id int) {
log.Printf("before do job:(%d) \n", id)
time.Sleep(3 * time.Second)
log.Printf("after do job:(%d) \n", id)
}
func main() {
go doSomething(1)
go doSomething(2)
go doSomething(3)
}
当运行代码的时候,会发现没有任何输出。
这是因为我们的 main() 函数其实也是在一个 goroutine 中执行,但是 main() 执行完毕后,其他三个 goroutine 还没开始执行,所以就无法看到输出结果。
为了看到输出结果,我们可以使用 time.Sleep() 方法让 main() 函数延迟结束,例如:
package main
import (
"log"
"time"
)
func doSomething(id int) {
log.Printf("before do job:(%d) \n", id)
time.Sleep(3 * time.Second)
log.Printf("after do job:(%d) \n", id)
}
func main() {
go doSomething(1)
go doSomething(2)
go doSomething(3)
time.Sleep(4 * time.Second)
}
输出结果为:
2023/01/16 12:24:23 before do job:(2)
2023/01/16 12:24:23 before do job:(1)
2023/01/16 12:24:23 before do job:(3)
2023/01/16 12:24:26 after do job:(3)
2023/01/16 12:24:26 after do job:(2)
2023/01/16 12:24:26 after do job:(1)
可以看到,执行完所有任务从原本的 9 秒下降到 3 秒,大大提高了我们的效率,根据打印输出结果还可以看出:
- 多个 goroutine 的执行是随机。
- 对于 IO 密集型任务特别有效,比如文件,网络读写。
使用 sync.WaitGroup 实现同步
上面例子中,其实我们还可以使用 sync.WaitGroup 来等待所有的 goroutine 结束,从而实现并发的同步,这比使用 time.Sleep() 更加优雅,例如:
package main
import (
"log"
"sync"
"time"
)
func doSomething(id int, wg *sync.WaitGroup) {
defer wg.Done()
log.Printf("before do job:(%d) \n", id)
time.Sleep(3 * time.Second)
log.Printf("after do job:(%d) \n", id)
}
func main() {
var wg sync.WaitGroup
wg.Add(3)
go doSomething(1, &wg)
go doSomething(2, &wg)
go doSomething(3, &wg)
wg.Wait()
log.Printf("finish all jobs\n")
}
运行代码输出结果为:
2023/01/16 13:56:09 before do job:(1)
2023/01/16 13:56:09 before do job:(3)
2023/01/16 13:56:09 before do job:(2)
2023/01/16 13:56:12 after do job:(1)
2023/01/16 13:56:12 after do job:(2)
2023/01/16 13:56:12 after do job:(3)
2023/01/16 13:56:12 finish all jobs
channel
channel 简介
goroutine 是 Go 中实现并发的重要机制,channel 是 goroutine 之间进行通信的重要桥梁。
使用内建函数 make 可以创建 channel,举例如下:
ch := make(chan int) // 注意: channel 必须定义其传递的数据类型
也可以用 var 声明 channel, 如下:
var ch chan int
以上声明的 channel 都是双向的,意味着可以该 channel 可以发送数据,也可以接收数据。
“发送”和“接收”是 channel 的两个基本操作。
ch <- x // channel 接收数据 x
x <- ch // channel 发送数据并赋值给 x
<- ch // channel 发送数据,忽略接受者
channel buffer
上文提到,可以通过 make(chan int) 创建channel,此类 channel 称之为非缓冲通道。事实上 channel 可以定义缓冲大小,如下:
chInt := make(chan int) // unbuffered channel 非缓冲通道
chBool := make(chan bool, 0) // unbuffered channel 非缓冲通道
chStr := make(chan string, 2) // bufferd channel 缓冲通道
需要注意的是,程序中必须同时有不同的 goroutine 对非缓冲通道进行发送和接收操作,否则会造成阻塞。
以下是一个错误的使用示例:
func main() {
ch := make(chan string)
ch <- "ping"
fmt.Println(<-ch)
}
这一段代码运行后提示错误: fatal error: all goroutines are asleep - deadlock!。
因为 main 函数是一个 goroutine, 在这一个 goroutine 中发送了数据给非缓冲通道,但是却没有另外一个 goroutine 从非缓冲通道中里读取数据, 所以造成了阻塞或者称为死锁。
在以上代码中添加一个 goroutine 从非缓冲通道中读取数据,程序就可以正常工作。如下所示:
func main() {
ch := make(chan string)
go func() {
ch <- "ping"
}()
fmt.Println(<-ch)
}
与非缓冲通道不同,缓冲通道可以在同一个 goroutine 内接收容量范围内的数据,即便没有另外的 goroutine 进行读取操作,如下代码可以正常执行:
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
}
单向 channel
单向通道即限定了该 channel 只能接收或者发送数据,单向通道通常作为函数的参数,如下例所示:
func receive(receiver chan<- string, msg string) {
receiver <- msg
}
func send(sender <-chan string, receiver chan<- string) {
msg := <-sender
receiver <- msg
}
func main() {
ch1 := make(chan string, 1)
ch2 := make(chan string, 1)
receive(ch1, "pass message")
send(ch1, ch2)
fmt.Println(<-ch2)
}
需要注意的是,在变量声明中是不应该出现单向通道的,因为通道本来就是为了通信而生,只能接收或者只能发送数据的通道是没有意义的。
channel 遍历和关闭
close() 函数可以用于关闭 channel,关闭后的 channel 中如果有缓冲数据,依然可以读取,但是无法再发送数据给已经关闭的channel。
func main() {
ch := make(chan int, 10)
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
res := 0
for v := range ch {
res += v
}
fmt.Println(res)
}
select 语句
select 专门用于通道发送和接收操作,看起来和 switch 很相似,但是进行选择和判断的方法完全不同。
在下述例子中,通过 select 的使用,保证了 worker 中的事务可以执行完毕后才退出 main 函数
func strWorker(ch chan string) {
time.Sleep(1 * time.Second)
fmt.Println("do something with strWorker...")
ch <- "str"
}
func intWorker(ch chan int) {
time.Sleep(2 * time.Second)
fmt.Println("do something with intWorker...")
ch <- 1
}
func main() {
chStr := make(chan string)
chInt := make(chan int)
go strWorker(chStr)
go intWorker(chInt)
for i := 0; i < 2; i++ {
select {
case <-chStr:
fmt.Println("get value from strWorker")
case <-chInt:
fmt.Println("get value from intWorker")
}
}
}
工程实践这一节,进度17/80ppt
看来过年要加更了