Golang 中返回含锁对象的风险

一个报错引起的思考。

1.问题

1
2
3
4
func ModelToResponse(user model.User) proto.UserInfoResponse {
// ...
return userInfoRsp
}

在实现以上代码时,Goland 有以下 Warning 信息。

1
Return copies the lock value: type 'proto.UserInfoResponse' contains 'protoimpl.MessageState' contains 'sync.Mutex' which is 'sync.Locker'

虽然不处理可以正常运行,但是本着学习的态度一探究竟。

2.原因

UserInfoResponse.protoimpl.MessageState 包含锁 sync.Mutex。为了保障并发安全,不建议直接通过值传递的方式返回含锁对象,而应该使用指针传递的方式。比如,

1
2
3
4
func ModelToResponse(user model.User) *proto.UserInfoResponse {
// ...
return &userInfoRsp
}

具体原因是:值拷贝过程中,也会复制锁(也就是存在两把锁),对于同一临界资源,两个协程分别使用两把锁起不到互斥作用。

3.分析

下面以一个例来具体分析:S 中含有一个指针和一个锁,使用锁可以保障指针指向内存的并发安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
"fmt"
"sync"
"time"
)

type S struct {
p *int
mutex sync.Mutex
}

func NewS() S {
a := 0
A := S{p: &a}
return A
}

func main() {
A := NewS()
B := A

go func() {
for i := 0; i < 10000; i++ {
A.mutex.Lock()
*A.p++
A.mutex.Unlock()
}
}()

go func() {
for i := 0; i < 10000; i++ {
B.mutex.Lock()
*B.p++
B.mutex.Unlock()
}
}()

time.Sleep(time.Second * 2)
fmt.Println(*A.p, *B.p) // 结果小于 20000
}

这个份代码会报两个 warning,分别是第 17 行 return A 和第 22 行 B := A 两个地方,报错意思是一样的,均不建议使用值传递的方式拷贝带锁的对象。

1
2
line 17:Return copies the lock value: type 'S' contains 'sync.Mutex' which is 'sync.Locker'
line 22:Variable declaration copies a lock value to 'B': type 'S' contains 'sync.Mutex' which is 'sync.Locker'

以上述代码为例,由于是值传递,对象中的锁也会被拷贝,A.MutexB.Mutex 是两个不同的锁,而 A.pB.p 是指向同一块内存,对于这块内存,两把锁无法起到互斥作用。

所以,这块内存的数据 a,在两个 goroutine 中一共进行 20000 次加法之后,结果仍小于 20000。

4.修正

1
2
3
4
5
func NewS() *S {
a := 0
A := S{p: &a}
return &A
}

只需改为指针传递即可。这样 A.MutexB.Mutex 是同一把锁,在并发场景下,对于 a 的加锁是有效的。

最后加法后的结果为 20000。


Golang 中返回含锁对象的风险
https://www.aimtao.net/copy-lock-value/
Posted on
2023-10-06
Licensed under