首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Go中的并发是困难的

我明白标题可能有些令人困惑,因为一般来说,Go被认为在并发方面有很好的内置支持。然而,我并不认为在Go中编写并发软件是容易的。让我向您展示我是什么意思。

使用全局变量

第一个例子是我们在项目中遇到的问题。直到最近,sarama库(用于Apache Kafka的Go库)中包含了以下代码(位于):

乍一看,这看起来没问题,对吧?如果版本没有在全局设置中,它要么基于构建信息,要么被分配为静态值()。否则,版本将按原样返回。当我们运行这段代码时,它似乎按预期工作。

然而,当并发调用函数时,全局变量可能会被多个goroutine同时访问,导致潜在的数据竞争。这些问题很难跟踪,因为它们只在运行时在恰当的条件下才会发生。

解决方案

这个问题在#2171中得到修复,通过使用,根据文档的解释,它是“执行一次且仅执行一次操作的对象”。这意味着我们可以使用它来设置版本,以便后续对函数的调用将返回结果。修复代码如下所示:

尽管我认为在这种情况下,也可以在不使用包的情况下通过使用函数来设置变量一次来进行修复。由于在Go运行函数后变量不会改变,所以应该是没问题的。

如何预防

您可以在测试期间或在使用时使用data race detector(自Go 1.1起可用)。当它检测到潜在的数据竞争时,它会打印一个警告。为了展示这是如何工作的,我稍微修改了一下代码来触发数据竞争:

现在我们可以使用标志来启用数据竞争检测器来运行它:

正如你所看到的,检测到了数据竞争。如果我们分析输出,可以看到我们同时对变量进行读写操作。这就是我们所说的数据竞争。之所以称为数据竞争,是因为两个goroutine正在"竞争"访问相同的数据。

从sync包中复制结构体

我在GitHub上找到了一些实际的例子,但没有一个足够重要以至于在这里提及。相反,我将基于我制作的一个示例来解释。所以,下面是例子的说明:

结构体包含两个属性:读/写锁和一个字符串。当调用函数时,变量会被复制到栈上(也称为按值传递),包括其字段。这是一个问题,因为sync包的文档中指出:

sync包提供了基本的同步原语,如互斥锁。除了Once和WaitGroup类型外,大多数都是为低级库例程使用的。更高级的同步最好通过通道和通信来完成。

不应复制包含此包中定义的类型的值。

当评估函数时,运行/不会影响结构体中的原始锁,这个锁无效。

解决方案

改用锁的指针。指针会被复制,并指向相同的值。更新后的版本如下所示:

如何预防

使用copylock分析器来在复制包中的类型时显示警告。最简单的方法是在发布代码之前运行。在原始代码上运行这个命令会得到以下输出:

使用 time.After

在GitHub上搜索时,我发现了Hashicorp的Raft实现中的一个pull request,我们可以使用它来演示以下问题。让我们首先展示代码(位于文件中):

这段代码来自方法。语句等待以下情况之一发生:计时器(用于定义超时)、关闭通道或还原操作完成时。看起来很简单,那问题在哪里呢?

函数的工作原理如下:

因此,它只是的简写形式,但它“泄露”了计时器(因为没有调用)。文档对此的说明如下:

After等待持续时间过去,然后在返回的通道上发送当前时间。它等价于NewTimer(d).C。直到计时器触发后,底层计时器才会被垃圾回收器回收。如果效率是一个问题,可以使用NewTimer并在不再需要计时器时调用Timer.Stop。

我真的不明白为什么一个有意“泄露”计时器的函数(可能会导致潜在的长期分配,取决于持续时间)最终出现在标准库中...

解决方案

我们可以手动创建计时器,而不是使用。具体如下所示:

当函数执行完毕时,即使计时器没有触发,它也会被清理。

如何预防

我不会在任何代码库中使用。除了节省一两行代码外,它没有实质性的优势,而且可能会引发很多问题,特别是当它在代码的热点路径中使用时。

结论

使用Go的内置并发支持可以快速编写并发软件。然而,它将确保数据正确同步和正确使用标准库中的工具的责任留给用户。这加上Go的简洁性,使得编写稳定、无bug的并发软件变得困难。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20230530A0ALIK00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券