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
go内存模型和channel 探究
一个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比系统级线程更加轻量的原因。