我的编程空间,编程开发者的网络收藏夹
学习永远不晚

GolangWaitGroup实现原理解析

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

GolangWaitGroup实现原理解析

原理解析

type WaitGroup struct {
   noCopy noCopy
   // 64-bit value: high 32 bits are counter, low 32 bits are waiter count.
   // 64-bit atomic operations require 64-bit alignment, but 32-bit
   // compilers only guarantee that 64-bit fields are 32-bit aligned.
   // For this reason on 32 bit architectures we need to check in state()
   // if state1 is aligned or not, and dynamically "swap" the field order if
   // needed.
   state1 uint64
   state2 uint32
}

其中 noCopy 是 golang 源码中检测禁止拷贝的技术。如果程序中有 WaitGroup 的赋值行为,使用 go vet 检查程序时,就会发现有报错。但需要注意的是,noCopy 不会影响程序正常的编译和运行。

state1字段

  • 高32位为counter,代表目前尚未完成的协程个数。
  • 低32位为waiter,代表目前已调用 Wait 的 goroutine 的个数,因为wait可以被多个协程调用。

state2为信号量。

WaitGroup 的整个调用过程可以简单地描述成下面这样:

  • 当调用 WaitGroup.Add(n) 时,counter 将会自增: counter + n
  • 当调用 WaitGroup.Wait() 时,会将 waiter++。同时调用 runtime_Semacquire(semap), 增加信号量,并挂起当前 goroutine。
  • 当调用 WaitGroup.Done() 时,将会 counter--。如果自减后的 counter 等于 0,说明 WaitGroup 的等待过程已经结束,则需要调用 runtime_Semrelease 释放信号量,唤醒正在 WaitGroup.Wait 的 goroutine。

关于内存对其

func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {
	if unsafe.Alignof(wg.state1) == 8 || uintptr(unsafe.Pointer(&wg.state1))%8 == 0 {
		// state1 is 64-bit aligned: nothing to do.
		return &wg.state1, &wg.state2
	} else {
		// state1 is 32-bit aligned but not 64-bit aligned: this means that
		// (&state1)+4 is 64-bit aligned.
		state := (*[3]uint32)(unsafe.Pointer(&wg.state1))
		return (*uint64)(unsafe.Pointer(&state[1])), &state[0]
	}
}

如果变量是 64 位对齐 (8 byte), 则该变量的起始地址是 8 的倍数。如果变量是 32 位对齐 (4 byte),则该变量的起始地址是 4 的倍数。

state1 是 32 位的时候,那么state1被当成是一个数组[3]uint32,数组的第一位是semap,第二三位存储着counter, waiter正好是64位。

为什么会有这种奇怪的设定呢?这里涉及两个前提:

前提 1:在 WaitGroup 的真实逻辑中, counter 和 waiter 被合在了一起,当成一个 64 位的整数对外使用。当需要变化 counter 和 waiter 的值的时候,也是通过 atomic 来原子操作这个 64 位整数。

前提 2:在 32 位系统下,如果使用 atomic 对 64 位变量进行原子操作,调用者需要自行保证变量的 64 位对齐,否则将会出现异常。golang 的官方文档 sync/atomic/#pkg-note-BUG 原文是这么说的:

On ARM, x86-32, and 32-bit MIPS, it is the caller’s responsibility to arrange for 64-bit alignment of 64-bit words accessed atomically. The first word in a variable or in an allocated struct, array, or slice can be relied upon to be 64-bit aligned.

因此,在前提 1 的情况下,WaitGroup 需要对 64 位进行原子操作。根据前提 2,WaitGroup 需要自行保证 count+waiter 的 64 位对齐。

这个方法非常的巧妙,只不过是改变 semap 的位置顺序,就既可以保证 counter+waiter 一定会 64 位对齐,也可以保证内存的高效利用。

注: 有些文章会讲到,WaitGroup 两种不同的内存布局方式是 32 位系统和 64 位系统的区别,这其实不太严谨。准确的说法是 32 位对齐和 64 位对齐的区别。因为在 32 位系统下,state1 变量也有可能恰好符合 64 位对齐。

sync.mutex的源码中就没有出现内存对其的操作,虽然它也有大量的atomic操作,那是因为state int32

sync.mutex中也是将四个状态存在一个变量地址,其实这么做的目的就是为了实现原子操作,因为没有办法同时修改多个变量还要保证原子性。

WaitGroup 直接把 counterwaiter 看成了一个统一的 64 位变量。其中 counter 是这个变量的高 32 位,waiter 是这个变量的低 32 位。 在需要改变 counter 时, 通过将累加值左移 32 位的方式。

这里的原子操作并没有使用Mutex或者RWMutex这样的锁,主要是因为锁会带来不小的性能损耗,存在上下文切换,而对于单个内存地址的原子操作最好的方式是atomic,因为这是由底层硬件提供的支持(CPU指令),粒度更小,性能更高。

源码部分

