使用通信来共享内存,而不是通过共享内存来通信

所有go语言的学习者都会看到这样一句话“使用通信来共享内存,而不是通过共享内存来通信”,这是go语言并发编程的座右铭,然而却不那么好理解。

为了搞清楚熟悉的锁模式并发编程和go的channel模式并发编程的区别,先分别看一下这两种模式都是怎么做的:

为了行文简洁,暂时把代码执行单元都称为“线程”,在go语言中都是go routine。线程和go routine的关系涉及go 运行时的实现,已经超出了本文讨论范围。

在锁模式中,一块内存可以被多个线程同时看到,所以叫共享内存。线程之间通过改变内存中的数据来通知其他线程发生了什么,所以是通过共享内存来通信。锁是为了保护一个线程对内存操作的逻辑完整性而引入的一种约定,注意是一种约定而不是规则(一个线程可以不获取锁就操作内存,也可以解锁其他线程加的锁从而破坏保护,这种错误很难发现)。这种约定要每个线程的编写人员自觉遵守,否则就会出现多线程问题,如数据被破坏,死锁,饥饿等。

在go模式中,一块内存同一时间只能被一个线程看到,另外一个线程要操作这块内存,需要当前线程让渡所有权,这个所有权的让渡过程是“通信”。通信的原子性由channel封装好了,内存同一时间只能被同一线程使用,所以这种模式下不需要显示的锁。然而go模式也有约定,如果传递的是内存的指针,或者是控制消息,还是等于共享了内存,还是要保证将所有权让渡后, 不能再操作这块内存,文末举了一个例子。在实际编写代码的时候,按照go模式,一般内存块使用的是局部变量,很难有机会在让渡所有权以后再操作同一块内存。

如果你发现需要持有让渡所有权的内存块,很可能说明设计有问题,可以尝试考虑在这种场景下,锁模式是不是更合适。

go模式在实际使用中的问题在于,如果没有恰当的设计,在多级多对多的内存所有权转让过程中,会使用很多channel,编写代码的人要管理这些channel。还有,转让出去的内存追踪起来比较困难。

总之并不是go语言狂热者吹捧的那样,一些并发都要使用channel,锁模式和channel模式各有适用的场景。

回到题中那句话,“使用通信来共享内存,而不是通过共享内存来通信”。这句话中前后两个“通信”的意思是不同的,这可能是难以理解的原因之一。本质上,两种模式谋求的都是同一时刻只有一个线程在操作同一块内存,以保护逻辑的原子性。后者通用引入所有权概念,channel和临时变量,简化了操作“约定”。

使用channel一样要遵守一些约定,一个约定就是将内存的所有权交出去后,无论是通过内存指针,还是消息控制,就不能再操作这块内存。否则一样会引入多线程问题,看下边代码。

package main

import (
	"fmt"
	"time"
)

func main() {

	output := make(chan []int)
	stop := make(chan struct{})

	go func() {
		arr := []int{1, 2, 3}
		output <- arr

		time.Sleep(time.Second * 2)

		arr[1] = 5
		stop <- struct{}{}
	}()

	outArr := <-output
	fmt.Printf("%v\n", outArr)

	outArr[1] = 10
	fmt.Printf("%v\n", outArr)

	time.Sleep(time.Second * 2)
	fmt.Printf("%v\n", outArr)

	<-stop
}

对于主线程而言,将outArr[1]设置为10后,两次读的结果不同,数据被破坏。

你可能感兴趣的:(go语言,后端)