当 Go struct 遇上 Mutex
struct 是我们写 Go 必然会用到的关键字, 不过当 struct 遇上一些比较特殊类型的时候, 你注意过你的程序是否正常吗 ?
一段代码
type URL struct {
Ip string
Port string
mux sync.RWMutex
params url.Values
}
func (c *URL) Clone() URL {
newUrl := URL{}
newUrl.Ip = c.Ip
newUrl.params = url.Values{}
return newUrl
}
这段代码你能看出来问题所在吗 ?
A: 程序正常
B: 编译失败
C: panic
D: 有可能发生 data race
E: 有可能发生死锁
如果你看出来问题在哪里的话, 那我再悄悄告诉你, 这段代码是 github 某 3k star Go 框架的底层核心代码, 那你是不是就觉得这个话题开始有意思了 ?
先说结论
上面那段代码的问题是 sync.RWMutex
引起的. 如果你看过有关 sync 相关类型的介绍或者相关源码时, 在 sync
包里面的所有类型都有句这样的注释: must not be copied after first use
, 可能很多人却并不知道这句话有什么作用, 顶多看到相关介绍时还记得 sync
相关类型的变量不能复制, 可能真正使用 Mutex, WaitGroup, Cond时, 早把这个注释忘的一干二净.
究其原因, 我觉得有下面两点原因:
- 不明白什么叫 sync 类型变量复制
- sync 类型的变量复制了会出现怎样的结果
下面的例子都以 Mutex 来举例
- 最容易看出来的情形
func main() {
var amux sync.Mutex
b := amux
b.Lock()
b.Unlock()
}
其实这种情况一般情况下, 没人这么用. 问题不大, 略过
- 嵌套在 struct 里面, struct 变量间的互相赋值
type URL struct {
Ip string
Port string
mux sync.RWMutex
params url.Values
}
func main() {
var url1 URL
url2 := url1
}
当 struct 嵌套 不可复制 类型时, 就需要开始小心了. 当 struct 嵌套层次过深或者 struct 变量随着值传递对外扩散时, 这个时候就会变得不可控了, 就需要特别小心了.
- struct 类型变量的值传递作为返回值
type URL struct {
Ip string
mux sync.RWMutex
}
func (c *URL) Clone() URL {
newUrl := URL{}
newUrl.Ip = c.Ip
return newUrl
}
- struct 类型变量的值传递作为 receiver
type URL struct {
Ip string
mux sync.RWMutex
}
func (c URL) String() string {
c.paramsLock.Lock()
defer c.paramsLock.Unlock()
buf.WriteString(c.params.Encode())
return buf.String()
}
复制后出现的结果
例子1:
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var age int
type Person struct {
mux sync.Mutex
}
func (p Person) AddAge() {
defer wg.Done()
p.mux.Lock()
age++
defer p.mux.Unlock()
}
func main() {
p1 := Person{
mux: sync.Mutex{},
}
wg.Add(100)
for i := 0; i < 100; i++ {
go p1.AddAge()
}
wg.Wait()
fmt.Println(age)
}
结果: 结果有可能是 100, 也有可能是99....
例子2:
package main
import (
"fmt"
"sync"
)
type Person struct {
mux sync.Mutex
}
func Reduce(p Person) {
fmt.Println("step...", )
p.mux.Lock()
fmt.Println(p)
defer p.mux.Unlock()
fmt.Println("over...")
}
func main() {
var p Person
p.mux.Lock()
go Reduce(p)
p.mux.Unlock()
fmt.Println(111)
for {
}
}
结果: Reduce 协程会死锁.
看到这里我们就能发现, 当 struct 嵌套了 Mutex, 如果以值传递的方式使用时, 有可能造成程序死锁, 有可能需要互斥的变量并不能达到互斥.
所以不管是单独使用 不能复制 类型的变量, 还是嵌套在 struct 里面都不能值传递的方式使用.
不能复制原因
以 Mutex 为例,
type Mutex struct {
state int32
sema uint32
}
我们使用 Mutex 是为了不同 goroutine 之间共享某个变量, 所以需要让这个变量做到能够互斥, 不然该变量就会被互相被覆盖. Mutex 底层是由 state
sema
控制的, 当 Mutex 变量被复制时, Mutex 的 state, sema 当时的状态也被复制走了, 但是由于不同 goroutine 之间的 Mutex 已经不是同一个变量了, 这样就会造成要么某个 goroutine 死锁或者不同 goroutine 共享的变量达不到互斥
struct 如何与 不可复制 的类型一块使用 ?
由上面可以看到不只是 sync 相关类型变量自身不能被复制,而且 sturct 嵌套 不可复制 类型变量时, 同样也不能被复制. 但是如果我将嵌套的不可复制变量改成指针类型变量呢, 是不是就解决了不能复制的问题 ?
type URL struct {
Ip string
mux *sync.RWMutex
}
这样确实解决了上述的不能复制问题. 但也引出了另外一个问题. 众所周知 Go 没有构造函数, 这就导致我们使用 URL 的时候都需要先去初始化 RWMutex, 不然就会造成同样很严重的空指针问题, 这个问题同样很棘手,也许哪个位置就忘了初始化这个 RWMutex.
根据 google groups 的讨论 How to copy a struct which contains a mutex?, 以及我查看了Kubernets 的相关源码(这里只是一个例子, 里面还有很多), 发现大家的观点基本上都是一致的, 都不会去选用 struct 去嵌套指针类型的变量, 由此不建议 struct 去嵌套 不可复制的 的指针类型变量. 最重要的原因: 没有一个工具能去准确的检测空指针.
所以一般情况下, 当 struct 嵌套了 不可复制 类型的变量时, 都需要传递的是 struct 类型变量的指针.
如何防止复制了不该复制的变量呢?
由于 Go 并不提供重载
的功能, 所以并不能做到去重载 struct 的相关的被复制的方法. 但是 Go 的槽点就来了, Go 本身还不提供不能被复制的相关的编译强约束. 这样就有可能导致出现不能被复制的类型被复制过后蒙混过关. 那我们需要怎么做呢 ?
Go 提供了另外一个工具 go vet
来做补充, 用这个工具是能检测出来不可复制的类型是否被复制过.
func main() {
var amux sync.Mutex
b := amux
b.Lock()
b.Unlock()
}
$ go vet main.go
# command-line-arguments
./main.go:7:7: assignment copies lock value to b: sync.Mutex
我们怎么把 go vet 与 日常开发结合起来呢?
- 目前的 Goland, Vscode 都会集成 go vet 的相关功能, 如果你强迫症比较严重的话, 你就能发现有相关提示.
- 把 go vet 与 CI 流程结合起来, 其实更推荐使用
golangci-lint
这个 lint 工具来做 CI
Go 还提供一段 noCopy 的代码, 当你的 struct 有不能被复制的需求的时候, 可以加入这段代码
type noCopy struct{}
// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
这段代码依然是给 go vet 来使用的.
说到这里, 禁止复制不能被复制的变量, 这个明明能在 编译期 就杜绝的事情, 为啥非要搞出来工具来做这个事情呢? 有点想不通.
不可复制的类型有哪些?
Go 提供的不可复制的类型基本上就是 sync 包内的所有类型: atomic.Value
, sync.Mutex
, sync.Cond
, sync.RWMutex
, sync.Map
, sync.Pool
, sync.WaitGroup
.
这些内置的不可被复制的类型当被复制时配合 go vet是能够发现的. 但是下面这种场景你是否遇见过?
package main
import "fmt"
type Books struct {
someImportantData []int
}
func DoSomething(otherBook Books) Books {
newBook := otherBook
// do something
for k := range newBook.someImportantData {
newBook.someImportantData[k]++ // just like this
}
return otherBook
}
func main() {
oldBook := Books{
someImportantData: make([]int, 0, 100),
}
oldBook.someImportantData = append(oldBook.someImportantData, 1, 2, 3)
fmt.Println("before DoSomething, old book:", oldBook.someImportantData)
DoSomething(oldBook)
fmt.Println("after DoSomething, old book:", oldBook.someImportantData)
// 使用oldBook.someImportantData 继续做某些事情
}
结果:
before DoSomething, old book: [1 2 3]
after DoSomething, old book: [2 3 4]
这个场景其实我们可能不经意间就会遇到. oldBook 是我们要操作的数据, 但是通过 DoSomething` 后, oldBook.someImportantData 的值可能就被改掉了, 这可能并不是我们所期待的. 由于 DoSomething 是通过复制传递的, 可能我们并不能很敏感关注到这个点, 导致程序继续往下走逻辑可能就错了. 我们是不是可以设置 Books 为不可复制呢 ? 这样可以让 go vet 帮助我们发现这些问题
最后的最后
你是否这样初始化过 WaitGroup ?
wg := sync.WaitGroup{}
这个算不算是被复制了呢, 欢迎留言讨论.
当 Go struct 遇上 Mutex的更多相关文章
- Go死锁——当Channel遇上Mutex时
背景 用metux lock for循环,在for循环中又 向带缓冲的Channel 写数据时,千万要小心死锁! 最近,我在测试ws长链接网关,平均一个星期会遇到一次服务假死问题,因为并不是所有rou ...
- 有奖试读&征文--当青春遇上互联网,是否能点燃你的创业梦
时至今日,互联网已经切入我们每一个人的工作.生活和学习的每一个角落.利用互联网这个工具,有人游戏,有人购物,有人上课,有人交友,而有那么一部分人去利用它完毕人生最完美的逆袭.相信每一个人心中都有个创业 ...
- C++-Typedef结构体遇上指针
继Typedef遇上结构体数组后,我们又产生了新的疑问. 上一期地址:https://www.cnblogs.com/lemaden/p/10122929.html 昨天一位朋友又问我了,说结构体数组 ...
- MVC遇上bootstrap后的ajax表单模型验证
MVC遇上bootstrap后的ajax表单验证 使用bootstrap后他由他自带的样式has-error,想要使用它就会比较麻烦,往常使用jqueyr.validate的话只有使用他自己的样式了, ...
- 敏捷遇上UML-需求分析及软件设计最佳实践(郑州站 2014-6-7)
邀请函: 尊敬的阁下:我们将在郑州为您奉献高端知识大餐,当敏捷遇上UML,会发生怎样的化学作用呢?首席专家张老师将会为您分享需求分析及软件设计方面的最佳实践,帮助您掌握敏捷.UML及两者相结合的实 ...
- 敏捷遇上UML—软创基地马年大会(广州站 2014-4-19)
我们将在广州为您奉献高端知识大餐,当敏捷遇上UML,会发生怎样的化学作用呢?首席专家张老师将会为您分享需求分析及软件设计方面的最佳实践,帮助您掌握敏捷.UML及两者相结合的实战技巧. 时间:2 ...
- 敏捷遇上UML——软创基地马年大会(深圳站 2014-3-15)
邀请函: 尊敬的阁下: 我们将在深圳为您奉献高端知识大餐,当敏捷遇上UML,会发生怎样的化学作用呢?首席专家张老师将会为您分享需求分析及软件设计方面的最佳实践,帮助您掌握敏捷.UML及两者相结合的实战 ...
- 初识genymotion安装遇上的VirtualBox问题
想必做过Android开发的都讨厌那慢如蜗牛的 eclipse原生Android模拟器吧! 光是启动这个模拟器都得花上两三分钟,慢慢的用起来手机来调试,但那毕竟不是长久之计,也确实不方便,后来知道了g ...
- SQL SERVER 2008 R2 SP1更新时,遇上共享功能更新失败解决方案
SQL SERVER 2008 R2 SP1更新时,遇上共享功能更新失败的问题,可作如下尝试: 更新失败后,在windows的[事件查看器→应用程序]中找到来源为MsiInstaller,事件ID为1 ...
随机推荐
- Prometheus时序数据库-内存中的存储结构
Prometheus时序数据库-内存中的存储结构 前言 笔者最近担起了公司监控的重任,而当前监控最流行的数据库即是Prometheus.按照笔者打破砂锅问到底的精神,自然要把这个开源组件源码搞明白才行 ...
- java自学第3期——继承、多态、接口、抽象类、final关键字、权限修饰符、内部类
一.继承: 关键字extends /* 定义一个父类:人类 定义父类格式:public class 父类名称{ } 定义子类格式:public class 子类名称 extends 父类名称{ } * ...
- AForge实现拍照
记得先引用DLL private FilterInfoCollection videoDevices; private VideoCaptureDevice videoSource; BLL.AWBL ...
- 基于docker搭建gitlab
一.概述 GitLab是一个利用 Ruby on Rails 开发的开源应用程序,实现一个自托管的Git项目仓库,可通过Web界面进行访问公开的或者私人项目. 它拥有与Github类似的功能,能够浏览 ...
- pyhont+unittest的测试固件
在执行一条自动化测试用例时需要做一些测试前的准备工作和测试后的清理工作,如:创建数据库链接.启动服务进程.打开文件.打开浏览器.测试环境的清理.关闭数据链接.关闭文件等.如果每执行一条用例都需要编写上 ...
- 剑指 Offer 14- I. 剪绳子 + 动态规划 + 数论
剑指 Offer 14- I. 剪绳子 题目链接 还是343. 整数拆分的官方题解写的更清楚 本题说的将绳子剪成m段,m是大于1的任意一个正整数,也就是必须剪这个绳子,至于剪成几段,每一段多长,才能使 ...
- HDOJ-1074(动态规划+状态压缩)
Doing Homework HDOJ-1074 1.本题主要用的是状态压缩的方法,将每种状态用二进制压缩表示 2.状态转移方程:dp[i|(1<<j)]=min(dp[i|(1<& ...
- SEO 在 SPA 站点中的实践
背景 观察基于 create-react-doc 搭建的文档站点, 发现网页代码光秃秃的一片(见下图).这显然是单页应用 (SPA) 站点的通病 -- 不利于文档被搜索引擎搜索 (SEO). 难道 S ...
- 如何安装jenkins并简单的使用
如何安装jenkins并使用 一.jenkins 简介: Jenkins是基于Java开发的一种持续集成工具,用于监控持续重复的工作,功能包括 : 1.持续的软件版本发布/测试项目: 2.监控外部调用 ...
- TensorFlow学习(2)
TensorFlow学习(2) 一.jupyter notebook的安装和使用 1. 什么是jupyter notebook jupyter notebook(http://jupyter.org/ ...