func (wg *WaitGroup) Add(delta int) {
    // wg.state()返回的是地址
	statep, semap := wg.state()
    // 原子操作,修改statep高32位的值,即counter的值
	state := atomic.AddUint64(statep, uint64(delta)<<32)
    // 右移32位,使高32位变成了低32,得到counter的值
	v := int32(state >> 32)
    // 直接取低32位,得到waiter的值
	w := uint32(state)
	// 不规范的操作
	if v < 0 {
		panic("sync: negative WaitGroup counter")
	}
    // 不规范的操作
	if w != 0 && delta > 0 && v == int32(delta) {
		panic("sync: WaitGroup misuse: Add called concurrently with Wait")
	}
    // 这是正常的情况
	if v > 0 || w == 0 {
		return
	}
    // 剩下的就是 counter == 0 且 waiter != 0 的情况
    // 在这个情况下,*statep 的值就是 waiter 的值,否则就有问题
    // 在这个情况下,所有的任务都已经完成,可以将 *statep 整个置0
    // 同时向所有的Waiter释放信号量
	// This goroutine has set counter to 0 when waiters > 0.
	// Now there can't be concurrent mutations of state:
	// - Adds must not happen concurrently with Wait,
	// - Wait does not increment waiters if it sees counter == 0.
	// Still do a cheap sanity check to detect WaitGroup misuse.
	if *statep != state {
		panic("sync: WaitGroup misuse: Add called concurrently with Wait")
	}
	// Reset waiters count to 0.
	*statep = 0
	for ; w != 0; w-- {
		runtime_Semrelease(semap, false, 0)
	}
}
func (wg *WaitGroup) Done() {
	wg.Add(-1)
}
func (wg *WaitGroup) Wait() {
    // wg.state()返回的是地址
	statep, semap := wg.state()
    // for循环是配合CAS操作
	for {
		state := atomic.LoadUint64(statep)
		v := int32(state >> 32) // counter
		w := uint32(state) // waiter
        // 如果counter为0,说明所有的任务在调用Wait的时候就已经完成了,直接退出
        // 这就要求,必须在同步的情况下调用Add(),否则Wait可能先退出了
		if v == 0 {
			return
		}
		// waiter++,原子操作
		if atomic.CompareAndSwapUint64(statep, state, state+1) {
            // 如果自增成功,则获取信号量,此处信号量起到了同步的作用
			runtime_Semacquire(semap)
			return
		}
	}
}

总结一下,WaitGroup 的原理就五个点:内存对齐,原子操作,counter,waiter,信号量。

  • 内存对齐的作用是为了原子操作。
  • counter的增减使用原子操作,counter的作用是一旦为0就释放全部信号量。
  • waiter的自增使用原子操作,waiter的作用是表明要释放多少信号量。

到此这篇关于Golang WaitGroup实现原理解析的文章就介绍到这了,更多相关Go WaitGroup内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

GolangWaitGroup实现原理解析

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

GolangWaitGroup实现原理解析

WaitGroup是Golang并发的两种方式之一,一个是Channel,另一个是WaitGroup,下面这篇文章主要给大家介绍了关于golang基础之waitgroup用法以及使用要点的相关资料,需要的朋友可以参考下
2023-02-03

gosyncOnce实现原理示例解析

这篇文章主要为大家介绍了gosyncOnce实现原理示例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-03

java LockSupport实现原理示例解析

这篇文章主要为大家介绍了java LockSupport实现原理示例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-09

reduce探索lodash.reduce实现原理解析

这篇文章主要介绍了reduce探索lodash.reduce实现原理解析
2023-02-27

async-validator实现原理源码解析

这篇文章主要为大家介绍了async-validator实现原理源码解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-11

解析Vue2实现composition API的原理

自从 Vue3 发布之后,composition API 这个词走入写 Vue 同学的视野之中,相信大家也一直听到 composition API 比之前的 options API 有多好多强,如今由于 @vue/composition-api 插件的发布,Vue2 的同学也可以上车咯,接下来我们主要以响应式的 ref 和 reactive 来深入分析一下,这个插件是怎么实现此
2023-05-14

AndroidFlutter实现原理浅析

这篇文章主要介绍了AndroidFlutter的实现原理是怎么样的,flutter可以说是当下最流行的跨平台技术了,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
2022-11-13

Gochannel实现原理分析

Channel是go语言内置的一个非常重要的特性,也是go并发编程的两大基石之一,下面这篇文章主要给大家介绍了关于Go中channel的相关资料,需要的朋友可以参考下
2023-05-14

goslice扩容实现原理源码解析

这篇文章主要为大家介绍了goslice扩容实现原理源码解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-03

java Semaphore共享锁实现原理解析

这篇文章主要为大家介绍了Semaphore共享锁实现原理解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-09

深入解析MySQL MVCC 原理与实现

深入解析MySQL MVCC 原理与实现MySQL是目前最流行的关系型数据库管理系统之一,它提供了多版本并发控制(Multiversion Concurrency Control,MVCC)机制来支持高效并发处理。MVCC是一种在数据库中处
2023-10-22

Golangmap实现原理浅析

Go中Map是一个KV对集合,下面这篇文章主要给大家介绍了关于Golang中map探究的相关资料,文中通过实例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
2022-12-16

一文解析MySQL的MVCC实现原理

目录1. 什么是MVCC2. 事务的隔离级别3. Undo Log(回滚日志)4. MVCC的实现原理4.1 当前读和快照读4.2 隐藏字段4.3 版本链4.4 Read View(读视图)5. 不同隔离级别下可见性分析5.1 READ C
2022-08-16

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录