为什么并发性很难

2020-11-10 07:38:19

编者按:对于许多开发人员来说,并发性是最难掌握的概念之一,但在现代软件开发中,它是一个需要掌握的重要概念。凯瑟琳·考克斯-布代(Katherine Cox-Buday)在她的《围棋中的并发性》(Conency In Go)一书的第一章中,讨论了并发编程中最常见的问题之一:竞争条件。

并发代码是出了名的难以正确编写。通常需要几次迭代才能使其按预期工作,即便如此,在时间发生变化(更高的磁盘利用率、更多用户登录到系统等)之前,错误在代码中存在多年的情况并不少见。导致一个以前未被发现的虫子抬起头来。事实上,对于这本书,我已经尽可能多地关注代码,试图缓解这种情况。

幸运的是,每个人在使用并发代码时都会遇到相同的问题。正因为如此,计算机科学家已经能够将常见问题归类,这使我们能够讨论它们是如何产生的、为什么以及如何解决它们。

当两个或多个操作必须以正确的顺序执行,但尚未编写程序以保证保持此顺序时,就会出现争用情况。

大多数情况下,这表现在所谓的数据竞争中,即一个并发操作试图读取一个变量,而在某个不确定的时间,另一个并发操作试图写入同一变量。

1var data int 2go func(){//在go中,您可以使用go关键字并发运行//函数。这样做会产生//所谓的大猩猩。3数据++4}()5如果数据==0{6 fmt.Printf(";值为%v\n";,data)7}。

在这里,第3行和第5行都试图访问变量数据,但不能保证访问的顺序。运行此代码有三种可能的结果:

打印“the value is 0”(值为0)。在本例中,第5行和第6行在第3行之前执行。

打印“Value is 1”(值为1)。在本例中,第5行在第3行之前执行,但第3行在第6行之前执行。

正如您所看到的,仅仅几行不正确的代码就会给您的程序带来巨大的可变性。

大多数情况下,引入数据竞争是因为开发人员在按顺序考虑问题。他们认为,因为一行代码先落在另一行代码之前,所以它会先运行。它们假定上面的goroutine将在IF语句中读取DATA变量之前调度和执行。

在编写并发代码时,您必须仔细地迭代可能的场景。除非您正在使用我们将在本书后面介绍的一些技术,否则您无法保证您的代码将按照源代码中列出的顺序运行。我有时觉得想象两次手术之间有一段很长的时间是有帮助的。想象一下,从调用goroutine到运行Goroutine的时间间隔为一个小时。该程序的其余部分将如何运行?如果从Goroutine成功执行到程序到达if语句需要一个小时呢?这样想对我有帮助,因为对计算机来说,刻度可能不同,但相对时差大致相同。

的确,一些开发人员陷入了在代码中散布休眠的陷阱,因为这似乎解决了他们的并发性问题。让我们在前面的程序中尝试一下:

1var data int 2 go func(){data++}()3次。睡眠(1*时间.秒)//这是错误的!4如果data==0{5 fmt.Printf(";值为%v\n";data)6}。

我们的数据竞赛解决了吗?不是的。事实上,这三种结果仍然有可能从这个项目中产生,只是可能性越来越小。我们在调用Goroutine和检查数据值之间停留的时间越长,我们的程序就越接近实现正确性-但这个概率逐渐接近逻辑正确性;它永远不会在逻辑上正确。

除此之外,我们现在还在我们的算法中引入了一个低效。我们现在不得不睡一秒钟,这样我们就更有可能看不到我们的数据竞赛了。如果我们使用了正确的工具,我们可能根本不必等待,或者等待可能只有一微秒。

这里的要点是,你应该始终以逻辑正确性为目标。在代码中引入休眠可能是调试并发程序的一种便捷方式,但它们不是解决方案。

争用条件是最隐蔽的并发错误类型之一,因为它们可能要在代码投入生产数年后才会出现。它们通常是由代码执行环境的变化或史无前例的发生引起的。在这些情况下,代码看起来运行正常,但实际上,操作按顺序执行的可能性很高。该计划迟早会产生意想不到的后果。

加入O‘Reilly在线学习平台。今天就可以免费试用,快速找到答案,或者掌握一些新的有用的东西。

凯瑟琳是一名计算机科学家,目前在DigitalOciean工作。她的爱好包括软件工程,创意写作,围棋(围棋,围棋,围棋)和音乐,所有这些她都断断续续地追求,并有不同程度的奉献。