多线程编程下的坑踩废了吗?如何精准避坑?

多线程编程下的坑踩废了吗?如何精准避坑?

首页枪战射击多线程更新时间:2024-04-26

这里是架构师优雅之道,不聊高深技术,不制造焦虑,踏踏实实聊聊技术不香吗?

上文我们说到多线程编程有人欢喜有人忧,既然多线程有这么多好处,为什么还眉头紧皱呢?

这个世界并不是孤立的,就像人一样,需要与他人进行交流。线程也一样,线程之间也需要情感交流,呸,是信息交互。

这里,我们先澄清两个概念:主内存和工作内存。很简单的。

没有什么高深的概念,简单而言,主内存可以理解成公摊区域,谁都可以走,工作内存相当于我们花了几个钱包的房子,只有主人可以进。

现在可以得出一个结论:线程是不能直接访问其他线程内部数据的,就像我们不能直接闯进别人的家里,这不成土匪了吗。

线程之间的通讯主要有两种方式:通过主内存通讯和通过消息通讯。

这两种方式哪个好哪个差还不知道,不同的编程语言采取的通信模型也不一样。

主内存模型

消息传递模型

程序的运行离不开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