✨✨ 欢迎大家来到景天科技苑✨✨
🎈🎈 养成好习惯,先赞后看哦~🎈🎈
🏆 作者简介:景天科技苑
🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。
🏆《博客》:Python全栈,Golang开发,PyQt5和Tkinter桌面开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi,flask等框架,云原生K8S,linux,shell脚本等实操经验,网站搭建,数据库等分享。
所属的专栏: Go语言开发零基础到高阶实战
景天的主页: 景天科技苑
文章目录
- Go语言中的Sync锁
-
- 一、互斥锁(Mutex)
-
- [1.1 基本用法](#1.1 基本用法)
- [1.2 使用sync.WaitGroup等待一组Goroutine完成](#1.2 使用sync.WaitGroup等待一组Goroutine完成)
- [1.3 注意事项](#1.3 注意事项)
- 二、读写锁(RWMutex)
-
- [2.1 基本用法](#2.1 基本用法)
- [2.2 注意事项](#2.2 注意事项)
- 三、Once(一次执行)
-
- [3.1 基本用法](#3.1 基本用法)
- [3.2 注意事项](#3.2 注意事项)
- 四、总结
Go语言中的Sync锁
在Go语言的并发编程中,如何确保多个goroutine安全地访问共享资源是一个关键问题。Go语言提供了sync包,其中包含了多种同步原语,用于解决并发编程中的同步问题。本文将详细介绍sync包中的锁机制,并结合实际案例,帮助读者理解和使用这些锁。
要想解决临界资源安全的问题,很多编程语言的解决方案都是同步。
通过上锁的方式,某一时间段,只能允许一个goroutine来访问这个共享数据,当前goroutine访问完毕, 解锁后,其他的goroutine才 能来访问。
我们可以借助于sync包下的锁操作。 synchronization
但是实际上,在Go的并发编程中有一句很经典的话:不要以共享内存的方式去通信:锁,而要以通信的方式去共享内存。
共享内存的方式
锁:多个线程拿的是同一个钥匙,go语言不建议使用锁机制来解决。不要以共享内存的方式去通信
而要以通信的方式去共享内存 go语言更建议我们使用 chan(通道) 来解决安全问题。(后面会学)
在Go语言中并不鼓励用锁保护共享状态的方式,在不同的Goroutine中分享信息(以共享内存的方式去通信)。
而是鼓励通过channeI将共享状态或共享状态的变化在各个Goroutine之间传递(以通信的方式去共享内存),这样同样能像用锁一样保证在同一的时间只有一个Goroutine访问共享状态。
当然,在主流的编程语言中为了保证多线程之间共享数据安全性和一致性,都会提供一套基本的同步工具集,如锁,条件变量,原子操作等等。
Go语言标准库也毫不意外的提供了这些同步机制,使用方式也和其他语言也差不多。
一、互斥锁(Mutex)
互斥锁(sync.Mutex)是最基本的同步机制之一,用于确保同一时间只有一个goroutine能够访问特定的资源。
当一个goroutine持有互斥锁时,其他试图获取该锁的goroutine将会被阻塞,直到锁被释放。
1.1 基本用法
go
package main
import (
"fmt"
"sync"
"time"
)
// 定义全局变量 票库存为10张
var tickets int = 10
// 定义一个锁 Mutex 锁头
var mutex sync.Mutex
func main() {
go saleTicket("张三")
go saleTicket("李四")
go saleTicket("王五")
go saleTicket("赵六")
time.Sleep(time.Second * 5)
}
// 售票函数
func saleTicket(name string) {
for {
// 在拿到共享资源之前先上锁
mutex.Lock()
if tickets > 0 {
time.Sleep(time.Millisecond * 1)
fmt.Println(name, "剩余票的数量为:", tickets)
tickets--
} else {
// 票卖完,解锁
mutex.Unlock()
fmt.Println("票已售完")
break
}
// 操作完毕后,解锁
mutex.Unlock()
}
}
上锁之后,就不会出现问题了
1.2 使用sync.WaitGroup等待一组Goroutine完成
sync.WaitGroup类型可以用来等待一组Goroutine完成。例如:
go
package main
import (
"fmt"
"sync"
"time"
)
// waitgroup、
var wg sync.WaitGroup
func main() {
// 公司最后关门的人 0
// wg.Add(2) wg.Add(2)来告诉WaitGroup我们要等待两个Goroutine完成 开启几个协程,就add几个
// wg.Done() 我告知我已经结束了 defer wg.Done()来在Goroutine完成时通知WaitGroup
// 开启几个协程,就add几个
wg.Add(2)
go test1()
go test2()
fmt.Println("main等待ing")
wg.Wait() // 等待 wg 归零,wg.Wait()来等待所有Goroutine完成 代码才会继续向下执行
fmt.Println("end")
// 理想状态:所有协程执行完毕之后,自动停止。
//如果每次都强制设置个等待时间。那么协程代码也可能在这个时间内还没跑完,也可能提前就跑完了,所以设置死的等待时间不合理。此时就需要用到了等待组WaitGroup
//time.Sleep(1 * time.Second)
}
func test1() {
for i := 0; i < 10; i++ {
time.Sleep(1 * time.Second)
fmt.Println("test1--", i)
}
wg.Done() //这里就将该代码块放在了其他逻辑之后
}
func test2() {
defer wg.Done() // defer wg.Done()来在Goroutine完成时通知WaitGroup 如果不用defer就得把该方法放在其他代码之后
for i := 0; i < 10; i++ {
fmt.Println("test2--", i)
}
}
主线程会等待所有协程执行完毕,才继续往下执行代码
1.3 注意事项
避免死锁 :确保在获取锁之后,无论发生什么情况(包括panic),都能够释放锁。可以使用defer语句来确保锁的释放。
减少锁的持有时间 :锁的持有时间越长,其他goroutine被阻塞的时间就越长,系统的并发性能就越差。因此,应该尽量减少锁的持有时间,只在必要的代码段中持有锁。
避免嵌套锁:尽量避免在一个锁已经持有的情况下再尝试获取另一个锁,这可能会导致死锁。
避免忘记调用Done :如果忘记调用Done方法,WaitGroup将会永远等待下去,导致程序无法正常结束。
避免负数计数器:调用Add方法时,如果传入的参数为负数,或者导致计数器变为负数,将会导致panic。
二、读写锁(RWMutex)
读写锁(sync.RWMutex)允许多个goroutine同时读取资源,但在写入时会阻塞所有其他读和写的goroutine。读写锁可以提高读多写少的场景下的并发性能。
2.1 基本用法
go
package main
import (
"fmt"
"sync"
)
var (
data map[string]int
rwMu sync.RWMutex
)
func readData(key string) int {
rwMu.RLock()
defer rwMu.RUnlock()
return data[key]
}
func writeData(key string, value int) {
rwMu.Lock()
defer rwMu.Unlock()
data[key] = value
}
func main() {
data = make(map[string]int)
var wg sync.WaitGroup
// 写操作
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
writeData(fmt.Sprintf("key%d", i), i*10)
}(i)
}
// 读操作
for i := 0; i < 100; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
value := readData(fmt.Sprintf("key%d", i%10))
fmt.Printf("Read: key%d = %d\n", i%10, value)
}(i)
}
wg.Wait()
}
在上述代码中,我们定义了一个全局变量data和一个读写锁rwMu。readData函数用于读取data中的值,在读取之前先获取读锁,读取完成后释放读锁。
writeData函数用于写入data中的值,在写入之前先获取写锁,写入完成后释放写锁。
在main函数中,我们启动了10个写goroutine和100个读goroutine,分别调用writeData和readData函数。通过sync.WaitGroup等待所有goroutine完成。
2.2 注意事项
避免写锁长时间持有:写锁会阻塞所有其他读和写的goroutine,因此应该尽量减少写锁的持有时间。
读多写少场景:读写锁适用于读多写少的场景,如果写操作非常频繁,读写锁的性能优势可能会消失。
避免嵌套锁:与互斥锁类似,读写锁也应该避免嵌套使用。
三、Once(一次执行)
sync.Once用于确保某个操作只执行一次,无论有多少个goroutine调用它。这对于单例模式或初始化只执行一次的场景非常有用。
3.1 基本用法
go
package main
import (
"fmt"
"sync"
)
var (
once sync.Once
message string
)
func initMessage() {
message = "Hello, World!"
}
func printMessage() {
once.Do(initMessage)
fmt.Println(message)
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
printMessage()
}()
}
wg.Wait()
}
在上述代码中,我们定义了一个全局变量message和一个sync.Once类型的变量once。
initMessage函数用于初始化message的值。printMessage函数通过once.Do方法确保initMessage只被调用一次,然后打印出message的值。
在main函数中,我们启动了10个goroutine,每个goroutine都调用printMessage函数。通过sync.WaitGroup等待所有goroutine完成。
3.2 注意事项
避免重复初始化:sync.Once确保某个操作只执行一次,因此它通常用于初始化全局变量或执行其他只需要执行一次的操作。
性能开销:虽然sync.Once的性能开销很小,但在高性能要求的场景下,仍然需要注意其使用。
四、总结
本文详细介绍了Go语言中sync包中的锁机制,包括互斥锁(sync.Mutex)、读写锁(sync.RWMutex)、Once(一次执行)和WaitGroup(等待组)。
通过实际案例,帮助读者理解和使用这些锁。在并发编程中,正确地使用这些同步原语,可以确保多个goroutine安全地访问共享资源,避免数据竞争和其他并发问题。希望本文能够对大家有所帮助。