今天小编给大家分享一下golang的block和race怎么解决的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。
一般并发的bug 有两种,死锁(block)和 竞争(race)
死锁发生时,go run 会直接报错
race 发生时,要加race 才会在运行时报warning
go run xxx.go 后面加上 -race 参数
$ go run -race race.go
==================
WARNING: DATA RACE
Write at 0x00c0000a2000 by goroutine 6:
main.main.func2()
/Users/harryhare/git/go_playground/src/race.go:15 +0x38
Previous write at 0x00c0000a2000 by goroutine 5:
main.main.func1()
/Users/harryhare/git/go_playground/src/race.go:9 +0x38
Goroutine 6 (running) created at:
main.main()
/Users/harryhare/git/go_playground/src/race.go:13 +0x9c
Goroutine 5 (running) created at:
main.main()
/Users/harryhare/git/go_playground/src/race.go:7 +0x7a
package main
import "time"
func main(){
var x int
go func(){
for{
x=1
}
}()
go func(){
for{
x=2
}
}()
time.Sleep(100*time.Second)
}
这个命令输出了Warning,告诉我们,goroutine5运行到第11行和main goroutine运行到13行的时候触发竞争了。
而且goroutine5是在第12行的时候产生的。
形成条件
一般情况下是由于在没有加锁的情况下多个协程进行操作对同一个变量操作形成竞争条件.
解决方式
方式1:使用互斥锁sync.Mutex
方式2:使用管道
使用管道的效率要比互斥锁高,也符合Go语言的设计思想.
在写如果检测race之前,首先明白第一个问题,什么是race?
当多个goroutine同时在对同一个变量执行读和写冲突的操作时,结果是不能确定的,这就是race。比如goroutine1在读a,goroutine2在写a,如果不能确定goroutine1读到的结果是goroutine2写之前还是写之后的值,就是race了。
var x int
go func() {
v := x
}()
x = 5
上面的代码v的值到底是0,还是5呢?不知道,这段代码存在race。这是比较口头的描述,严谨的形式化的描述,就需要讲Go的内存模型。
Go的内存模型描述的是"在一个groutine中对变量进行读操作能够侦测到在其他goroutine中对该变量的写操作"的条件。
假设A和B表示一个多线程的程序执行的两个操作。如果A happens-before B,那么A操作对内存的影响 将对执行B的线程(且执行B之前)可见。
有了happens before这么形式化的描述之后,是否有race,等价于对于同一块内存访问,是否有存在无法判断happens before的冲突操作。即是说:
对于前面那段代码,v := x和x = 5两个操作访问了同一块内存x,并且没有任何保证v := x是happens before x = 5的,所以这段代码有race。
那么"实现race dectect"这个问题,就转化成了"happens before事件的检测问题"。
如何检测到happens before事件呢?
我们可以把"哪个线程id,在什么时间,访问哪块内存,是读还是写",只要把所有内存访问的事件都记录下来,然后遍历,验证这些操作之间的先后顺序。一旦发现,比如,读和写两条操作记录,无法满足读happens before写,就是检测到race了。
但是要记录所有的内存访问操作,看起来代价似乎有点吓人。其实只是记录可能会被并发访问的变量,并不是所有变量,下里的g是局部变量,就不需要记录了。
func f() {
g := 3
}
但是代价似乎还是很大?确实。好吧,会慢10倍还是100倍我不确定,反正线上代码是不会开race跑的。既然Go都已经做了,肯定是能做的。
需要有两部分,在Go里面-race编译选项会做相应的处理。编译部分需要在涉及到内存访问的地方插入指令来记录事件;运行时则是检测事件之间的happens before。
一条内存访问事件可以用8个字节来记录:16位线程id,42位时间戳,5位记内存位置,1位标记是读还是写。
线程id不用解释,读写标记也不用解释。时间戳是逻辑时钟,不是每次取真实时间。
只用5位如何记录内存位置呢?这里就有点技巧了,Go的内存管理也用到了同样的技巧。对于实际使用的一块内存区域,映射另一块"影子"内存区域,映射出来的是真实的"影子"。
比如有一个数组A[1000],它的"影子"是B[1000]。A[i]里面发生了什么事件,只在记录在B[i]里面就行了。注意两者大小不需要是一样的,比如
int A[1000]; // 真实使用的数组
char B[1000]; // 用于记录发生在A数组里面操作,如果只记读/写1位足已,记其它也不一定用到8位
同理,对于实际使用的内存区域是[0x7fffffffffff 0x7f0000000000],它的"影子"区域可以是[0x1fffffffffff 0x180000000000],5位可以表示64个单元,如果实际使用的内存使用按8字节对齐之后,是足够表示一组的。
好像有点说不明白,这么解释吧:3位可以表示8个单元的状态,对吧?2的3次方等于8
A[8个8字节的单元] => B[3位]
A里面是否发生了读或者写的操作,在B里面用位的0或1记录来下。说明只用少量内存就可以记录大量事件!
回到事件的记录格式,一条记录占8个字节,其中有5位记录内存位置。5位是可以记录64个8字节的,也就是race dectect的空间开销是使用的内存的1/8(其实不是,因为对同一内存的事件,要记录一组)。
看个例子,我们记录下了第一条事件,线程T1,在E1时间戳,访问内存区域[0 2],执行写操作:
(T1,E1,0:2,W)
第二条事件,线程T2,在E2时间戳,读内存区域[4 8]:
(T2,E2,4:8,R)
因为位置没有交集,所以没有冲突。
第三条事件,线程T3,在E3时间戳,读内存区域[0 4]:
(T3,E3,0:4,R)
这个区域是跟第一个事件的区域有交集的,那么假设E1无法满足happens before E3,那么就检测到冲突了。
type hchan struct {
qcount uint // total data in the queue 当前队列中的数据的个数
dataqsiz uint // size of the circular queue channel环形队列的大小
buf unsafe.Pointer // points to an array of dataqsiz elements 存放数据的环形队列的指针
elemsize uint16 // channel 中存放的数据类型的大小|即每个元素的大小
closed uint32 // channel 是否关闭的标示
elemtype *_type // element type channel中存放的元素的类型
sendx uint // send index 当前发送元素指向channel环形队列的下标指针
recvx uint // receive index 当前接收元素指向channel环形队列的下标指针
recvq waitq // list of recv waiters 等待接收元素的goroutine队列
sendq waitq // list of send waiters 等待发送元素的goroutine队列
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
// 保持此锁定时不要更改另一个G的状态(特别是,没有准备好G),因为这可能会因堆栈收缩而死锁。
lock mutex
}
简单说明:
buf
是有缓冲的channel所特有的结构,用来存储缓存数据。是个循环链表
sendx
和recvx
用于记录buf
这个循环链表中的~发送或者接收的~index
lock
是个互斥锁。
recvq
和sendq
分别是接收(<-channel)或者发送(channel <- xxx)的goroutine抽象出来的结构体(sudog)的队列。是个双向链表
源码位于/runtime/chan.go
中(目前版本:1.11)。
创建channel实际上就是在内存中实例化了一个hchan
的结构体,并返回一个ch指针,我们使用过程中channel在函数之间的传递都是用的这个指针,这就是为什么函数传递中无需使用channel的指针,而直接用channel就行了,因为channel本身就是一个指针。
先考虑一个问题,如果你想让goroutine以先进先出(FIFO)的方式进入一个结构体中,你会怎么操作?加锁!对的!channel就是用了一个锁。hchan本身包含一个互斥锁mutex
以上就是“golang的block和race怎么解决”这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注亿速云行业资讯频道。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。