《10节课学会Golang-09-Goroutine》
项目地址:github.com/Zhouchaowen/golang-tuto...
视频地址:b站 10节课学会Golang,Go快速入门
Goroutine
Goroutine 是 Go 语言中轻量级的并发处理方式之一。它可以看作是一个轻量级线程,一个程序可以包含成百上千个 Goroutine。Goroutine 的启动非常快,只需要几纳秒的时间,而且 Goroutine 的调度是由 Go 运行时系统自动完成的,开发者不需要手动进行线程调度。
Goroutine基础
golang中想要并发的执行一短逻辑可以通过go func()实现。
go func() {
// goroutine 执行的代码
}()
一个go func()会启动一个后台并发任务, 大概流程是通过go关键字将这个func()打包成一个任务,然后提交给golang的并发调度器,并发调度器会根据一定策略来执行这些任务。
package main
import (
"fmt"
"time"
)
// 并发与并行:https://gfw.go101.org/article/control-flows-more.html
// 使用 goroutine 打印数据
func main() {
language := []string{"golang", "java", "c++", "python", "rust", "js"}
tutorial := []string{"入门", "初级", "中级", "高级", "专家"}
// Go 程(goroutine)是由 Go 运行时管理的轻量级线程
// 在函数调⽤语句前添加 go 关键字,就可创建一个 goroutine
go listLanguage(language) // 通过goroutine启动该函数
go listTutorial(tutorial)
<-time.After(time.Second * 10) // 10s后执行下一行
fmt.Println("return")
}
func listLanguage(items []string) {
for i := range items {
fmt.Printf("language: %s\n", items[i])
time.Sleep(time.Second)
}
}
func listTutorial(items []string) {
for i := range items {
fmt.Printf("tutorial: %s\n", items[i])
time.Sleep(time.Second)
}
}
WaitGroup使用
再上一小节中通过<-time.After(time.Second * 10)来等待goroutine执行完成, 这是非常难以控制的。
在真实的场景中我们并不那么容易知道一个Goroutine什么时候执行完成, 我们需要一种更简单的方式来等待Goroutine的结束。
sync.WaitGroup 是 Go 语言中用于并发控制的一个结构体,它可以用于等待一组 Goroutine 的完成。
WaitGroup 包含三个方法:
Add(delta int):向WaitGroup中添加delta个等待的Goroutine。Done():表示一个等待的Goroutine已经完成了,向WaitGroup中减少一个等待的Goroutine。Wait():等待所有添加到WaitGroup中的Goroutine都完成。
使用 WaitGroup 进行并发控制的基本流程如下:
- 创建
WaitGroup对象wg。 - 启动多个
Goroutine,在每个Goroutine的开始处调用wg.Add(1)将等待的Goroutine数量加 1。 - 在每个
Goroutine中进行任务处理,当任务处理完毕后,在Goroutine的结束处调用wg.Done()将已完成的Goroutine数量减 1。 - 在主
Goroutine中调用wg.Wait()等待所有的Goroutine完成任务。
package main
import (
"fmt"
"sync"
"time"
)
func listLanguage(items []string, wg *sync.WaitGroup) { // 一般不建议这样使用
defer wg.Done()
for i := range items {
fmt.Printf("language: %s\n", items[i])
time.Sleep(time.Second)
}
}
func listTutorial(items []string) {
for i := range items {
fmt.Printf("tutorial: %s\n", items[i])
time.Sleep(time.Second)
}
}
// 使用 WaitGroup等待goroutine执行完成
func main() {
language := []string{"golang", "java", "c++", "python", "rust", "js"}
tutorial := []string{"入门", "初级", "中级", "高级", "专家"}
var wg sync.WaitGroup
wg.Add(2) // 设置需要等待 goroutine 的数量,目前为2
go listLanguage(language, &wg) // 通过 goroutine 启动该函数
go func() { // 建议使用方式
defer wg.Done() // 程序运行完毕, 将等待数量减1
listTutorial(tutorial)
}()
wg.Wait() // 当等待数量为0后执行下一行
//<-time.After(time.Second * 10) // 10s后执行下一行。 通过 wg.Wait() 代替
fmt.Println("return")
}
并发下载图片小练习
package main
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"sync"
)
func getImageData(url, name string) {
resp, _ := http.Get(url) // 通过 http.get 请求读取 url 的数据
// 创建一个缓存读取返回的 response 数据
buf := new(bytes.Buffer)
buf.ReadFrom(resp.Body)
dir, _ := os.Getwd() // 获取当前执行程序目录
fileName := path.Join(dir, name) // 拼接保存图片的文件地址
// 将数据写到指定文件地址,权限为0666
err := ioutil.WriteFile(fileName, buf.Bytes(), 0666)
if err != nil {
fmt.Printf("Save to file failed! %v", err)
}
}
// 并发下载图片
func main() {
var wg sync.WaitGroup
defer wg.Wait()
wg.Add(3)
go func() {
defer wg.Done()
getImageData("https://img2.baidu.com/it/u=3125736368,3712453346&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500", "1.jpg")
}()
go func() {
defer wg.Done()
getImageData("https://img2.baidu.com/it/u=4284966505,4095784909&fm=253&fmt=auto&app=138&f=JPEG?w=640&h=400", "2.jpg")
}()
go func() {
defer wg.Done()
getImageData("https://img1.baidu.com/it/u=3580024761,2271795904&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=667", "3.jpg")
}()
}
Goroutine并发安全
Goroutine 的出现使得 Go 语言可以更加方便地进行并发编程。但是在使用 Goroutine 时需要注意避免资源竞争和死锁等问题。
当多个goroutine并发修改同一个变量有可能会产生并发安全问题导致结果错误,因为修改可能是非原子的。这种情况可以将修改变成原子操作(atomic)或通过加锁保护(sync.Mutex, sync.RWMutex),让修改的步骤串行防止并发安全问题。
package main
import (
"fmt"
"sync"
)
// NoConcurrence 并发操作一个变量是不安全的,需要加锁
func NoConcurrence() {
sum := 0
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 10000000; i++ {
sum++
}
}()
go func() {
defer wg.Done()
for i := 0; i < 10000000; i++ {
sum++
}
}()
wg.Wait()
fmt.Println(sum)
}
func Concurrence() {
sum := 0
var wg sync.WaitGroup
var mu sync.Mutex // 互斥锁(保护临界区,同一时刻只能有一个 goroutine 可以操作临界区)
// var rmu sync.RWMutex
wg.Add(2) // 设置需要等待 goroutine 的数量,目前为2
go func() {
defer wg.Done() // 程序运行完毕, 将 goroutine 等待数量减1
for i := 0; i < 10000000; i++ {
mu.Lock() // 加锁保护临界区
sum++
mu.Unlock() // 操作完成解锁,临界区
}
}()
go func() {
defer wg.Done()
for i := 0; i < 10000000; i++ {
mu.Lock() // 加锁保护临界区
sum++
mu.Unlock() // 操作完成解锁,临界区
}
}()
wg.Wait()
fmt.Println(sum)
}
// goroutine 的并发安全问题
func main() {
NoConcurrence()
Concurrence()
}
Mutex 和 RWMutex 都是 Go 语言中的并发控制机制,它们都可以用于保护共享资源,避免并发访问导致的数据竞争和不一致性。
Mutex 是最简单的并发控制机制,它提供了两个方法:
Lock():获取互斥锁,如果互斥锁已经被其他Goroutine获取,则当前Goroutine会阻塞等待。Unlock():释放互斥锁,如果当前Goroutine没有获取互斥锁,则会引发运行时panic。(必须先Lock, 在Unlock)
Mutex 适用于对共享资源的互斥访问,即同一时间只能有一个 Goroutine 访问共享资源的情况。
RWMutex 是在 Mutex 的基础上进行了扩展,它允许多个 Goroutine 同时读取共享资源,但只允许一个 Goroutine 写共享资源。RWMutex 提供了三个方法:
RLock():获取读锁,允许多个Goroutine同时获取读锁。RUnlock():释放读锁。Lock():获取写锁,只允许一个Goroutine获取写锁。Unlock():释放互斥锁。
RWMutex 适用于读写分离的场景,可以提高共享资源的并发读取性能。
思考题
参考
blog.boot.dev/golang/gos-waitgroup...
gfw.go101.org/article/control-flow...
larrylu.blog/race-condition-in-gol...
项目地址
github.com/Zhouchaowen/golang-tuto...
交流群
微信关注【面试情报局】我们一起干翻面试官, 回复golang加入交流群
本作品采用《CC 协议》,转载必须注明作者和本文链接
关于 LearnKu
推荐文章: