go内存模型和channel 探究
在多个线程对同一个变量进行了读写操作的时候,由于不同的goroutine的执行顺序不能确定,可能会为程序带来不可预测的后果。要保证程序的并发安全,需要使用锁机制。
go内存模型确定了在何种条件下一个goroutine中的read操作可以观测到另一个goroutine中的write操作。
对于在不同的goroutine中操作的数据应该用channel保护起来,或者用其他同步机制,比如sync或者sync/atomic包。
func main() {
var m = sync.Mutex{}
var i = 0
m.Lock()
go func() {
// for {
i = 3
m.Unlock()
// }
}()
m.Lock()
fmt.Println(i)
m.Unlock()
fmt.Println("EXIST WITH CODE 0")
}
用channel实现的版本,channel保证对对通道c的写入发生在读取之前,本质上chan也是用锁来实现的。
var c = make(chan int)
var a string
func f() {
a = "hello, world"
<-c
}
func main() {
go f()
c <- 0
print(a)
}
在上面的例子中,如果c是一个缓冲通道,则不能保证,这是由channel的底层实现决定的,从$GOROOT/src/runtime/chan.go中可以看到
type hchan struct {
qcount uint // 队列中的有效数据,等于队列长度代表满
dataqsiz uint // 队列的总长度
buf unsafe.Pointer // 指向堆中的指针
elemsize uint16
closed uint32
elemtype *_type // element type,这个变量决定了单位内存大小
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// 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.
lock mutex
}
下面摘自 Gopher 2017大会上Kavya的PPT
一个Buffer channel 底层是一个hchan结构体,用make创建一个channel的时候,实际上返回的是指向堆中的一个buffer的指针,数据存储在 环形队列 中,等待写入或读出数据的goroutine放在两个队列 recvq 和 sendq 中,还有一个lock来保证并发安全。
有这样两个性质。
- FIFO先进先出(队列的性质)
- 并发安全,(读写都加了排他锁)
- 可以让goroutine 挂起和唤醒
当一个goroutine向channel中写入数据的时候,
- 先给通道加上锁
- 然后数据copy入队列,sendx +1
- 再将这个锁释放。
同样的,一个goroutine从channel中读取数据的时候,
- 为通道加上锁
- 然后从队列中copy出数据, recvx +1
- 将这个锁释放
向一个已经关闭的channel发送数据会引发panic,关闭一个已经关闭的channel也会引发panic。
从一个已经关闭的channel中读取数据不会引发Panic,会读出一个zero value,为了避免死循环,在读出channel时应该判断channel是否关闭。
val, ok := <- ch
if !ok{
log.Println("channel closed")
return
}
fmt.Println(val)
当所有的goroutine都停止或者处于阻塞的时候,主线程还未退出,会引发死锁。
另外,go语言采用自带的调度器来暂停、唤醒goroutine,而系统级别的OS线程始终是运行的。这也是goroutine比系统级线程更加轻量的原因。