go 多线程

news/2024/12/5 2:21:27/文章来源:https://www.cnblogs.com/Capooooo/p/18585230

go 多线程

进程、线程、和协程

  • 进程
    • 分配系统资源(CPU 时间、内存等)基本单位
    • 有独立的内存空间,切换开销大
  • 线程:进程的一个执行流,是 CPU 调度并能独立运行的的基本单位
    • 同一进程中的多线程共享内存空间,线程切换代价小
    • 多线程通信方便
    • 从内核层面来看线程其实也是一种特殊的进程,它跟父进程共享了打开的文件和文件系统信息,共
      享了地址空间和信号处理函数
  • 协程
    • Go 语言中的轻量级线程实现
    • Golang 在 runtime、系统调用等多方面对 goroutine 调度进行了封装和处理,当遇到长时间执行
      或者进行系统调用时,会主动把当前 goroutine 的 CPU (P) 转让出去,让其他 goroutine 能被调度
      并执行,也就是 Golang 从语言层面支持了协程

线程和协程的差异

  • 每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少
    • goroutine:2KB
    • 线程:8MB
  • 线程/goroutine 切换开销方面,goroutine 远比线程小
    • 线程:涉及模式切换(从用户态切换到内核态)、16个寄存器、PC、SP...等寄存器的刷新
    • goroutine:只有三个寄存器的值修改 - PC / SP / DX.
  • GOMAXPROCS
    • 控制并行线程数量

协程示例

  • 启动新协程:go functionName()
for i := 0; i < 10; i++ {go fmt.Println(i)
}
time.Sleep(time.Second)

channel - 多线程通信

Channel 是什么

  • Channel 是多个协程之间通讯的管道
    • 一端发送数据,一端接收数据
    • 同一时间只有一个协程可以访问数据,无共享内存模式可能出现的内存竞争
    • 协调协程的执行顺序
  • 声明方式
    • var identifier chan datatype
    • 操作符<-
  • 示例
ch := make(chan int)
go func() {fmt.Println("hello from goroutine")ch <- 0 //数据写入Channel
}()
i := <-ch//从Channel中取数据并赋值

通道缓冲

  • 基于 Channel 的通信是同步的
  • 当缓冲区满时,数据的发送是阻塞的
  • 通过 make 关键字创建通道时可定义缓冲区容量,默认缓冲区容量为 0

遍历通道缓冲区

ch := make(chan int, 10)
go func() {for i := 0; i < 10; i++ {rand.Seed(time.Now().UnixNano())n := rand.Intn(10) // n will be between 0 and 10fmt.Println("putting: ", n)ch <- n}close(ch)
}()
fmt.Println("hello from main")
for v := range ch {fmt.Println("receiving: ", v)
}

单向通道

  • 只发送通道
    • var sendOnly chan<- int
  • 只接收通道
    • var readOnly <-chan int
  • Istio webhook controller
    • func (w *WebhookCertPatcher) runWebhookController(stopChan <-chan struct{}) {}
var c = make(chan int)
go prod(c)
go consume(c)
func prod(ch chan<- int){for { ch <- 1 }
}
func consume(ch <-chan int) {for { <-ch }
}

select

  • 当多个协程同时运行时,可通过 select 轮询多个通道
    • 如果所有通道都阻塞则等待,如定义了 default 则执行 default
    • 如多个通道就绪则随机选择
select {case v:= <- ch1:...case v:= <- ch2:...default:...
}

定时器 Timer

  • time.Ticker 以指定的时间间隔重复的向通道 C 发送时间值
  • 使用场景
    • 为协程设定超时时间
timer := time.NewTimer(time.Second)
select {// check normal channelcase <-ch:fmt.Println("received from ch")case <-timer.C:fmt.Println("timeout waiting from channel ch")
}

  • Go 语言不仅仅提供基于 CSP 的通讯模型,也支持基于共享内存的多线程数据访问
  • Sync 包提供了锁的基本原语
  • sync.Mutex 互斥锁
  • sync.RWMutex 读写分离锁
    • 不限制并发读,只限制并发写和并发读写
  • sync.WaitGroup
    • 等待一组 goroutine 返回
  • sync.Once
    • 保证某段代码只执行一次
  • sync.Cond
    • 让一组 goroutine 在满足特定条件时被唤醒

Mutex 示例

Kubernetes 中的 informer factory

// Start initializes all requested informers.
func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) {f.lock.Lock()defer f.lock.Unlock()for informerType, informer := range f.informers {if !f.startedInformers[informerType] {go informer.Run(stopCh)f.startedInformers[informerType] = true}}
}

WaitGroup 示例

// CreateBatch create a batch of pods. All pods are created before
waiting.
func (c *PodClient) CreateBatch(pods []*v1.Pod) []*v1.Pod {ps := make([]*v1.Pod, len(pods))var wg sync.WaitGroupfor i, pod := range pods {wg.Add(1)go func(i int, pod *v1.Pod) {defer wg.Done()defer GinkgoRecover()ps[i] = c.CreateSync(pod)}(i, pod)}wg.Wait()return ps
}

