15 go语言(golang) - 并发编程goroutine原理及数据安全

底层原理

Go 的 goroutine 是一种轻量级的线程实现,允许我们在程序中并发地执行函数。与传统的操作系统线程相比,goroutine 更加高效和易于使用。

轻量级调度

  • 用户态调度:Go 运行时提供了自己的调度器,这意味着 goroutine 的创建和切换是在用户空间完成的,而不需要操作系统内核参与。这使得 goroutine 切换比传统线程更快。

  • M:N 模型:Go 使用 M:N 调度模型,将 M 个 goroutines 映射到 N 个操作系统线程上。这种方式可以有效利用多核 CPU,同时保持每个 OS 线程上的多个并发任务。

栈管理

  • 动态栈大小:goroutines 从一个很小的栈开始(大约几 KB),而不是像传统线程那样分配较大的固定栈(通常为 MB)。当需要更多空间时,goroutine 的栈会自动增长,并在不再需要时收缩。

  • 避免内存浪费:这种动态调整机制避免了大量未使用内存的浪费,使得同时运行大量 goroutines 成为可能。

Goroutine 调度器

  • Goroutines、OS Threads 和 Processors ( P )

    • G 表示 Goroutine。
    • M 表示 Machine,对应实际的 OS Thread。
    • P 表示 Processor,是 Go runtime 中用于执行 Go code 的抽象概念。每个 P 持有一个本地 run queue 来管理待执行的 Gs。
  • 在 Go 程序启动时,会根据可用 CPU 核心数创建对应数量的 P,每个 P 会绑定到一个 M 上来执行 Gs。当某个 G 阻塞或完成后,P 可以将其切换出去并从队列中选择下一个 G 执行。

  • G 是具体要完成工作的单元,M 是实际执行工作的实体,而 P 则是提供环境和条件以便高效调度这些工作的抽象层。

Goroutine ( G )

  • 定义:每个 goroutine 是一个独立执行的函数,类似于轻量级线程。
  • 结构:在实现中,goroutine 是一个包含栈、程序计数器和其他调度信息的数据结构。
  • 栈管理:goroutines 使用动态栈,可以从很小(几 KB)开始,并根据需要增长和收缩。这种灵活性允许同时运行大量 goroutines,而不会浪费内存。

Machine ( M )

  • 定义M 表示 Machine,是与操作系统线程直接对应的实体。
  • 职责
    • 执行分配给它们的 G
    • 管理与操作系统交互,如进行系统调用时阻塞或唤醒 OS 线程。

Processor ( P )

  • 定义P 表示 Processor,是 Go runtime 中用于执行 Go code 的抽象概念。

  • 数量控制

    • 在程序启动时,Go runtime 会创建若干个 P,默认数量等于机器上的 CPU 核心数,但可以通过 runtime.GOMAXPROCS(n) 函数调整。
  • 职责

    • 每个 P 持有一个本地 run queue,用来存储待执行的 Gs(goroutines)。
    • 调度器会从这些队列中选择 G 并将其分配给 M 执行。

调度机制

  1. 工作窃取算法(Work Stealing)

    • 每个 P 都有自己的本地队列,当某个 P 的队列为空时,它可以从其他 P 那里“窃取”一些任务来执行。这种机制提高了负载均衡,有助于充分利用多核处理器资源。
  2. 全局运行队列

    • 除了每个 P 的本地队列外,还有一个全局运行队列。当新建 G 或者当某些情况下无法放入本地队列时,会被放入全局运行队列。空闲 M 可以从这里获取新的任务以保持忙碌状态。
  3. 抢占式调度

    • Goroutines 是抢占式调度,这意味着长时间运行且不释放 CPU 的 Goroutine 可以被暂停,以便让其他 Goroutines 获得执行机会。这避免了一些 Goroutines 独占 CPU 而导致其他任务饥饿的问题。
  4. 阻塞处理

    • 当 G 被阻塞(例如等待 I/O 操作),M 会将该 G 挂起并尝试获取另一个可用 G 来继续工作。如果没有可用 G,则可能会休眠或终止该 M,以节省资源。

调度流程

在这里插入图片描述

  1. 获取 P

    • 当一个线程 M 想要运行时,它必须首先获取一个 P。P 是 CPU 的抽象,限制了并发运行的最大数量(通常等于 GOMAXPROCS)。
  2. 从本地队列获取 G

    • 一旦 M 和 P 关联,M 会尝试从其关联的 P 的本地队列(LRQ)中获取下一个待执行的 G。如果找到了,则直接开始执行。
  3. 全局队列检查

    • 如果本地队列没有可用的 G,M 会查看全局队列(GRQ)。全局队列是所有空闲或等待分配到某个 P 上运行的 goroutines 集合。
    • 从 GRQ 中取出一批 G 放入当前 P 的本地队列,以便后续调度。
  4. 工作窃取机制

    • 如果全局队列也没有可用 G,那么 M 将尝试从其他随机选择的一些活跃且有任务积压在其 LRQ 中的 P 窃取一半数量放到自己的 LRQ。这种机制称为“工作窃取”,用于平衡负载和提高 CPU 利用率。
  5. 执行 Goroutine

    • 一旦成功获得了待运行 Goroutine(无论是通过哪种方式),M 开始实际执行这个 Goroutine。当这个 Goroutine 执行完成后,重复上述过程以寻找下一个可以被调度和运行的新任务。
  6. 持续循环

    • 这种寻找、分配、窃取和执行过程会不断循环进行,以确保所有创建出来但尚未完成工作的 Goroutines 能够尽快得到处理,同时最大化利用系统资源来提高程序性能。

通信与同步

  • Go 提供了 Channel 来实现不同 Goroutines 间的数据传递和同步,这是一种 CSP(Communicating Sequential Processes)模型,实现了安全且高效的数据共享机制。

  • 除此之外,还可以使用 sync 包中的互斥锁、条件变量等进行更细粒度控制,但 Channel 是最常用且推荐的方法,因为它鼓励通过消息传递而非共享数据来实现协作。

数据竞争

在 Go 中,goroutines 是并发执行的基本单位,它们可以在同一时间访问共享数据。这种并发性虽然提高了程序的效率,但也带来了数据竞争和不安全访问的问题。

尽管有锁等可以解决数据安全问题,但我们仍然尽可能的避免可变共享状态,通过将需要修改的数据封装到单个 goroutine 中,并通过 channel 与其他部分交互,可以有效降低复杂性和错误风险。

数据竞争(Data Race)

  • 定义:当两个或多个 goroutines 同时访问相同的内存位置,并且至少有一个是写操作时,就会发生数据竞争。
  • 影响:数据竞争可能导致不可预测的行为、程序崩溃或错误结果。

示例代码:

func main() {for i := 0; i < 100; i++ {go modify()go modify2()}fmt.Println(a) // 每次执行输出结果都可能不为1
}var a = 1func modify() {a += 1
}func modify2() {a -= 1
}

由于 Go 语言运行时调度 goroutine 的执行顺序是不确定的,最终 a 的值可能是增加、减少,甚至可能保持不变。这取决于 goroutine 执行的顺序和时机。

同时可以使用 Go 的 race detector 来检测代码中的潜在数据竞争问题。在编译或运行时添加 -race 标志即可启用此功能。

go run -race test_main.go

输出:

==================
WARNING: DATA RACE
Read at 0x0000111c15f0 by goroutine 6:main.modify()/Users/fangyirui/GolandProjects/awesomeProject/_29goroutine/test_main.go:18 +0x24Previous write at 0x0000111c15f0 by goroutine 8:main.modify()/Users/fangyirui/GolandProjects/awesomeProject/_29goroutine/test_main.go:18 +0x3cGoroutine 6 (running) created at:main.main()/Users/fangyirui/GolandProjects/awesomeProject/_29goroutine/test_main.go:9 +0x32Goroutine 8 (finished) created at:main.main()/Users/fangyirui/GolandProjects/awesomeProject/_29goroutine/test_main.go:9 +0x32
==================
...
...
...
==================
WARNING: DATA RACE
Read at 0x0000111c15f0 by main goroutine:main.main()/Users/fangyirui/GolandProjects/awesomeProject/_29goroutine/test_main.go:12 +0x5ePrevious write at 0x0000111c15f0 by goroutine 22:main.modify()/Users/fangyirui/GolandProjects/awesomeProject/_29goroutine/test_main.go:18 +0x3cGoroutine 22 (finished) created at:main.main()/Users/fangyirui/GolandProjects/awesomeProject/_29goroutine/test_main.go:9 +0x32
==================
2
Found 7 data race(s)
exit status 66

互斥锁 (sync.Mutex)

  • 基本概念:互斥锁(Mutex)是一种最基本的同步原语,用于保护临界区,以确保同一时间只有一个 goroutine 可以访问被保护的数据。

  • 使用方法

    • Lock(): 获取互斥锁。如果已经被其他 goroutine 锁定,则阻塞直到获取到该锁。
    • Unlock(): 释放互斥锁。
    var (count = 1mu    = sync.Mutex{}wg    = sync.WaitGroup{}
    )func add() {mu.Lock()count += 1mu.Unlock()wg.Done()
    }func minus() {mu.Lock()count -= 1mu.Unlock()wg.Done()
    }func Test1(t *testing.T) {for i := 0; i < 1000; i++ {wg.Add(1)go add()wg.Add(1)go minus()}wg.Wait()fmt.Println(count)
    }
    

读写锁 (sync.RWMutex)

  • 基本概念:读写互斥锁允许多个读取操作同时进行,但写操作是独占的。这意味着在持有读锁时,可以允许其他 goroutine 获取读锁,但不能获取写锁;而持有写锁时,任何其他 goroutine 都不能获取读或写。

  • 使用方法

    • RLock() / RUnlock():用于读取操作,允许多个同时存在。
    • Lock() / Unlock():用于写入操作,是独占性的,与sync.Mutex的没啥区别。
    var (data    = 0rwMutex = sync.RWMutex{}
    )// 读操作
    func readData() {rwMutex.RLock() // 加读锁fmt.Println("读取数据:", data)time.Sleep(1 * time.Second) // 模拟读操作耗时rwMutex.RUnlock()           // 释放读锁wg.Done()
    }// 写操作
    func writeData(value int) {rwMutex.Lock() // 加写锁data = valuefmt.Println("写入数据:", data)rwMutex.Unlock() // 释放写锁wg.Done()
    }func Test2(t *testing.T) {// 启动多个读操作for i := 0; i < 10; i++ {wg.Add(1)go readData()}// 启动一个写操作wg.Add(1)go writeData(100)wg.Wait()
    }
    

注:如果不加读锁,结果会比较不可控,读锁还是可以保证写入的原子性,即保证同时读的结果都一致

原子操作 (sync/atomic)

  • 提供了一组底层函数,用于对整数值进行原子加载、存储和修改。这些函数可以避免使用锁,从而提高性能。
var count2 int32 = 0func addAtomic() {atomic.AddInt32(&count2, 1)wg.Done()
}func minusAtomic() {atomic.AddInt32(&count2, -1)wg.Done()
}func Test3(t *testing.T) {for i := 0; i < 1000; i++ {wg.Add(1)go addAtomic()wg.Add(1)go minusAtomic()}wg.Wait()fmt.Println(count2)
}

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

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

相关文章

Flink细粒度的资源管理

Apache Flink致力于为所有应用程序自动导出合理的默认资源需求。对于希望根据其特定场景微调其资源消耗的用户,Flink提供了细粒度的资源管理。这里我们就来看下细粒度的资源管理如何使用。(注意该功能目前仅对DataStream API有用) 1. 适用场景 使用细粒度的资源管理的可能…

《操作系统 - 清华大学》5 -5:缺页异常

文章目录 1. 缺页异常的处理流程2.在何处保存未被映射的页&#xff1f;3. 虚拟内存性能 1. 缺页异常的处理流程 缺页中断的处理过程: CPU读内存单元&#xff0c;在TLB中根据其虚拟地址匹配物理地址&#xff0c;未命中&#xff0c;读页表; 由于页表项的存在位为0&#xff0c;CP…

Linux-NFS

文章目录 NASNFSNFS配置 &#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f916;Linux专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2024年11月27日12点50分 NAS 网络接入存储 共享存储文件存储 NAS设备包括 NAS引擎一个或多个网络接口一个操作系统…

OpenHarmony属性信息怎么修改?触觉智能RK3566鸿蒙开发板来演示

本文介绍在开源鸿蒙OpenHarmony系统下&#xff0c;修改产品属性信息的方法&#xff0c;触觉智能Purple Pi OH鸿蒙开发板演示&#xff0c;搭载了瑞芯微RK3566四核处理器&#xff0c;Laval鸿蒙社区推荐开发板&#xff0c;已适配全新OpenHarmony5.0 Release系统&#xff0c;感兴趣…

杰发科技AC7803——不同晶振频率时钟的配置

计算公式 PLL_POSDIV [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62] PLL_PREDIV_1 1 2 4 USE_XTAL 24M SYSCLK_FREQ 64M SYSCLK_DIVIDER 1 VCO USE_XTAL*…

面向对象进阶-抽象类

抽象方法 将共性行为&#xff08;方法&#xff09;抽取到父类&#xff0c;由于每个子类执行内容不一样&#xff0c;在父类不能确定具体的方法体&#xff0c;该方法可以定义为抽象方法。 抽象类&#xff1a;如果一个类中存在抽象方法&#xff0c;那么该 类必须声明为抽象类。…

【数据结构专栏】二叉搜索树(Binary Search Tree)的剖析?

文章目录 &#x1f9e8;前言1、二叉搜索树的基本概念&#xff1f;2、二叉搜索树的节点结构组成&#xff1f;3、二叉搜索树的插入操作&#xff1f;4、二叉搜索树的删除操作&#xff1f;5、二叉搜索树的遍历&#xff1f;6、二叉搜索树的性能分析&#xff1f;&#x1f389;完整代码…

分布式调用 - 服务间的远程调用RPC

文章目录 导图PreRPC 概述RPC 调用过程RPC 动态代理1. 接口定义 (SeverProvider)2. 实现类 (ServerProviderImpl)3. 动态代理类 (DynamicProxy)4. 客户端 (Client)5. 代码工作流程6. 总结和注意点7. 结果输出8. 小结 RPC 序列化协议编码网络传输 导图 服务和应用的调用基于场景…

vue3项目搭建-4-正式启动项目,git管理

安装插件&#xff1a; npm install vue router npm install eslint 完成目录&#xff1a; 需要添置文件夹&#xff1a; apis -> api接口 composables -> 组合函数 directives -> 全局指令 styles -> 全局样式 utils -> 工具函数 git 管理&#xff1a; …

GPON原理

GPON网络架构 对于OLT来说&#xff0c;它就相当于一个指挥官&#xff0c;它指挥PON口下的ONU在指定的时间段内发送数据以及发起测距过程等 而ONU则是一个士兵&#xff0c;按照OLT的指挥做出相应 而ODN它主要就是提供一个传输通道&#xff0c;主要包括分光器和光纤组成 对于PO…

SJYP 24冬季系列 FROZEN CHARISMA发布

近日&#xff0c;女装品牌SJYP 2024年冬季系列——FROZEN CHARISMA已正式发布&#xff0c;展现了更加干练的法式风格。此次新品发布不仅延续了SJYP一贯的强烈设计风格和个性时尚&#xff0c;更融入了法式风情的干练元素&#xff0c;为消费者带来了一场视觉与穿着的双重盛宴。  …

【H2O2|全栈】JS进阶知识(十一)axios入门

目录 前言 开篇语 准备工作 获取 介绍 使用 结束语 前言 开篇语 本系列博客主要分享JavaScript的进阶语法知识&#xff0c;本期主要对axios进行基本的了解。 与基础部分的语法相比&#xff0c;ES6的语法进行了一些更加严谨的约束和优化&#xff0c;因此&#xff0c;在…

git使用文档手册

创建一个本地代码工作空间&#xff0c;比如这里使用test目录作为工作目录 针对仓库地址 http://192.168.31.125:9557/poxiaoai-crm/project-crm.git。 1. 安装 Git 确保您的系统已经安装了 Git。如果未安装&#xff0c;请根据操作系统访问 Git 官网 下载并安装。 验证安装 …

数据结构——排序算法第二幕(交换排序:冒泡排序、快速排序(三种版本) 归并排序:归并排序(分治))超详细!!!!

文章目录 前言一、交换排序1.1 冒泡排序1.2 快速排序1.2.1 hoare版本 快排1.2.2 挖坑法 快排1.2.3 lomuto前后指针 快排 二、归并排序总结 前言 继上篇学习了排序的前面两个部分:直接插入排序和选择排序 今天我们来学习排序中常用的交换排序以及非常稳定的归并排序 快排可是有多…

11.26深度学习_神经网络-数据处理

一、深度学习概述 1. 什么是深度学习 ​ 人工智能、机器学习和深度学习之间的关系&#xff1a; ​ 机器学习是实现人工智能的一种途径&#xff0c;深度学习是机器学习的子集&#xff0c;区别如下&#xff1a; ​ 传统机器学习算法依赖人工设计特征、提取特征&#xff0c;而深…

数据结构 (13)串的应用举例

前言 数据结构中的串&#xff08;String&#xff09;&#xff0c;也称为字符串&#xff0c;是一种常见且重要的数据结构&#xff0c;在计算机科学中被广泛应用于各种场景。 一、文本处理 文本编辑器&#xff1a;在文本编辑器中&#xff0c;字符串被用来表示和存储用户输入的文本…

PointNet++论文复现

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

时间的礼物:如何珍视每一刻

《时间的礼物&#xff1a;如何珍视每一刻》 夫时间者&#xff0c;宇宙之精髓&#xff0c;生命之经纬&#xff0c;悄无声息而流转不息&#xff0c;如织锦之细线&#xff0c;串联古今&#xff0c;贯穿万物。 人生短暂&#xff0c;犹如白驹过隙&#xff0c;倏忽而逝&#xff0c;…

NVIDIA /CUDA 里面的clock rate详细介绍

本文主要介绍&#xff1a; cuda中的时钟频率具体有哪些&#xff1f;clock rate怎么调节&#xff1f; cuda中可以通过nvml 函数或者命令来调整时钟频率&#xff08;clock rate&#xff09; 介绍 命令行 nvdia-smi -q -i 0 可以查询device相关参数&#xff0c;可以用后面的命…

扫振牙刷设计思路以及技术解析

市面上目前常见的就两种&#xff1a;扫振牙刷和超声波牙刷 为了防水&#xff0c;表面还涂上了一层防水漆 一开始的电池管理芯片&#xff0c;可以让充电更加均衡。 如TP4056 第一阶段以恒流充电&#xff1b;当电压达到预定值时转入第二阶段进行恒压充电&#xff0c;此时电流逐…