Go中的并发是困难的

news2024/12/23 13:34:44

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

使用全局变量

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

package sarama

import "runtime/debug"

var v string

func version() string {
    if v == "" {
        bi, ok := debug.ReadBuildInfo()
        if ok {
            v = bi.Main.Version
        } else {
            v = "dev"
        }
    }
    return v
}

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

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

解决方案

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

package sarama

import (
    "runtime/debug"
    "sync"
)

var (
    v     string
    vOnce sync.Once
)

func version() string {
    vOnce.Do(func() {
        bi, ok := debug.ReadBuildInfo()
        if ok {
           v = bi.Main.Version
        } else {
           v = "dev"
        }
    })
    return v
}

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

如何预防

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

package main
import (
    "fmt"
    "runtime/debug"
)
var v string
func version() string {
    if v == "" {
        bi, ok := debug.ReadBuildInfo()
        if ok {
            v = bi.Main.Version
        } else {
            v = "dev"
        }
    }
    return v
}
func main() {
    go func() {
        version()
    }()
    fmt.Println(version())
}

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

➜ go run -race .                               
==================
WARNING: DATA RACE
Write at 0x000104a16b90 by main goroutine:
  main.version()
      main.go:14 +0x78
  main.main()
      main.go:27 +0x30Previous read at 0x000104a16b90 by goroutine 7:
  main.version()
      main.go:11 +0x2c
  main.main.func1()
      main.go:24 +0x24Goroutine 7 (finished) created at:
  main.main()
      main.go:23 +0x2c
==================
(devel)
Found 1 data race(s)
exit status 66

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

从sync包中复制结构体

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

package main
import "sync"
type User struct {
    lock sync.RWMutex
    Name string
}
func doSomething(u User) {
    u.lock.RLock()
    defer u.lock.RUnlock()
    // do something with `u`
}
func main() {
    u := User{Name: "John"}
    doSomething(u)
}

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

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

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

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

解决方案

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

type User struct {
    lock *sync.RWMutex
    Name string
}

读锁验证

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

➜ go vet .
# data-synchronization
./main.go:10:20: doSomething passes lock by value: data-synchronization.User contains sync.RWMutex
./main.go:20:14: call of doSomething copies lock value: data-synchronization.User contains sync.RWMutex

使用 time.After

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

var timer <-chan time.Time
if timeout > 0 {
    timer = time.After(timeout)
}

// Perform the restore.
restore := &userRestoreFuture{
    meta:   meta,
    reader: reader,
}
restore.init()
select {
case <-timer:
    return ErrEnqueueTimeout
case <-r.shutdownCh:
    return ErrRaftShutdown
case r.userRestoreCh <- restore:
    // If the restore is ingested then wait for it to complete.
    if err := restore.Error(); err != nil {
        return err
    }
}

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

time.After函数的工作原理如下:

func After(d Duration) <-chan Time {
    return NewTimer(d).C
}

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

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

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

解决方案

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

var timerCh <-chan time.Time
if timeout > 0 {
    timer := time.NewTimer(timeout)
    defer timer.Stop()
    timerCh = timer.C
}
// Perform the restore.
restore := &userRestoreFuture{
    meta:   meta,
    reader: reader,
}
restore.init()
select {
case <-timerCh:
    return ErrEnqueueTimeout
case <-r.shutdownCh:
    return ErrRaftShutdown
case r.userRestoreCh <- restore:
    // If the restore is ingested then wait for it to complete.
    if err := restore.Error(); err != nil {
        return err
    }
}

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

如何预防

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

结论

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

如果你喜欢我的文章,点赞,关注,转发!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/595539.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【致敬未来的攻城狮计划】打卡1:rcsa+keil环境搭建

前言 这回参加的是csdn李肯老师的攻城狮计划&#xff0c;简单说就是我白嫖板子&#xff0c;输出学习笔记。 板子是瑞萨的CPK_RA2E1&#xff0c;还有触摸元件&#xff0c;看起来很有意思hh。 环境搭建 一开始决定采取vscode搭建的方式。后期进行到最后一步——cmake build的时…

SQL-计算留存率cohort

目录 1、留存率cohort介绍及其业务价值 2、计算思路 3、实操 3.1、日对日留存cohort 3.2、周对周留存cohort 3.3、月对月留存cohort 1、留存率cohort介绍及其业务价值 留存率cohort也叫做同期群留存分析&#xff0c;将同一时间范围内的用户分为一组&#xff0c;计算这批…

Linux命令(26)之uptime

Linux命令之uptime 1.uptime介绍 linux命令uptime是用来为用户提供系统从开启到当前运行uptime命令时系统已运行的时长信息&#xff0c;除此之外&#xff0c;还提了系统启动时间&#xff0c;当前登录用户&#xff0c;系统平均负载信息。 2.uptime用法 uptime [参数] uptime…

华为OD机试真题(Java),四则运算(100%通过+复盘思路)

一、题目描述 输入一个表达式&#xff08;用字符串表示&#xff09;&#xff0c;求这个表达式的值。 保证字符串中的有效字符包括[‘0’-‘9’],‘’,‘-’, ‘*’,‘/’ ,‘(’&#xff0c; ‘)’,‘[’, ‘]’,‘{’ ,‘}’。且表达式一定合法。 数据范围&#xff1a;表达…

gitlab记录

1、docker方式部署启动 参考文档&#xff1a; https://blog.csdn.net/weixin_53443677/article/details/125518696 https://blog.csdn.net/weixin_39034012/article/details/119211630 1.1、docker启动gitlab 前期准备 > # 拉镜像 > docker pull gitlab/gitlab-ce:late…

chatgpt赋能python:Python代码30行:提高网站SEO的最佳实践

Python 代码 30 行&#xff1a;提高网站 SEO 的最佳实践 搜索引擎优化&#xff08;SEO&#xff09;是网站成功的重要因素&#xff0c;它可以让网站排名更高并吸引更多的流量。Python 代码可以帮助您实现最佳的 SEO 实践&#xff0c;并提高网站的可见性和排名。下面是一个包含 …

Tugraph的设计和源码初步解析

1. Tugraph Tugraph是一款开源的性能优秀的图数据库&#xff0c;该图数据库使用多版本的BTree作为数据的存储引擎&#xff0c;同时设置了点边数据在这个存储引擎上的布局&#xff08;主要考虑数据的局部性&#xff09;&#xff0c;从而达到高性能查询的目的。本文主要从Tugrap…

ubuntu系统登录密码重置方法

公司搬家&#xff0c;需要更改git服务器地址&#xff0c;发现服务器密码也忘记了&#xff0c;折腾了下&#xff0c;通过如下方法修改成功。 一、重启计算机并进入GRUB菜单&#xff08;如果您的计算机没有显示GRUB菜单&#xff0c;请尝试按住Shift键或Esc键&#xff0c;直到出现…

手机安卓Termux搭建Hexo博客网站,并发布公网访问。

文章目录 1. 安装 Hexo2. 安装cpolar内网穿透3. 公网远程访问4. 固定公网地址 转载自cpolar极点云的文章&#xff1a;安卓手机使用Termux搭建Hexo个人博客网站【内网穿透公网访问】 Hexo 是一个用 Nodejs 编写的快速、简洁且高效的博客框架。Hexo 使用 Markdown 解析文章&#…

DAY04_JDBC快速入门JDBC API详解SQL防注入数据库连接池JDBC综合练习

目录 1 JDBC1.1 JDBC概念1.2 JDBC本质 1.3 JDBC好处 2 JDBC快速入门2.1 编写代码步骤2.2 具体操作 3 JDBC API详解3.1 DriverManager3.2 Connection3.2.1 获取执行对象3.2.2 事务管理 3.3 Statement3.4 ResultSet3.4.1 ResultSet案例 3.5 PreparedStatement3.5.1 SQL注入3.5.2 …

