【Go 基础】并发相关

并发相关

CAS

CAS算法(Compare And Swap),是原⼦操作的⼀种,,CAS 算法是⼀种有名的⽆锁算法。⽆锁编程,即不使⽤锁的情况下实现多线程之间的变量同步。可⽤于在多线程编程中实现不被打断的数据交换操作,从⽽避免多线程同时改写某⼀数据时由于执⾏顺序不确定性以及中断的不可预知性产⽣的数据不⼀致问题。

Go中的CAS操作是借⽤了CPU提供的原⼦性指令来实现。 CAS操作修改共享变量时候不需要对共享变量加锁,⽽是通过类似乐观锁的⽅式进⾏检查,本质还是不断的占⽤CPU 资源换取加锁带来的开销(⽐如上下⽂切换销)。

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

var (
	counter int32          //计数器
	wg      sync.WaitGroup //信号量
)

func main() {
	threadNum := 5
	wg.Add(threadNum)
	for i := 0; i < threadNum; i++ {
		go incCounter(i)
	}
	wg.Wait()
}
func incCounter(index int) {
	defer wg.Done()
	spinNum := 0
	for {
		// 原⼦操作
		old := counter
		ok := atomic.CompareAndSwapInt32(&counter, old, old+1)
		if ok {
			break
		} else {
			spinNum++
		}
	}
	fmt.Printf("thread,%d,spinnum,%d\n", index, spinNum)
}

当主函数 main ⾸先创建了5个信号量,然后开启五个线程执⾏ incCounter ⽅法,incCounter 内部执⾏, 使⽤ CAS 操作递增 counter 的值,atomic.CompareAndSwapInt32 具有三个参数,第⼀个是变量的地址,第⼆个是变量当前值,第三个是要修改变量为多少,该函数如果发现传递的 old 值等于当前变量的值,则使⽤第三个变量替换变量的值并返回 true,否则返回 false。

这⾥之所以使⽤⽆限循环是因为在⾼并发下每个线程执⾏ CAS 并不是每次都成功,失败了的线程需要重写获取变量当前的值,然后重新执⾏ CAS 操作。

go 中 CAS 操作可以有效的减少使⽤锁所带来的开销,但是需要注意在⾼并发下这是使⽤ cpu 资源做交换的。

什么是并发编程

首先,要明确并行和并发的概念。

并行,是指两个或者多个事件在同一时刻发生。并发,是指两个或多个事件在同一时间间隔发生。

并发偏重于多个任务交替执行,而多个任务之间还可能是串行的。而并行才是真正意义上的“同时执行”。

**并发编程是指在一台机器上“同时”处理多个任务。**并发是在同一实体上的多个事件,多个事件在同一时间间隔发生。并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。

go 并发机制及 CSP 并发模型

CSP 模型

CSP 模型是上个世纪七十年代提出的,不同于传统的多线程通过共享内存来通信,CSP 讲究的是“以通信的方式来共享内存”。用于描述两个独立的并发实体通过共享的通讯 channel 进行通信的并发模型。CSP 中的 channel 是第一类对象,它不关注发送消息的实体,而是关注发送消息时使用的 channel。

go 中 channel 一般创建后,用于协程之间传递信息,类似于一个消息队列。

go 并发机制

goroutine 是 go 实际并发执行的实体,它底层使用协程实现并发,协程是一种运行在用户态的用户线程,类似于 greenthread,go 底层选择使用协程的原因如下:

  • 它在用户空间操作,避免了内核态和用户态的切换成本
  • 可以由语言和框架层进行调度
  • 相较于线程,拥有更小的栈空间,允许创建大量的协程
goroutine 的特性

go 中有三个对象:

  • G(goroutine):我们说的协程,是用户级的轻量级线程,每个 goroutine 对象中的 sched 保存着它的上下文信息
  • M(work thread):对 OS 内核级线程的封装,数量对应真实的 CPU 数量(一般远大于)
  • P(processor):逻辑处理器,即为 G 和 M 的调度对象,用来调度 G 和 M 之间的关联关系,数量可以通过 GOMAXPROCS() 来设置,默认为核心数