Cond 示例

Kubernetes 中的队列,标准的生产者消费者模式

// Add marks item as needing processing.
func (q *Type) Add(item interface{}) {q.cond.L.Lock()defer q.cond.L.Unlock()if q.shuttingDown {return}if q.dirty.has(item) {return}q.metrics.add(item)q.dirty.insert(item)if q.processing.has(item) {return}q.queue = append(q.queue, item)q.cond.Signal()
}// Get blocks until it can return an item to be processed. If shutdown = true,
// the caller should end their goroutine. You must call Done with item when you
// have finished processing it.
func (q *Type) Get() (item interface{}, shutdown bool) {q.cond.L.Lock()defer q.cond.L.Unlock()for len(q.queue) == 0 && !q.shuttingDown {q.cond.Wait()}if len(q.queue) == 0 {// We must be shutting down.return nil, true}item, q.queue = q.queue[0], q.queue[1:]q.metrics.get(item)q.processing.insert(item)q.dirty.delete(item)return item, false
}

深入理解 Go 语言协程调度

  • 进程:资源分配的基本单位
  • 线程:调度的基本单位
  • 无论是线程还是进程,在 Linux 中都是以 task_struct描述,从内核角度看,与进程没有区别
  • Glibcpthread库中提供了 NPTL 支持

image-20241203221514252

CPU 对内存的访问

  • CPU 上有个 Memory Management Unit(MMU) 单元
  • CPU 把虚拟地址给 MMU,MMU 去物理内存中查询页表,得到实际的物理地址
  • CPU 维护一份缓存 Translation Lookaside Buffer(TLB),缓存虚拟地址和物理地址的映射关系

image-20241203222648055

进程切换的开销

  • 直接开销
    • 切换页表全局目录(PGD)
    • 切换内核态堆栈
    • 切换硬件上下文(进程恢复前,必须装入寄存器的数据统称为硬件上下文)
    • 刷新 TLB
    • 系统调度器的代码执行
  • 间接开销
    • CPU 缓存失效导致的进程需要到内存直接访问的 IO 操作变多

线程切换的开销

  • 线程本质上只是一批共享资源的进程,线程切换本质上依然需要内核进行进程切换
  • 一组线程因为共享内存资源,因此一个进程的所有线程共享虚拟地址空间,线程切换相比进程
    切换,主要节省了虚拟地址空间的切换

用户线程

无需内核帮助,应用程序在用户空间创建的可执行单元,创建销毁完全在用户态完成。

image-20241203223312513

Goroutine

Go 语言基于 GMP 模型实现用户态线程

  • G: 表示 goroutine,每个 goroutine 都有自己的栈空间,定时器,初始化的栈空间在 2k 左右,空间会随着需求增长。
  • M:抽象化代表内核线程,记录内核线程栈信息,当 goroutine 调度到线程时,使用该 goroutine 自己的栈信息。
  • P:代表调度器,负责调度 goroutine,维护一个本地 goroutine 队列,M 从 P 上获得 goroutine 并执行,同时还负责部分内存的管理。

GMP 模型细节

image-20241203223929723

G 所处的位置

  • 进程都有一个全局的 G 队列
  • 每个 P 拥有自己的本地执行队列
  • 有不在运行队列中的 G
    • 处于 channel 阻塞态的 G 被放在 sudog
    • 脱离 P 绑定在 M 上的 G,如系统调用
    • 为了复用,执行结束进入 P 的 gFree 列表中的 G

Goroutine 创建过程

  • 获取或者创建新的 Goroutine 结构体
    • 从处理器的 gFree 列表中查找空闲的 Goroutine
    • 如果不存在空闲的 Goroutine,会通过 runtime.malg 创建一个栈大小足够的新结构体
  • 将函数传入的参数移到 Goroutine 的栈上
  • 更新 Goroutine 调度相关的属性,更新状态为_Grunnable
  • 返回的 Goroutine 会存储到全局变量 allgs 中

将 Goroutine 放到运行队列上

  • Goroutine 设置到处理器的 runnext 作为下一个处理器
    执行的任务
  • 当处理器的本地运行队列已经没有剩余空间时,就会把
    本地队列中的一部分 Goroutine 和待加入的 Goroutine
    通过 runtime.runqputslow 添加到调度器持有的全局
    运行队列上

调度器行为

  • 为了保证公平,当全局运行队列中有待执行的 Goroutine 时,通过 schedtick 保证有一定
    几率(1 / 61)会从全局的运行队列中查找对应的 Goroutine
  • 从处理器本地的运行队列中查找待执行的 Goroutine
  • 如果前两种方法都没有找到 Goroutine,会通过 runtime.findrunnable 进行阻塞地查找
    Goroutine
    • 从本地运行队列、全局运行队列中查找
    • 从网络轮询器中查找是否有 Goroutine 等待运行
    • 通过 runtime.runqsteal 尝试从其他随机的处理器中窃取待运行的 Goroutine

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

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

相关文章

第57篇 docker的常用命令

1 镜像管理2 容器管理3 容器运行4 网络管理5 插件管理6 数据卷管理7 日常操作8 常用dockerfile指令

技术美术学习路线

技术美术学习路线 理想路线 第一个阶段熟练一门编程语言(课设写个例如医院管理系统,图书管理系统之类的):C/python/C++/C#,并尝试用它写一个飞机大战小游戏养成良好的审美,每日收集图片,鉴赏各种美术风格从Unity客户端开始学习,之后逐步学习技术美术:如何不写代码却能…

Pwn-栈溢出

原理 基本的栈帧结构(以 x64 的栈为例)(图片摘自Hello-CTF) RBP 为栈底寄存器,RSP 为栈顶寄存器,分别记录了栈帧中记录数据部分的起始和终止地址。函数的临时变量的在内存中的位置都是通过这两个寄存器加减偏移确定的。 栈底分别还记录了上一个栈帧的 RBP 的值,以及函数的…

DSB的数字正交解调

1.DSB调制过程 ​ DSB信号是一种双边带调幅调制信号,又叫双边带调幅,通过改变载波的振幅来实现基带数据的传输。 其函数表达式如下: \[s(t) = m(t)*cos(2\pi ft + \varphi) \]其中:m(t):表示基带信号。 \(cos(2\pi ft + \varphi )\):表示载波信号。2.DSB的数字正交解调 ​ …

JDY-68A模块语音播报

该模块引脚如下图所示:连接其中的VCC与GND即可让蓝牙模块运行起来 如果要实现播放手机中的声音,需要参考下面图中第10个引脚其中Audio是声音输出,连接功放模块的输入+,而功放模块中的输入-直接接地线即可。 该模块有个问题就是其为贴片设计,需要自行设计底板,但是如果只是要测试…

《代数学基础Ⅰ》期中考试分析

总体分析:本次考试得分85(17%),尚可,但未达到自己的预期(90+)。 错题分析:目前未下发答题纸,我觉得主要有两方面问题。 1. 分类讨论不全,特别关注分式的分母能否取零导致的分类 2. 当且仅当的证明,这是一个充要条件,充分性多数情况下比较难证,要多加练习。(坏了,…

第56篇 docker简单介绍

1.docker介绍 Docker是一个用于构建,运行,传送 应用程序的平台。以下所有环境打包成一个集装环境1.1 为什么要用docker? 第一点:安装依赖过多 如果不用docker,需要部署一个带有数据库的,前后端服务的应用时,就要执行step的步骤安装环境,网站才能运行该应用第二点:不同…

设计位置编码

Gall 定律 一个有效的复杂系统通常是从一个有效的简单系统演化而来的 —— John Gall本文将带你一步步探究 Transformer 模型中先进的位置编码技术。我们将通过迭代改进编码位置的方法,最终得出 旋转位置编码 (Rotary Postional Encoding, RoPE),这也是最新发布的 LLama 3.…

中国AI大模型市场:创业公司在巨头竞争中的生存之道|报告汇总PDF洞察(附原数据表)

原文链接:https://tecdat.cn/?p=38460 在2023年10月上旬的一次聚会中,众多投资了AI大模型的投资者在轻松的氛围中探讨了当前市场的严峻挑战。市场数据显示,仅仅半年前,投资者们还在为争夺投资份额而焦虑。然而,当前市场情绪已经发生了转变,投资者普遍认为,大模型领域的…

Python基于滑动窗口CNN损伤梁桥数据、故宫城墙图像数据分类可视化|附数据代码

全文链接:https://tecdat.cn/?p=38442 原文出处:拓端数据部落公众号 分析师:Yufei Guo 在现代土木结构工程领域,结构损伤的准确识别与定位对于保障基础设施的安全性和耐久性具有极为关键的意义。传统的人工检查方法,如目视检查以及借助专业设备进行检测,在很长一段时间内…

【专题】日本车企利润骤降94%,裁员9000人,中国新能源汽车行业迎头赶上|报告汇总PDF洞察(附原数据表)

原文链接:https://tecdat.cn/?p=38407 在全球化的汽车产业竞争中,中国新能源汽车的迅猛发展正重塑市场格局。近期,日本车企遭遇前所未有的挑战,利润大幅下滑,日产净利润暴跌94%,全球裁员9000人,而丰田等巨头也感受到了市场的压力。与此同时,中国品牌如比亚迪以破纪录的…

【恐怖の算法】 扫描线

【恐怖の算法】 扫描线【恐怖の算法】 扫描线 引入 扫描线一般运用在图形上面,它和它的字面意思十分相似,就是一条线在整个图上扫来扫去,它一般被用来解决图形面积,周长,以及二维数点等问题。 二维矩形面积并问题 在二维坐标系上,给出多个矩形的左下以及右上坐标,求出所…