goroutine之间如何正确的共享变量,顺序一致性内存模型,happens before

你是否也在多个协程之间共享一个变量,并试图做一些流程的控制。比如一个后台程序,希望它能够做到安全退出,于是开启了一个协程监控信号,如果收到了信号就修改标志变量,在主程序中通过判断这个变量然后自动退出。然而会发现,这并不会一定生效,也就是说,其他goroutine中有可能永远也读不到标志变量的改变。

这让我不禁想起来了一句经典的话:不要通过共享内存来通信,要通过通信来共享内存。

所以,面对上面的场景,改用通道来实现。

var stopSignalChan = make(chan struct{})

func StartSignalHandler() {
	var sig os.Signal
	var hookableSignals = []os.Signal{
		syscall.SIGHUP,
	}

	sigChan := make(chan os.Signal, 1)

	signal.Notify(
		sigChan,
		hookableSignals...,
	)

	pid := syscall.Getpid()
	for {
		sig = <-sigChan
		switch sig {
		case syscall.SIGHUP:
			close(stopSignalChan)
		default:
		}
	}
}

func CheckIfNeedStop() bool {
	select {
	case <-stopSignalChan:
		return true
	default:
		return false
	}
}

Happens Before

在Go语言中,同一个Goroutine线程内部,顺序一致性内存模型是得到保证的。但是不同的Goroutine之间,并不满足顺序一致性内存模型,需要通过明确定义的同步事件来作为同步的参考。如果两个事件不可排序,那么就说这两个事件是并发的。为了最大化并行,Go语言的编译器和处理器在不影响上述规定的前提下可能会对执行语句重新排序(CPU也会对一些指令进行乱序执行)。

对于一个goroutine来说,虽然指令会被编译器乱序重排,但它其中变量的读,写操作的执行表现必须和代码得出的预期是一致的。但是在两个不同的goroutine对相同变量操作时,可能因为指令重排导致不同的goroutine对变量的操作顺序的认识变得不一致。为了解决这种二义性问题,Go语言中引进一个happens before的概念,它用于描述对内存操作的先后顺序问题。如果事件e1 happens before 事件 e2,那么事件e2 happens after e1。如果事件e1 does not happen before 事件 e2,并且e1 does not happen after e2,那么事件e1和e2同时发生。

对于一个单一的goroutine,happens before 的顺序和代码的顺序是一致的。

为了保证读事件可以感知对变量V的写事件,我们首先要确保W是变量V的唯一的写事件。同时还要满足以下条件:

  1. 写事件 happens before 读事件。
  2. 其他对变量V的访问必须 happens before 写事件,或者 happens after 读事件。

第二组条件比第一组条件更加严格。因为,它要求在W和R并行执行的程序中不能再有其他的读操作。对于在单一的goroutine中两组条件是等价的,读事件可以确保感知到对变量的写事件。但是,对于在两个goroutines共享变量V,我们必须通过同步事件来保证 happens-before 条件。

channel通讯

用管道通信是两个goroutines之间同步的主要方法。通常的用法是不同的goroutines对同一个管道进行读写操作,一个goroutines写入到管道中,另一个goroutines从管道中读数据。管道上的发送操作发生在管道的接收完成之前(happens before)。

var c = make(chan int, 10)
var a string

func f() {
	a = "hello, world"
	c <- 0
}

func main() {
	go f()
	<-c
	print(a)
}

可以确保会输出"hello, world"。因为,a的赋值发生在向管道 c发送数据之前,而管道的发送操作在管道接收完成之前发生。因此,在print 的时候,a已经被赋值。

var c = make(chan int)
var a string

func f() {
	a = "hello, world"
	<-c
}
func main() {
	go f()
	c <- 0
	print(a)
}

同样可以确保输出“hello, world”。因为,a的赋值在从管道接收数据 前发生,而从管道接收数据操作在向unbuffered 管道发送完成之前发生。所以,在print 的时候,a已经被赋值。如果用的是缓冲管道(如 c = make(chan int, 1) ),将不能保证输出 “hello, world”结果(可能会是空字符串,但肯定不会是未知的字符串, 或导致程序崩溃)。

使用sync来定义执行顺序

var l sync.Mutex
var a string

func f() {
	a = "hello, world"
	l.Unlock()
}

func main() {
	l.Lock()
	go f()
	l.Lock()
	print(a)
}

错误的示例

var a string
var done bool

func setup() {
	a = "hello, world"
	done = true
}

func main() {
	go setup()
	for !done {
	}
	print(a)
}

我们没有办法保证在main中看到了done值被修改的同时也 能看到a被修改,因此程序可能输出空字符串。更坏的结果是,main 函数可能永远不知道done被修改,因为在两个线程之间没有同步操作,这样main 函数永远不能返回。

type T struct {
	msg string
}

var g *T

func setup() {
	t := new(T)
	t.msg = "hello, world"
	g = t
}

func main() {
	go setup()
	for g == nil {
	}
	print(g.msg)
}

即使main观察到了 g != nil 条件并且退出了循环,但是依然不能保证它看到了g.msg的初始化之后的结果。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值