正常情况下,一个 CPU 对象启动一个 M,M 检查并执行 G。遇到 G 阻塞的时候,会启动一个新的 M,以充分利用 CPU 资源。因此,有时候 M 数量会远大于 CPU个数。

GPM 如何调度
  1. 新创建的 G 会先保存在 P 的本地队列中,如果本地队列已满,那么就会保存在全局的 G 队列中,等待被 P 执行。
  2. M 和 P 绑定之后,M 会不断从 P 的本地队列中无锁地取出 G,并切换到这个 G 的堆栈执行。当 P 的本地 G 队列空了,就会从全局 G 队列获取一批 G。当全局队列中也没有待运行的 G 时,就会尝试从其他的 P 窃取部分 G 来执行,相当于 P 之间的负载均衡。
一轮调度

其实,一般我们知道上面的调度逻辑就差不多了。不过,如果需要深入了解的话,还是从最开始的一轮调度开始理一下吧。

引导程序会为 Go 程序的运行建立必要的环境,在完成一系列初始化工作之后,Go 程序的 main 函数才会真正执行。引导程序会在最后让调度器进行一轮调度,这样才能让封装了 main 函数的 G 能马上执行。

一轮调度的总体流程

  1. **调度器先判断当前 M 是否被锁定。**一般来说,Go 的调度器会按照一定策略动态地关联 M、P 和 G,并以此高效的执行并发程序,一般来说并不需要用户程序的任何干预。但是在极少情况下,用户程序不得已需要对 Go 的运行时调度进行干预。

    **锁定 M 和 G 可以说是为了 CGO 准备的。**因为有些 C 语言的函数库(如 OpenGL)会用到线程本地存储技术,将数据存储在当前内核线程的私有缓存中。因此,包含了调用此类 C 函数库的代码的 G 会变得特殊:**在特定时间只能与同一个 M 产生关联,这样会对调度效率造成负面影响。**从上面的流程图可以看出,就算锁定了,一定要尽量减少锁定的时间。

  2. 如果发现当前 M 已经和某个 G 锁定了,那么会立即停止调度并停止当前 M。一旦和它锁定的 G 处于可运行状态,它就会被唤醒并继续运行这个 G。

  3. 如果判断当前 M 未与任何 G 锁定,那么调度器会检查是否有运行时串行任务正在等待执行(此类任务执行时需要停止 Go 调度器,STW)。如果 gcwaiting 不为 0,那么会停止并阻塞当前 M 以等待运行时串行任务执行完成。

  4. 否则,开始真正的可运行 G 寻找之旅。一旦找到一个可运行的 G,调度器会在判定该 G 未与任何 M 锁定之后,立即让当前 M 运行它。(查找的过程:先尝试获取执行 GC 任务的 G -> 从全局可运行 G 队列获取 G -> 从本地 P 得可运行 G 队列获取 G,最后到全力查找可运行的 G)

全力查找可运行的 G

概括来说,就是会多次尝试从各个地方查找可运行的 G:

  1. **获取执行终结器的 G:**所有的终结函数的执行都会由一个专用的 G 负责,如果这个专用 G 已完成任务,调度器就会获取它,把它置为 Grunable 状态并放入本地 P 的可运行 G 队列
  2. **从本地 P 得可运行 G 队列获取 G:**调度器尝试从这里获取一个 G,并返回
  3. **从调度器的可运行 G 队列获取 G:**调度器尝试从这里获取一个 G,并返回
  4. **从网络 I/O 轮询器获取 G:**如果 netpoller 已经被初始化且有过网络 I/O 操作,那么调度器就会尝试从这里获取一个 G 列表,并且把表头的 G 返回,同时把其他的 G 都放入调度器的可运行 G 队列。如果没有初始化或者没有过网络 I/O 操作,就跳过。这里没有获取成功也不会阻塞。
  5. **从其他 P 的可运行 G 队列获取 G:**条件允许时,调度器会使用伪随机算法从全局 P 列表中选取 P,然后尝试从它们的可运行 G 队列中盗取一半的 G 到本地 P 的可运行 G 队列。选取 P 和盗取 G 会重复多次,成功就停止。如果一直没有成功,那么第一阶段结束。
  6. **获取执行 GC 标记任务的 G:**第二阶段,调度器会先判断是否正处于 GC 的标记阶段,以及本地 P 是否可以用于 GC 标记任务,如果都是 true,就将本地 P 持有的 GC 标记专用 G 置为 Grunnable 并返回
  7. 从调度器的可运行 G 队列获取 G:调度器再次尝试从这里获取一个 G,并返回。如果还是找不到,就会解除本地 P 与当前 M 地关联,并把该 P 放入调度器的空闲 P 列表
  8. **从全局 P 列表中的每个 P 得可运行 G 队列获取 G:**遍历全局 P 中的所有 P,检查他们的可运行 G 队列。只要某个 P 的可运行 G 队列不为空,那就从调度器的空闲 P 列表中取一个 P,判定其可用后,将其和当前 M 关联在一起,然后返回第一阶段(第五步),否则继续后续;
  9. **获取执行 GC 标记任务的 G:**判断是否正处于 GC 的标记阶段,以及与 GC 标记任务相关的全局资源是否可用,如果都是 true,调度器从空闲 P 列表拿出一个 P。如果这个 P 持有一个 GC 标记专用 G,就关联该 P 与当前 M,然后再次执行第二阶段(从6 开始)
  10. **从网络 I/O 轮询器获取 G:**此步骤与步骤4 基本相同,但是这里的获取是阻塞的。只有当 netpoller 那里有可用的 G 时,阻塞才会解除。同样的,如果没有初始化或者没有过网络 I/O 操作,就跳过。

如果经过上述10个步骤之后,还没有找到可运行的 G,调度器就会停止当前 M。后续重新唤醒时,会重新进入查找可运行的 G 的子流程。

自旋状态: 意味着它还没有找到 G 来运行。一般来说,运行时系统中至少会有一个自旋的 M,调度器也会尽量保证有一个自旋的 M 存在。除非发现没有自旋的 M,调度器是不会新启用或恢复一个 M 去运行新 G 的。

为什么要有 P
  • GM 模型的问题
    • 存在单一的全局 mutex 和集中状态管理:mutex 需要保护所有与 goroutine 相关的操作,导致锁竞争严重;
    • goroutine 交接问题:M 之间经常交接可运行的 goroutine,而且可能会导致延迟增加和额外开销。每个 M 必须能够执行任何可运行的 G;
    • 每个 M 都需要做内存缓存:会导致资源消耗过大,数据局部性差
    • 存在系统调用的情况下,线程经常会被阻塞和解阻塞。这增加了很多额外的性能开销
  • GMP 模型的优化
    • 每个 P 都有自己的本地队列,大幅度减轻了对全局队列的直接依赖,所带来的效果就是锁竞争的减少,而 GM 模型的性能开销大头就是锁竞争;
    • 每个 P 相对的平衡上,在 GMP 模型中也实现了 Work Stealing 算法,如果 P 的本地队列为空,会从全局队列或者其他 P 得本地队列窃取可运行的 G 来运行,减少空转,提高了资源利用率;

一般来说,M 的数量都会多于 P。在 Go 中,M 的数量默认是 10000,P 的默认数量是 CPU 核数。由于 M 的属性,也就是如果存在系统阻塞调用,阻塞了 M,又不够用的情况下,M 会不断增加。

M 不断增加的话,如果本地队列挂载在 M 上,那么就意味着本地队列也会随之增加。这显然是不合理的,因为本地队列的管理会变得复杂,且 Work Stealing 性能会大幅度下降。

M 被系统调用阻塞之后,我们是期望他能将未执行的任务分配给其他 M 继续运行,而不是全部停止。因此,使用 M 是不合理的,引入新的组件 P,把本地队列关联到 P 上,就能很好的解决这个问题。

go 的 CSP 并发模型

go 的 CSP 并发模型,是通过 goroutine 和 channel 来实现的。

goroutine 是 go 中并发的执行单位,channel 用于各个并发结构体之间的通信。

go 中常用的并发模型

通过 channel 通知实现并发控制

无缓冲的通道指的是通道的大小为 0,也就是说,这种类型的通道在接收前没有能力保存任何值,它要求发送 goroutine 和接收 goroutine 同时准备好,才可以完成发送和接收操作。无缓冲的通道我们也称之为同步通道,通过它接可以简单实现并发控制。

func main() {
  ch := make(chan struct{})
  go func() {
    fmt.Println("start working")
    time.Sleep(time.Second * 1)
    ch <- struct{}{}
  }()
  
  <- ch
  fmt.Println("finished")
}
通过 sync 包中的 WaitGroup 实现并发控制

goroutine 是异步执行的,有的时候为了防止在结束 main 函数的时候结束掉 goroutine,所以需要同步等待,这时候就需要使用 WaitGroup 了。在 Sync 包中,提供了 WaitGroup,它会等待它收集的所有 goroutine 任务全部完成。

需要注意的是,WaitGroup 在第一次使用后,不能被拷贝。

func main() {
	wg := sync.WaitGroup{}
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(wg sync.WaitGroup, i int) {
			fmt.Printf("i:%d", i)
			wg.Done()
		}(wg, i)
	}
	wg.Wait()
	fmt.Println("exit")
}

上面代码运行之后会出现死锁。是因为 wg 给拷贝传递到了 goroutine 中,导致只有 Add 操作,其实 Done 操作是在 wg 的副本执行的。因此,Wait 就会死锁。

有两个修改方式:1️⃣ 将匿名函数中的 wg 传入类型改为 *sync.WaitGroup,这样就能引用到正确的 WaitGroup 了。2️⃣ 将匿名函数中的 wg 的的传入参数去掉,因为 go 支持闭包类型,在匿名函数中可以直接使用外面的 wg 变量。

Go 1.7 之后的 Context

通常,一些简单场景下使用 channel 和 WaitGroup 已经足够了,但是当面临一些复杂多变的网络并发场景下 channel 和 WaitGroup 就显得有些力不从心了。

⽐如⼀个⽹络请求 Request,每个 Request 都需要开启⼀个 goroutine 做⼀些事情,这些 goroutine ⼜可能会开启其他的 goroutine,⽐如数据库和RPC服务。

所以我们需要⼀种可以跟踪 goroutine 的⽅案,才可以达到控制他们的⽬的,这就是 Go 语⾔为我们提供的 Context,称之为上下⽂⾮常贴切,它就是 goroutine 的上下⽂。

它是包括⼀个程序的运⾏环境、现场和快照等。每个程序要运⾏时,都需要知道当前程序的运⾏状态,通常Go 将这些封装在⼀个 Context ⾥,再将它传给要执⾏的 goroutine 。context 包主要是⽤来处理多个 goroutine 之间共享数据,及多个 goroutine 的管理。

context 包的核⼼是 struct Context,接⼝声明如下:

// A Context carries a deadline, cancelation signal, and requestscoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
  // Done returns a channel that is closed when this `Context` is canceled
  // or times out.
  // Done() 返回⼀个只能接受数据的channel类型,当该context关闭或者超时时间到了的时候,该channel就会有⼀个取消信号
  Done() <-chan struct{}
  // Err indicates why this Context was canceled, after the Done channel

  // is closed.
  // Err() 在Done() 之后,返回context 取消的原因。
  Err() error
  // Deadline returns the time when this Context will be canceled, if any.
  // Deadline() 设置该context cancel的时间点
  Deadline() (deadline time.Time, ok bool)
  // Value returns the value associated with key or nil if none.
  // Value() ⽅法允许 Context 对象携带request作⽤域的数据,该数据必须是线程安全的。
  Value(key interface{}) interface{}
}

Context 对象是协程安全的,你可以把⼀个 Context 对象传递给任意个数的 gorotuine,对它执⾏取消操作时,所有 goroutine 都会接收到取消信号。

⼀个 Context 不能拥有 Cancel ⽅法,同时我们也只能 Done channel 接收数据。其中的原因是⼀致的:接收取消信号的函数和发送信号的函数通常不是⼀个。

典型的场景是:⽗操作为⼦操作操作启动 goroutine,⼦操作也就不能取消⽗操作。

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

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

相关文章

【H2O2|全栈】Node.js与MySQL连接

目录 前言 开篇语 准备工作 初始配置 创建连接池 操作数据库 封装方法 结束语 前言 开篇语 本节讲解如何使用Node.js实现与MySQL数据库的连接&#xff0c;并将该过程进行函数封装。 与基础部分的语法相比&#xff0c;ES6的语法进行了一些更加严谨的约束和优化&#…

基于人工智能的新中高考综合解决方案

1. 引言 近年来&#xff0c;随着人工智能技术的飞速发展&#xff0c;教育领域也迎来了深刻的变革。针对新中高考改革的需求&#xff0c;本解决方案集成了科大讯飞在人工智能领域的核心技术&#xff0c;旨在通过智能化手段提升教育教学效率与质量&#xff0c;助力学生全面发展。…

【Linux基础】yum 与 vim 的操作

目录 Linux 应用商店——yum yum和yum源是什么 关于镜像的简单理解 yum 的基本操作 yum的安装 yum install 命令 yum查看软件包 yum list 命令 yum的卸载 yum remove 命令 关于 rzsz 软件 安装 rzsz 软件&#xff1a; rz 命令 sz 命令 yum 源拓展 Linux 编辑器…

Elasticsearch数据迁移(快照)

1. 数据条件 一台原始es服务器&#xff08;192.168.xx.xx&#xff09;&#xff0c;数据迁移后的目标服务器&#xff08;10.2.xx.xx&#xff09;。 2台服务器所处环境&#xff1a; centos7操作系统&#xff0c; elasticsearch-7.3.0。 2. 为原始es服务器数据创建快照 修改elas…

【MySQL】数据类型的注意点和应用

&#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &#x1f4e2;本文由 JohnKi 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f4e2;未来很长&#…

首次打开韦东山提供的Ubuntu-18.04镜像后,该做哪些事?

目录 01-测试有无网络02-配置最基本的嵌入式开发环境(安装tftp-nfs等)03-缩短关机强制结束进行时间04-关闭软件的自动更新05-未完待续... 01-测试有无网络 ping www.baidu.com 02-配置最基本的嵌入式开发环境(安装tftp-nfs等) 需要安装 tftp&#xff0c;nfs&#xff0c;vim …

2030. gitLab A仓同步到B仓

文章目录 1 A 仓库备份 到 B 仓库2 B 仓库修改main分支的权限 1 A 仓库备份 到 B 仓库 #!/bin/bash# 定义变量 REPO_DIR"/home/xhome/opt/git_sync/zz_xx_xx" # 替换为你的本地库A的实际路径 REMOTE_ORIGIN"http://192.168.1.66:8181/zzkj_software/zz_xx_xx.…

Python与C++混合编程的优化策略与实践

在现代软件开发中&#xff0c;混合编程已成为一种普遍的开发模式。这种模式能够充分发挥不同编程语言的优势&#xff0c;实现性能与开发效率的最佳平衡。本文将深入探讨Python和C混合编程的策略与实践经验。 混合编程就像建造一座现代化的大厦&#xff0c;C就像大厦的钢筋混凝…

【kettle】mysql数据抽取至kafka/消费kafka数据存入mysql

目录 一、mysql数据抽取至kafka1、表输入2、json output3、kafka producer4、启动转换&#xff0c;查看是否可以消费 二、消费kafka数据存入mysql1、Kafka consumer2、Get records from stream3、字段选择4、JSON input5、表输出 一、mysql数据抽取至kafka 1、表输入 点击新建…

INS风格户外风光旅拍人像自拍摄影Lr调色教程,手机滤镜PS+Lightroom预设下载!

调色教程 户外风光旅拍人像自拍摄影结合 Lightroom 调色&#xff0c;可以打造出令人惊艳的视觉效果。这种风格将自然风光与人像完美融合&#xff0c;强调色彩的和谐与氛围感的营造。 预设信息 调色风格&#xff1a;INS风格预设适合类型&#xff1a;人像&#xff0c;户外&…

【burp】burpsuite基础(八)

Burp Suite基础&#xff08;八&#xff09; 声明&#xff1a;该笔记为up主 泷羽的课程笔记&#xff0c;本节链接指路。 警告&#xff1a;本教程仅作学习用途&#xff0c;若有用于非法行为的&#xff0c;概不负责。 ip伪装 安装组件jython 下载好后&#xff0c;在burp中打开扩展…

《船舶物资与市场》是什么级别的期刊?是正规期刊吗?能评职称吗?

问题解答 问&#xff1a;《船舶物资与市场》是不是核心期刊&#xff1f; 答&#xff1a;不是&#xff0c;是知网收录的正规学术期刊。 问&#xff1a;《船舶物资与市场》级别&#xff1f; 答&#xff1a;国家级。主管单位&#xff1a;中国船舶集团有限公司 主办单…

【电子通识】案例:USB Type-C USB 3.0线缆做直通连接器TX/RX反向

【电子通识】案例:连接器接线顺序评估为什么新人总是评估不到位?-CSDN博客这个文章的后续。最近在做一个工装项目,需要用到USB Type-C线缆做连接。 此前已经做好了线序规划,结果新人做成实物后发现有的USB Type-C线缆可用,有的不行。其中发现USB3.0的TX-RX信号与自己的板卡…

Antd X : 迅速搭建 AI 页面的解决方案

前言 随着 AI 热度的水涨船高&#xff0c;越来越多的 AI 应用如井喷式爆发&#xff0c;那么如何迅速搭建一个 AI 应用的美观高质量 Web 前端页面呢&#xff0c; Antd 团队给出了一个解决方案。 X Ant DesIgn XAI 体验新秩序Ant Design 团队匠心呈现 RICH 设计范式&#xff0…

自建服务器,数据安全有保障

在远程桌面工具的选择上&#xff0c;向日葵和TeamViewer功能强大&#xff0c;但都存在收费昂贵、依赖第三方服务器、数据隐私难以完全掌控等问题。相比之下&#xff0c;RustDesk 凭借开源免费、自建服务的特性脱颖而出&#xff01;用户可以在自己的服务器上部署RustDesk服务端&…

[Collection与数据结构] 位图与布隆过滤器

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏: &#x1f9ca; Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 &#x1f355; Collection与…

php 系统函数 记录

PHP intval() 函数 PHP函数介绍—array_key_exists(): 检查数组中是否存在特定键名 如何使用PHP中的parse_url函数解析URL PHP is_array()函数详解&#xff0c;PHP判断是否为数组 PHP函数介绍&#xff1a;in_array()函数 strpos定义和用法 strpos() 函数查找字符串在另一字符串…

重生之我在异世界学编程之C语言:深入位段篇

大家好&#xff0c;这里是小编的博客频道 小编的博客&#xff1a;就爱学编程 很高兴在CSDN这个大家庭与大家相识&#xff0c;希望能在这里与大家共同进步&#xff0c;共同收获更好的自己&#xff01;&#xff01;&#xff01; 本文目录 引言正文一 位段的基本使用&#xff08;1…

2. 读取文件

题目4: 读取excel 文件2_1People,查看数据结构(行与列数,列名),观察数据内容(前3行与后3行) import pandas as pd# 题目4: 读取excel 文件2_1People,查看数据结构(行与列数,列名),观察数据内容(前3行与后3行) people pd.read_excel(2_1People.xlsx) print(people.shape) #…

【Mac】安装Gradle

1、说明 Gradle 运行依赖 JVM&#xff0c;需要先安装JDK&#xff0c;Gradle 与 JDK的版本对应参见&#xff1a;Java Compatibility IDEA的版本也是有要求Gradle版本的&#xff0c;二者版本对应关系参见&#xff1a;Third-Party Software and Licenses 本次 Gradle 安装版本为…