基于opencv实现两路yuv数据拼接合成一张大图

背景 实时音视频通话&#xff08;RTC&#xff09;越来越注重安全审核&#xff0c;特别是在1v1娱乐社交场景中&#xff0c;对于视频反垃圾的需求也越来越大。随之而来的是客户对审核成本降低的诉求日益强烈。针对1v1场景&#xff0c;将两路视频拼接成一张图片进行审核相比于分别…

大数据Doris(三十一):Broker Load导入HDFS json格式数据和注意事项

文章目录 Broker Load导入HDFS json格式数据和注意事项 一、导入HDFS json格式数据 1、创建Doris表

nginx(八十一)rewrite模块指令再探之(三)重定向

一 return和rewrite重定向再探 ① 前言 多种重定向跳转方式的差异 nginx与Location响应头细节探讨 本为不涉及讨论如下的绝对重定向1) return 301 http://www.wzj.com:6443/url?namewzj2) rewrite ... http://www.wzj.com:6443/url 2) rewrite ... http://www.wzj.com:64…

一分钟学一个 Linux 命令 - pwd

前言 大家好&#xff0c;我是 god23bin。欢迎大家继续围观《一分钟学一个 Linux 命令》&#xff0c;每天只需一分钟&#xff0c;记住一个 Linux 命令不成问题。本篇文章将聚焦于 pwd 命令&#xff0c;一个超级简单又常用的命令。在接下来的内容中&#xff0c;我将快速介绍 pwd…

Elasticsearch总结

详细描述一下 Elasticsearch 搜索的过程&#xff1f; 1、搜索被执行成一个两阶段过程&#xff0c;我们称之为 Query Then Fetch&#xff1b; 2、在初始查询阶段时&#xff0c;查询会广播到索引中每一个分片拷贝&#xff08;主分片或者副本分片&#xff09;。 每个分片在本地执…

chatgpt赋能python:使用Python关闭所有子进程

使用Python关闭所有子进程 如果您使用Python编写了多进程应用程序&#xff0c;那么您可能会遇到一些关闭所有子进程的问题。这种情况可能是您的主进程已经完成了&#xff0c;但是子进程却没有关闭&#xff0c;从而导致资源浪费和程序崩溃。在这篇文章中&#xff0c;我们将讨论…

STM32F1xx -- Systick 系统滴答定时器

1. SysTick 是一个向 CPU 提供定时中断信号的计数器&#xff0c;其计数速率是由 Cortex-M 系列处理器的系统时钟频率和 SysTick 计数器的重载值共同决定的。 1.1 Systick 时钟来源之一&#xff0c;Systick 一般设置为1ms 中断一次&#xff0c;为系统任务调度提供服务&#xff…

R语言:集卡活动概率测算模拟

背景&#xff1a;以支付宝集五福活动为代表的集卡类营销活动背后&#xff0c;每张卡出现的概率测算是非常重要的&#xff0c;假设我们可以预估有多少人参与活动以及大致每人能抽多少次&#xff0c;且限定一共有多少人能够集齐&#xff0c;在这些限定条件下&#xff0c;每张卡出…

CentOS 系统上安装 Jenkins

#######################注意我这里安装jenkins版本要求实际是要安装jdk11版本的~~~我一开始弄错了 您可以按照以下步骤在 CentOS 上安装 JDK&#xff1a; 1. 首先&#xff0c;打开终端并使用 yum 命令更新系统软件包列表。输入以下命令来执行此操作&#xff1a; sudo yu…

chatgpt赋能python:Python为什么闪退?

Python为什么闪退&#xff1f; Python作为一种高级编程语言&#xff0c;已经赢得了世界各地许多开发者的青睐。但是&#xff0c;有时候Python会因为各种原因而突然闪退&#xff0c;给开发者带来极大的困扰。那么&#xff0c;Python为什么会闪退呢&#xff1f; 1. 内存泄漏 内…