这里是架构师优雅之道,不聊高深技术,不制造焦虑,踏踏实实聊聊技术不香吗?
上文我们说到多线程编程有人欢喜有人忧,既然多线程有这么多好处,为什么还眉头紧皱呢?
这个世界并不是孤立的,就像人一样,需要与他人进行交流。线程也一样,线程之间也需要情感交流,呸,是信息交互。
这里,我们先澄清两个概念:主内存和工作内存。很简单的。
没有什么高深的概念,简单而言,主内存可以理解成公摊区域,谁都可以走,工作内存相当于我们花了几个钱包的房子,只有主人可以进。
现在可以得出一个结论:线程是不能直接访问其他线程内部数据的,就像我们不能直接闯进别人的家里,这不成土匪了吗。
线程之间的通讯主要有两种方式:通过主内存通讯和通过消息通讯。
这两种方式哪个好哪个差还不知道,不同的编程语言采取的通信模型也不一样。
主内存模型
消息传递模型
程序的运行离不开CPU,CPU就像我们的大脑,是个非常聪明的家伙,在单线程环境下,他会严格按照定义的程序执行,而在多线程环境下,就不一定了。这就是CPU指令重排序。
CPU指令重排序:是现代CPU为了提高执行效率的一种优化手段。指CPU在处理指令的时候,可能会对指令的顺序进行重新排序,以便更好的利用资源,提高指令执行的效率。
简单的说,同样的一段代码,单线程和多线程环境下的执行结果可能会不一样,像极了这个世界的反复无常。
以下面代码为例
var x int
var wg sync.WaitGroup
func writer() {
x = 1// 写操作
wg.Done()
}
func reader() {
for x == 0 { // 读操作
}
fmt.Println("x = ", x)
wg.Done()
}
func main() {
for i := 0; i < 1000; i {
x = 0// 初始化共享变量
wg.Add(2)
go writer()
go reader()
wg.Wait()
}
}
上面代码很简单,分别创建了两个Goroutine。一个负责写,一个负责读。还定义了一个共享变量x。我们期望输出x = 1。CPU指令重排序的存在,可能会导致读操作在写操作之前执行,从而造成读取到的值为0,而不是预期的1。
修复上面代码也非常简单,可以通过互斥锁解决。
var x int
var wg sync.WaitGroup
var lock sync.Mutex
func writer() {
lock.Lock()
x = 1// 写操作
lock.Unlock()
wg.Done()
}
func reader() {
lock.Lock()
val := x // 读操作
lock.Unlock()
fmt.Println("x =", val)
wg.Done()
}
func main() {
for i := 0; i < 1000; i {
x = 0// 初始化共享变量
wg.Add(2)
go writer()
go reader()
wg.Wait()
}
}
这样就能保证先写后读了。
然而,在实际工作中,由于CPU指令的重排序,解决并不简单,甚至是很多异常情况非常隐蔽,这就要求在多线程环境下,如果要对共享资源进行操作,一定要采取合适的同步策略来规避CPU的指令重排序。
当然,上面的例子也可以改成channel发送消息的方式
func writer(ch chan<- int) {
ch <- 1// 写操作
}
func reader(ch <-chan int) {
val := <-ch // 读操作
fmt.Println("x =", val)
}
func main() {
for i := 0; i < 1000; i {
ch := make(chanint)
go writer(ch)
go reader(ch)
}
}
上面代码利用了channel的阻塞性,(在完成写操作之前,会保证读操作阻塞,保证了写操作一定先于读操作)。
本文我们以Golang语言为例,介绍了关于共享资源操作的正确方式,那么多线程下对共享资源的操作还有没有其他方式呢?
我们下次聊!
如果本文对你有用,点个赞吧!
Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved