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 = 1

func 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 +0x24

Previous write at 0x0000111c15f0 by goroutine 8:
  main.modify()
      /Users/fangyirui/GolandProjects/awesomeProject/_29goroutine/test_main.go:18 +0x3c

Goroutine 6 (running) created at:
  main.main()
      /Users/fangyirui/GolandProjects/awesomeProject/_29goroutine/test_main.go:9 +0x32

Goroutine 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 +0x5e

Previous write at 0x0000111c15f0 by goroutine 22:
  main.modify()
      /Users/fangyirui/GolandProjects/awesomeProject/_29goroutine/test_main.go:18 +0x3c

Goroutine 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 = 1
    	mu    = sync.Mutex{}
    	wg    = sync.WaitGroup{}
    )
    
    func add() {
    	mu.Lock()
    	count += 1
    	mu.Unlock()
    	wg.Done()
    }
    
    func minus() {
    	mu.Lock()
    	count -= 1
    	mu.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    = 0
    	rwMutex = 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 = value
    	fmt.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 = 0

func 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)
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

dmdba用户资源限制ulimit -a 部分配置未生效

dmdba用户资源限制ulimit -a 部分配置未生效 1 环境介绍2 数据库实例日志报错2.1 mpp01 实例日志报错2.2 mpp02 实例日志报错 3 mpp02 服务器资源限制情况4 关闭SELinux 问题解决4.1 临时关闭 SELinux4.2 永久关闭 SELinux 5 达梦数据库学习使用列表 1 环境介绍 Cpu x86 Os Ce…

【计算机网络】核心部分复习

目录 交换机 v.s. 路由器OSI七层更实用的TCP/IP四层TCPUDP 交换机 v.s. 路由器 交换机-MAC地址 链接设备和设备 路由器- IP地址 链接局域网和局域网 OSI七层 物理层&#xff1a;传输设备。原始电信号比特流。数据链路层&#xff1a;代表是交换机。物理地址寻址&#xff0c;交…

【新人系列】Python 入门(十四):文件操作

✍ 个人博客&#xff1a;https://blog.csdn.net/Newin2020?typeblog &#x1f4dd; 专栏地址&#xff1a;https://blog.csdn.net/newin2020/category_12801353.html &#x1f4e3; 专栏定位&#xff1a;为 0 基础刚入门 Python 的小伙伴提供详细的讲解&#xff0c;也欢迎大佬们…

JVM指令集概览:基础与应用

写在文章开头 在现代软件开发中,Java 语言凭借其“一次编写,到处运行”的理念成为了企业级应用的首选之一。这一理念的背后支撑技术正是 Java 虚拟机(JVM)。JVM 是一个抽象的计算机,它实现了 Java 编程语言的各种特性,并且能够执行编译后的字节码文件。了解 JVM 的工作原…

HarmonyOS4+NEXT星河版入门与项目实战(22)------动画(属性动画与显示动画)

文章目录 1、属性动画图解2、案例实现-小鱼移动游戏1、代码实现2、代码解释3、资源图片4、实现效果3、显示动画4、案例修改-显示动画5、总结1、属性动画图解 这里我们用一张完整的图来汇整属性动画的用法格式和使用的主要属性范围,如下所示: 2、案例实现-小鱼移动游戏 1、代…

diffusion model: prompt-to-prompt 深度剖析

参考&#xff1a;diffusion model(十四)&#xff1a; prompt-to-prompt 深度剖析-CSDN博客 P2P提出的Motivation 目前大火的文生图技术(text to image)&#xff0c;给定一段文本&#xff08;prompt&#xff09;和随机种子&#xff0c;文生图模型会基于这两者生成一张图片。生…

【vue for beginner】Vue该怎么学?

&#x1f308;Don’t worry , just coding! 内耗与overthinking只会削弱你的精力&#xff0c;虚度你的光阴&#xff0c;每天迈出一小步&#xff0c;回头时发现已经走了很远。 vue2 和 vue3 Vue2现在正向vue3逐渐更新中&#xff0c;官方vue2已经不再更新。 这个历程和当时的pyt…

Python语法基础(三)

&#x1f308;个人主页&#xff1a;羽晨同学 &#x1f4ab;个人格言:“成为自己未来的主人~” 我们这篇文章来说一下函数的返回值和匿名函数 函数的返回值 我们先来看下面的这一段函数的定义代码 # 1、返回值的意义 def func1():print(111111111------start)num166print…

光伏功率预测!Transformer-LSTM、Transformer、CNN-LSTM、LSTM、CNN五模型时序预测

目录 预测效果基本介绍程序设计参考资料 预测效果 基本介绍 Transformer-LSTM、Transformer、CNN-LSTM、LSTM、CNN五模型多变量时序光伏功率预测 (Matlab2023b 多输入单输出) 1.程序已经调试好&#xff0c;替换数据集后&#xff0c;仅运行一个main即可运行&#xff0c;数据格式…

Jpype调用jar包

需求描述 ​   公司要求使用python对接口做自动化测试&#xff0c;接口的实现是Java&#xff0c;部分接口需要做加解密&#xff0c;因此需要使用python来调用jar包来将明文加密成密文&#xff0c;然后通过http请求访问接口进行测试。 如何实现 1.安装Jpype ​   首先我…

HTML飞舞的爱心

目录 系列文章 写在前面 完整代码 代码分析 写在后面 系列文章 序号目录1HTML满屏跳动的爱心&#xff08;可写字&#xff09;2HTML五彩缤纷的爱心3HTML满屏漂浮爱心4HTML情人节快乐5HTML蓝色爱心射线6HTML跳动的爱心&#xff08;简易版&#xff09;7HTML粒子爱心8HTML蓝色…

前端面试题-1(详解事件循环)

1.了解浏览器的进程模型 1.什么是进程&#xff1f; 程序运行需要有它自己专属的内存空间&#xff0c;可以把这块内存空间简单的理解为进程 每个应用至少有一个进程&#xff0c;进程之间相互独立&#xff0c;即使要通信&#xff0c;也需要双方同意。 2.什么是线程&#xff1f…

记录QT5迁移到QT6.8上的一些问题

经常看到有的同学说网上的教程都是假的&#xff0c;巴拉巴拉&#xff0c;看看人家发布时间&#xff0c;Qt官方的API都会有所变动&#xff0c;多搜索&#xff0c;多总结&#xff0c;再修改记录。 下次遇到问题多这样搜索 QT 4/5/6 xxx document&#xff0c;对比一下就知道…

python常见问题-pycharm无法导入三方库

1.运行环境 python版本&#xff1a;Python 3.9.6 需导入的greenlet版本&#xff1a;greenlet 3.1.1 2.当前的问题 由于需要使用到greenlet三方库&#xff0c;所以进行了导入&#xff0c;以下是我个人导入时的全过程 ①首先尝试了第1种导入方式&#xff1a;使用pycharm进行…

【计算机网络】—— 物理层

文章目录 前言 一、基本概念 1. 传输媒体 2. 物理层协议的主要任务 3. 物理层的任务 二、传输媒体 1. 导引型 同轴电缆 双绞线 光纤 电力线 2. 非导引型 三、传输方式 1. 串行、并行 2. 同步、异步 3. 单工、半双工、全双工 四、编码和调制 1. 基本概念 2. …

OGRE 3D----4. OGRE和QML共享opengl上下文

在现代图形应用开发中,OGRE(Object-Oriented Graphics Rendering Engine)和QML(Qt Modeling Language)都是非常流行的工具。OGRE提供了强大的3D渲染能力,而QML则用于构建灵活的用户界面。在某些应用场景中,我们需要在同一个应用程序中同时使用OGRE和QML,并且共享OpenGL…

Lumoz TGE在即,NFT助力提前解锁esMOZ

引导语&#xff1a; 虽然近期比特币逼近 10 万美元大关&#xff0c;但很多加密玩家却满仓山寨币&#xff0c;收益甚至可能没有跑赢大盘。别着急&#xff0c;2024 年最后一个“大羊毛”来袭&#xff0c;这便是Lumoz esMOZOG NFT王炸空投组合。 11 月 5 日&#xff0c;模块化计算…

Trimble X12助力电力管廊数据采集,为机器人巡视系统提供精准导航支持

地下电缆是一个城市重要的基础设施&#xff0c;它不仅具有规模大、范围广、空间分布复杂等特点&#xff0c;更重要的是它还承担着信息传输、能源输送等与人们生活息息相关的重要功能&#xff0c;也是一个城市赖以生存和发展的物质基础。 01、项目概述 本次项目是对某区域2公里左…

Kubernetes(k8s)1.30.7简单快速部署对外部开放的有状态服务MYSQL8(快速有效)

如何在Kubernetes集群中快速创建部署一个单节点的有状态&#xff08;即将数据文件挂载到宿主机&#xff0c;防止重新部署mysql服务&#xff0c;数据文件丢失&#xff09;的对外开放的MYSQL服务。 通过创建一个 Kubernetes Deployment 并使用 PersistentVolumeClaim 将其连接到…

WPF+MVVM案例实战与特效(三十)- 封装一个系统日志显示控件

文章目录 1、运行效果2、日志控件封装1、文件创建2、DisplayLogPanel.xaml 代码3、using System;3、using System;3、数据模型4、枚举类型3、案例实现1、LogPanelWindow.xaml2、LogPanelViewModel.cs4、总结1、运行效果 2、日志控件封装 1、文件创建 打开 Wpf_Examples ,在 …