1、GO学习之Hello World
2、GO学习之入门语法
3、GO学习之切片操作
4、GO学习之 Map 操作
5、GO学习之 结构体 操作
6、GO学习之 通道(Channel)
7、GO学习之 多线程(goroutine)
8、GO学习之 函数(Function)
9、GO学习之 接口(Interface)
10、GO学习之 网络通信(Net/Http)
11、GO学习之 微框架(Gin)
12、GO学习之 数据库(mysql)
13、GO学习之 数据库(Redis)
14、GO学习之 搜索引擎(ElasticSearch)
15、GO学习之 消息队列(Kafka)
16、GO学习之 远程过程调用(RPC)
17、GO学习之 goroutine的调度原理
18、GO学习之 通道(nil Channel妙用)
19、GO学习之 同步操作sync包
20、GO学习之 互斥锁、读写锁该如何取舍
21、GO学习之 条件变量 sync.Cond
按照公司目前的任务,go 学习是必经之路了,虽然行业卷,不过技多不压身,依旧努力!!!
同步操作在并发中是必不可少的的,若对一些公用的资源进行操作,为了保证操作的原子和一致性,就需要使用到锁来进行控制。
Go 语言在提供CSP(通信顺序进程)并发模型原语的的同时,还提供了标准库 sync包 针对传统基于共享内存并发模型的同步原语,包括 互斥锁(sync.Mutex)、读写锁(sync.RWMutex)、条件锁(sync.Cond)等。
不要通过共享内存来通信,而是要通过通信来共享内存
,一般的场景中,先使用 CSP 并发模型实现,就是 goroutine + channel 编程。但也有一些特殊的场景,需要 sync包 提供的低级同步原语。我们来对 channel 和 sync包 的性能做一个对比,示例代码如下(channel_sync_test.go ):
注意!!!
- 基准测试代码文件必须是_test.go结尾,和单元测试一样;
- 基准测试的函数以Benchmark开头;
- 参数为 *testing.B;
- 基准测试函数不能有返回值;
- b.ResetTimer是重置计时器,这样可以避免for循环之前的初始化代码的干扰;
- b.N是基准测试框架提供的,是循环次数,无需关心;
- go test -bench . .\channel_sync_test.go 运行;
package main
import (
"sync"
"testing"
)
var data = 0
// 声明互斥锁
var mu sync.Mutex
// 声明一个通道
var ch = make(chan struct{}, 1)
func syncByMutex() {
mu.Lock()
data++
mu.Unlock()
}
func syncByChannel() {
ch <- struct{}{}
data++
<-ch
}
// 基准测试函数以 Benchmark 开头
func BenchmarkSectionByMutex(b *testing.B) {
for i := 0; i < b.N; i++ {
syncByMutex()
}
}
// 基准测试函数以 Benchmark 开头
func BenchmarkSectionByChannel(b *testing.B) {
for i := 0; i < b.N; i++ {
syncByChannel()
}
}
运行结果:
PS D:\workspaceGo\src\sync> go test -bench . .\channel_sync_test.go
goos: windows
goarch: amd64
cpu: Intel(R) Core(TM) i5-8300H CPU @ 2.30GHz
BenchmarkSectionByMutex-8 79662760 13.15 ns/op
BenchmarkSectionByChannel-8 27814993 40.38 ns/op
PASS
ok command-line-arguments 2.523s
从运行结果中看,BenchmarkSectionByChannel 测试函数是 40.38 ns/op
,BenchmarkSectionByMutex 是 13.15 ns/op
,很明显 sync包 的的性能更佳。
sync.Mutex
(互斥锁)。在 sync 包中,有这么些注释:
- Values containing the types defined in this package should not be copied. (不应该包含那些包含了此包中类型的值)
- A Mutex must not be copied after first use. (禁止复制首次使用后的 Mutex)
还有其他 sync包 中也有诸如此类注释,那是为什么呢?我们来进行一个小 demo:
package main
import (
"log"
"sync"
"time"
)
// 声明一个结构体 data
type data struct {
n int
sync.Mutex
}
func main() {
// 声明一个结构体对象 d
d := data{n: 100}
// 启动一个线程进行加锁操作
go func(d data) {
for {
log.Println("go 2 try to lock...")
d.Lock()
log.Println("go 2 locked ok...")
time.Sleep(3 * time.Second)
d.Unlock()
log.Println("go 2 unlock ok...")
}
}(d)
d.Lock()
log.Println("go main lock ok...")
// 在 Mutex 首次使用后复制值
go func(d data) {
log.Println("go 1 try lock...")
d.Lock()
log.Println("go 1 locked ok...")
time.Sleep(3 * time.Second)
d.Unlock()
log.Println("go 1 unlock ok...")
}(d)
time.Sleep(1000 * time.Second)
d.Unlock()
log.Println("go main unlock ok...")
}
运行结果:
PS D:\workspaceGo\src\sync> go run .\sync.go
2023/11/04 16:57:24 go main lock ok...
2023/11/04 16:57:24 go 2 try to lock...
2023/11/04 16:57:24 go 2 locked ok...
2023/11/04 16:57:24 go 1 try lock...
2023/11/04 16:57:27 go 2 unlock ok...
2023/11/04 16:57:27 go 2 try to lock...
2023/11/04 16:57:27 go 2 locked ok...
2023/11/04 16:57:30 go 2 unlock ok...
2023/11/04 16:57:30 go 2 try to lock...
2023/11/04 16:57:30 go 2 locked ok...
...
在示例中创建了两个 goroutine :go 1 和 go 2,从运行结果中看到 go 1 阻塞在了加锁操作上了,则 go 2 则是按照预期正常运行。go 1 和 go 2 的区别就在于 go 2 是在互斥锁首次使用之前创建的,而 go 1 则是在互斥锁加锁操作并且在锁定状态之后创建的,并且程序在创建 go 1的时候复制了 data 的实例并且使用了这个副本。
我们可以在 $GOROOT/src/sync/mutex.go 源码中看到如下声明语句:
type Mutex struct {
state int32
sema uint32
}
其实 sync.Mutex 的实现很简单,定义了两个字段 state 和 sema。
对Mutex实例的复制即是对两个整型字段的复制。在初始状态下,Mutex 示例处于 Unlocked 状态(state:0,sema:0),上述案例中,go 2 在复制了初始状态的 Mutex 实例,副本的 state 和 sema 均为 0,则与 go 2新定义的 Mutex 无异,则go 2可以继续正常运行。
后续主程序调用了 Lock 方法,Mutex 实例变为 Locked 状态,而此后 go 1 创建是恰恰复制了处于 Locked 状态的实例,那副本实例也是 Locked 状态的,所以 go 1 进去了阻塞状态(也是死锁状态,因为没有任何机会调用 Unlock 了)。
通过本文案例可以直观的看到,sync包中的实例在首次实例化后被复制的副本一旦被使用了将导致不可预期的结果。所以在使用 sync包 的时候,推荐通过 闭包 或者 传递类型实例 的地址或指针的方式进行,这是使用 sync包 需要注意的地方。
现阶段还是对 Go 语言的学习阶段,想必有一些地方考虑的不全面,本文示例全部是亲自手敲代码并且执行通过。
如有问题,还请指教。
评论去告诉我哦!!!一起学习一起进步!!!