Go并发编程

2023-11-16

目录

一些基本概念

并发任务单元的状态

并发任务单元:进程,线程,协程

同步

异步

并发和并行

并发编程

创建并发任务

WaitGroup

等待goroutine结束

WaitGroup.Wait

WaitGroup.Add

获取CPU数量

获取Goroutine的编号和返回值

GOMAXPROCS

重新调度

终止任务

终止进程:os.Exit

终止当前任务:runtime.Goexit

通道 Channel

声明

发送、接收数据

等待goroutine结束

同步模式和异步模式

指针

cap & len:获取缓冲区大小和当前已缓冲数量

关闭通道:close

关闭同步通道:解除阻塞

关闭异步通道

ok-idom模式

range模式


Go的并发绝对称得上是Go的一大特色。Go使用类似协程的方式来处理并发单元,却又在运行时层面做了更深度的优化处理。这使得语法上的并发编程变得极为容易,仅仅使用关键字go就可以创建一个goroutine(并发任务单元)。然而简便的并发带来的是控制上的难度,而Go的并发编程也足以写出一部篇幅不少的大作。本篇博客用较为浅显的例子总结下Go并发编程中常见的问题和解决方案。

一些基本概念

在开启Go并发编程之前,我们先来总结一些常见的概念。这些概念在并发编程中或多或少都会用到。

并发任务单元的状态

一个并发任务单元的生命周期,从新建开始,包括就绪、运行、阻塞终止。阻塞指的是数据未准备就绪,该并发任务单元一直等待。例如某个运行中的并发任务单元需要使用一个资源,但不巧的是该资源被其它并发任务单元占用,因此该并发任务单元就会进入阻塞状态,一直等待该资源被释放。

并发任务单元:进程,线程,协程

进程、线程和协程是我们经常听到的三个概念。它们都是并发任务单元。

  • 进程:进程是指一个程序在给定数据集合上的一次执行过程,是系统进行资源分配和运行调用的独立单位。可以简单的理解为操作系统中正在执行的程序。
  • 线程:线程是由进程创建的。进程启动时会最先创建一个线程,即主线程。主线程可以创建其它的子线程,因此一个进程可以包含一个或多个线程。线程必须在某个进程中执行,一个进程内的多个线程共享该进程所拥有的所有数据资源,例如打开的文件、同一个地址空间、甚至是进程所拥有的硬件设备(物理内存、磁盘、打印机等)。
  • 协程:协程也被称作微线程,它的资源开销比线程更小。而go关键字创建的并发任务单元可以简单的理解为是一个协程。

同步

在发起一个调用时,在没有得到结果之前,该过程会一直等待,直到该调用返回。例如调用一个函数,在该函数没有返回结果之前(哪怕该函数本身没有返回值),调用者会一直等待该函数返回(即执行结束)。

异步

调用者在发起一个调用后,不必等待该调用是否执行完毕,这就是异步。

并发和并行

我们经常提到并发和并行的概念,但经常容易混淆二者的意思。先来看概念:

并发:逻辑上具备同时处理多个任务的能力

并行:物理上在同一时刻执行多个并发任务

我们通常说的程序是并发设计的,指的是所设计的程序允许多个任务同时执行。但实际上往往并不是我们所期望的那样。例如在单核处理器上,多个任务只能以间隔的方式切换执行。并行依赖多核处理器等物理设备,也就是说,并行是并发设计的理想执行模式。

并发编程

在说完上面常见的名词之后,我们来看看Go的并发编程。

创建并发任务

只需在函数调用前添加关键字go即可实现并发任务单元goroutine的创建:

package main

import "fmt"

func main() {
	go fmt.Println("hello, world!")
	
	go func(message string) {
		fmt.Println(message)
	} ("hello world!")
}

需要注意的是关键字go并非执行并发操作,而是创建了一个并发任务单元。创建后任务会被放置在系统队列中,等待调度器安排合适的系统线程去获取执行权。并发任务单元在运行时不保证彼此之间的执行顺序。

当有多个逻辑处理器时,调度器会将 goroutine 平等分配到每个逻辑处理器上。 这会让 goroutine 在不同的线程上运行。 不过要想真的实现并行的效果,用户需要让自己的程序运行在有多个物理处理器的机器上。 否则,哪怕 Go语言运行时使用多个线程,goroutine 依然会在同一个物理处理器上并发运行,达不到并行的效果。

与defer一样,goroutine在创建时会立即计算并复制执行参数:

package main

import (
	"fmt"
	"time"
)

// 默认值是0。
var c int

func counter() int {
	c++
	return c
}

func main() {
	a := 100

	// 使用真实的值传入
	go func(x, y int) {
		// 利用time.Sleep将goroutine阻塞1秒,使goroutine的逻辑在main之后运行。
		// 这里不是一个好的方式来阻塞goroutine。后面章节会介绍更好的方案来控制goroutine的执行顺序。
		time.Sleep(time.Second)
		fmt.Println("goroutine1: ", x, y)
	}(a, counter())

	// 换成指针
	go func(x, y *int) {
		// 让该goroutine最后执行。
		time.Sleep(time.Second * 2)
		fmt.Println("goroutine2: ", *x, *y)
	}(&a, &c)

	a += 100
	fmt.Println("main: ", a, counter())

	c = 23

	// 等待两个goroutine执行结束。
	// 这里也不是一个推荐方法,后续章节会详细介绍如何等待goroutine结束。
	time.Sleep(time.Second * 3)

	// 程序输出
	// main:  200 2
	// goroutine1:  100 1
	// goroutine2:  200 23
}

WaitGroup

WaitGroup的常见作用是等待goroutine的结束。因为进程退出时不会等待goroutine结束,因此当main函数退出时,goroutine可能还没有开始执行:

package main

import (
	"fmt"
	"time"
)

func main() {
	go func() {
		time.Sleep(time.Second)
		fmt.Println("Inside the goroutine.")
	}()

	fmt.Println("exit...")
	
	// 输出:
	// exit...
}

上一个小节“创建并发任务”中,main函数使用time.Sleep来等待所有goroutine结束。这虽然算是一个行之有效的方式,但并不推荐。因为在实际开发中,我们并不会严格知道goroutine的所用时间。

等待goroutine结束

如要等待多个任务结束,sync.WaitGroup是一个推荐的选择。通过设定计数器,让每个goroutine在退出前递减,直至归零时解除阻塞。

package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	max       = 10
	waitGroup sync.WaitGroup
)

func main() {
	// 累加计数
	waitGroup.Add(max)
	for i := 0; i < max; i++ {
		go func(index int) {
			// 递减计数
			defer waitGroup.Done()
            // 这里的阻塞是为了保证main函数中 “Inside main function”的打印先于所有goroutine执行
			time.Sleep(time.Second)
			fmt.Println("goroutine: ", index)
		}(i)
	}

	fmt.Println("Inside main function.")

	// 此时main阻塞,直到计数归零
	waitGroup.Wait()

	fmt.Println("main exit.")

	// 程序输出:
	// Inside main function.
	// goroutine:  7
	// goroutine:  1
	// goroutine:  3
	// goroutine:  8
	// goroutine:  0
	// goroutine:  2
	// goroutine:  6
	// goroutine:  5
	// goroutine:  9
	// goroutine:  4
	// main exit.
}

WaitGroup.Wait

Wait可以在多处阻塞,它们都能接收到通知。

在上面的例子中,我们为了保证goroutine在main函数"Inside main function"打印之后执行,在goroutine内部添加了time.Sleep。其实我们可以利用Wait的机制避免使用time.Sleep这种粗暴的手段:

package main

import (
	"fmt"
	"sync"
)

var (
	max       = 10
	waitGroup sync.WaitGroup
	start     sync.WaitGroup
)

func main() {
	// 累加计数
	start.Add(1)
	waitGroup.Add(max)
	for i := 0; i < max; i++ {
		go func(index int) {
			// 递减计数
			defer waitGroup.Done()
			// 保证main函数中 “Inside main function”的打印先于所有goroutine执行
			start.Wait()
			fmt.Println("goroutine: ", index)
		}(i)
	}

	fmt.Println("Inside main function.")
	start.Done()

	// 此时main阻塞,直到计数归零
	waitGroup.Wait()

	fmt.Println("main exit.")
}

WaitGroup.Add

尽管WaitGroup.Add实现了原子操作,但建议在goroutine外使用。以免Add尚未执行,Wait已经退出。

package main

import (
	"fmt"
	"sync"
)

var (
	waitGroup sync.WaitGroup
)

func main() {
	go func() {
		// 来不及设置
		waitGroup.Add(1)
		fmt.Println("Inside the goroutine.")
		waitGroup.Done()
	}()

	waitGroup.Wait()
	fmt.Println("main exit.")
	// 程序输出:
	// main exit.
}

获取CPU数量

runtime.NumCPU函数返回一个int型数据,表示当前执行机器的CPU数量。

package main

import (
	"fmt"
	"runtime"
)

func main() {
	fmt.Println(runtime.NumCPU())
}

获取Goroutine的编号和返回值

使用go关键字创建的goroutine无法像普通函数调用那样获取返回值。所创建的goroutine也不能获知并发任务的编号。这些问题我们可以使用本地存储的方式解决。

package main

import (
	"fmt"
	"sync"
)

var (
	size      = 10
	waitGroup sync.WaitGroup
)

// 存储goroutine ID和返回结果的本地存储
type LocalStorage struct {
	ID     int
	Result interface{}
}

func main() {
	pool := make([]LocalStorage, size)

	for i := 0; i < size; i++ {
		waitGroup.Add(1)

		go func(id int) {
			defer waitGroup.Done()

			// 使用id*3拟定一个执行结果
			pool[id].ID = id
			pool[id].Result = id * 3
		}(i)
	}

	waitGroup.Wait()

	fmt.Printf("%+v\n", pool)
	// [{ID:0 Result:0} {ID:1 Result:3} {ID:2 Result:6} {ID:3 Result:9} {ID:4 Result:12} {ID:5 Result:15} {ID:6 Result:18} {ID:7 Result:21} {ID:8 Result:24} {ID:9 Result:27}]
}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Go并发编程 的相关文章

  • go struct{} 空结构体的特点和作用

    空结构体的特点和作用 参考代码 package main import fmt unsafe func main empStruct 空结构体的实例和作用 func empStruct 空结构体的特点 1 不占用内存 2 地址不变 var
  • go踩坑——no required module provides package go.mod file not found in current directory or any parent

    背景 准备运行下面代码 package main import github com gin gonic gin func main 创建一个默认的路由引擎 r gin Default GET 请求方式 hello 请求的路径 当客户端以G
  • golang sleep

    golang的休眠可以使用time包中的sleep 函数原型为 func Sleep d Duration 其中的Duration定义为 type Duration int64 Duration的单位为 nanosecond 为了便于使用
  • Golang连接Jenkins获取Job Build状态及相关信息

    文章目录 1 连接Jenkins 2 controller 3 module 4 router 5 效果展示 第三方包 gojenkins 方法文档 gojenkins docs 实现起来很简单 利用第三方库 连接jenkins 调用相关方
  • 【golang】error parsing regexp: invalid or unsupported Perl syntax (正则表达式校验密码)

    要在 Go 中编写密码校验规则 确保密码不少于8位且包含数字和字母 你可以使用正则表达式和 Go 的 regexp 包来实现 以下是一个示例代码 错误示范 package main import fmt regexp func valida
  • Go 语言输出文本函数详解

    Go语言拥有三个用于输出文本的函数 Print Println Printf Print 函数以其默认格式打印其参数 示例 打印 i 和 j 的值 package main import fmt func main var i j stri
  • 为什么最近听说 Go 岗位很少很难?

    大家好 我是煎鱼 其实这个话题已经躺在我的 TODO 里很久了 近来很多社区的小伙伴都私下来交流 也有在朋友圈看到朋友吐槽 Go 上海的大会没什么人 还不如 Rust 大会 比较尴尬 今天主要是看看为什么 Go 岗位看起来近来很难的样子 也
  • Go 程序编译过程(基于 Go1.21)

    版本说明 Go 1 21 官方文档 Go 语言官方文档详细阐述了 Go 语言编译器的具体执行过程 Go1 21 版本可以看这个 https github com golang go tree release branch go1 21 sr
  • 【go语言开发】编写单元测试

    本文主要介绍使用go语言编写单元测试用例 首先介绍如何编写单元测试 然后介绍基本命令的使用 最后给出demo示例 文章目录 前言 命令 示例 前言 在go语言中编写单元测试时 使用说明 测试文件命名 在 Go 语言中 测试文件的命名应与被测
  • go-zero开发入门之网关往rpc服务传递数据2

    go zero 的网关服务实际是个 go zero 的 API 服务 也就是一个 http 服务 或者说 rest 服务 http 转 grpc 使用了开源的 grpcurl 库 当网关需要往 rpc 服务传递额外的数据 比如鉴权数据的时候
  • go-zero目录结构和说明

    code of conduct md 行为准则 CONTRIBUTING md 贡献指南 core 框架的核心组件 bloom 布隆过滤器 用于检测一个元素是否在一个集合中 breaker 熔断器 用于防止过多的请求导致系统崩溃 cmdli
  • go-zero开发入门之网关往rpc服务传递数据1

    go zero 的网关往 rpc 服务传递数据时 可以使用 headers 但需要注意前缀规则 否则会发现数据传递不过去 或者对方取不到数据 go zero 的网关对服务的调用使用了第三方库 grpcurl 入口函数为 InvokeRPC
  • go-zero 的 etcd 配置

    实现代码在 core discov config go 文件中 type EtcdConf struct Hosts string Key string ID int64 json optional User string json opt
  • Go 语言中切片的使用和理解

    切片与数组类似 但更强大和灵活 与数组一样 切片也用于在单个变量中存储相同类型的多个值 然而 与数组不同的是 切片的长度可以根据需要增长和缩小 在 Go 中 有几种创建切片的方法 使用 datatype values 格式 从数组创建切片
  • 【golang】go执行shell命令行的方法( exec.Command )

    所需包 import os exec cmd 的用法 cmd exec Command ls lah ls是命令 后面是参数 e cmd Run 多个参数的要分开传入 如 ip link show bond0 cmd
  • 【go语言】error错误机制及自定义错误返回类型

    简介 Go 语言通过内置的 error 接口来处理错误 该接口定义如下 type error interface Error string 这意味着任何实现了 Error 方法的类型都可以作为错误类型 在 Go 中 通常使用 errors
  • go开发--操作mysql数据库

    在 Go 中访问 MySQL 数据库并进行读写操作通常需要使用第三方的 MySQL 驱动 Go 中常用的 MySQL 驱动有 github com go sql driver mysql 和 github com go xorm xorm
  • Go、Docker、云原生学习笔记全攻略:从零开始,一步步走向精通!(2024版)

    第一章 Go语言学习宝典 一 介绍 01 Go 语言的前生今世 二 开发环境搭建 01 Go 语言开发环境搭建 三 初识GO语言 01 Go 多版本管理工具 02 第一个 Go 程序 hello world 与 main 函数 03 Go
  • 【go语言】读取toml文件

    一 简介 TOML 全称为Tom s Obvious Minimal Language 是一种易读的配置文件格式 旨在成为一个极简的数据序列化语言 TOML的设计原则之一是保持简洁性 易读性 同时提供足够的灵活性以满足各种应用场景 TOML
  • 【go语言】AST抽象语法树详解&实践之扫描代码生成错误码文档

    背景 为了能识别出代码中抛出错误码的地址和具体的错误码值 再根据错误码文件获取到错误码的具体值和注释 方便后续的排错 这里使用AST进行语法分析获取到代码中的目标对象 一 编译过程 在开始解析代码之前先补充了解一下编译过程 编译过程是将高级

随机推荐

  • 使用http动词篡改的认证旁路

    文章目录 一 漏洞描述 二 解决建议 三 解决方法 Springboot 配置文件增加配置 编写配置类 编写过滤器 提示 以下是本篇文章正文内容 下面案例可供参考 一 漏洞描述 可能会升级用户特权并通过 Web 应用程序获取管理许可权可能会
  • C++小坑:问号表达式的输出

    文章目录 发现问题 解决方案 发现问题 本来只是想写这样一个测试是否连接成功的判断 std cout lt lt Result gt lt lt Avaliable hey you got it hell suck it lt lt std
  • DSView源码阅读笔记(持续更新中···)

    一 DSView源码阅读笔记 主线任务 将源码成功编译运行 提取示波器功能代码 添加示波器通道数量 找到接收数据部分源码 在win平台上使用qt开发环境进行代码重构 支线任务 以下笔记内容部分是猜测内容 DSView pv mainwind
  • RMS正则化 和 STD正则化 的一些见解

    研究styleganv2过程中 记录下它使用的正则化方法的一些见解 RMS 方均根 STD 标准差 stylegan 中的 pixel norm 是 RMS正则化 常见的BN层 IN层 用的是STD 在不减均值的情况下 RMS正则化公式 t
  • 针对于QT5下找不到QApplication头文件的问题界解决

    感谢前辈的总结 这里用了CTRL C CTRL V进行操作 原地址 http bbs csdn net topics 380130389 老版本 C C code 1 2 include
  • 【计算机网络】数据通信的基础知识

    通信系统的一般模型 数据通信系统的组成部分 源点 信源 产生数据 如从键盘输入 产生数字比特流 发送器 对数字比特流进行编码 如调制器 信道 是信号传输的通道 可能是一条简易的传输线路 也可能是一个复杂的网络 接收器 设备的功能与发送设备相
  • 保姆式教学-实现天空盒旋转

    目录 一 天空盒材质设置 1 在菜单栏window gt Rendering gt lighting 2 设置天空盒子材质 替换默认材质 3 认识Rotation变量 二 代码实现让天空盒转起来 在一个小Unity项目中 需要将天空盒旋转
  • 将MATLAB环境下深度学习目标检测模型部署在Jetson TX2开发板

    摘要 在MATLAB2019b环境下训练深度学习目标检测模型 利用MATLABcoder和GPUcoder生成c 代码和CUDA代码 并部署在NVIDIA Jetson TX2开发板上运行 1 利用NVIDIA SDK manager对TX
  • Python的heapq堆模块

    heapq模块 一 heapq内置模块 二 heapq 模块的使用 1 创建堆方法 2 访问堆的方法 2 获取堆的最大值 最小值方法 总结 一 heapq内置模块 Python中的heapq模块提供了一种堆队列heapq类型 这样实现堆排序
  • Java线程与操作系统线程的关系

    操作系统的线程 Linux操作系统启动一个线程 int pthread create pthread t thread const pthread attr t attr void start routine void void arg 再
  • UE4-DeltaTime(时间增量)

    UE4 DeltaTime 时间增量 Time 2020年10月14日13 33 52 Author Yblackd UE4 DeltaTime 1 结论 2 deltaTime 增量时间 3 为什么乘以 时间增量 4 注意误区 5 参考
  • Linux:Xorg占用现存过大问题

    usr lib xorg Xorg占用3692 MB显存 导致程序出现CUDA out of memory问题 解决方案 1 Ctrl Alt F1 F7 关闭图形界面 输入用户名 密码 输入nvidia smi查看GPU使用情况 发现明显
  • 中国电子信息制造产业运营模式及未来投资方向建议报告2022版

    中国电子信息制造产业运营模式及未来投资方向建议报告2022版 修订日期 2022年2月 出版单位 鸿晟信合研究院 对接人员 周文文 内容分析有删减 了解详情可查看咨询鸿晟信合研究院专员 目录 第1章 中国电子信息制造业发展环境分析 1 1
  • stable diffusion webui 教程:安装与入门

    stable diffusion webui 安装与入门 原理简介 一 源码仓库 二 模型库地址 三 在 Windows 上自动安装步骤 安装Python 安装git 下载源代码 编辑 webui user bat 四 如何打开 五 依据文
  • PHP 创建派生类对象时基类部分问题

    之前我以为在派生类的构造函数中 在调用基类的构造函数前是不能使用基类成员的 因为基类对象还未构造 其中的成员也不存在 但在以下测试中发现 在调用基类的构造函数前基类中的成员已经存在 基类构造函数只是改变了基类中成员的值 class base
  • Android Studio3.4.2新建C++项目,CMakeLists批量添加代码编译不过的坑

    上段时间升级了AS到3 4 2 最后新建了个C 的项目 然后生成的那个native lib cpp文件就可以编译 但是我的项目里 C 代码文件非常多 显然一个一个地添加太慢了 然后就想批量添加进去 但总是编译不过 真是orz 像上图这样 批
  • php微信token存储,php获取微信公众帐号access_token存储并长期使用

    header Content type text html charset utf 8 apitest new GetWeixinToken apitest gt cacheData weixin access token 获取微信公众号的
  • CSDN接入AIGC辅助创作,对此你怎么看?

    catalogue 写在前面 GitChat 百万粉丝计划 CSDN接入AIGC 写在最后 写在前面 哈喽 大家好 我是几何心凉 这是一份全新的专栏 得到CSDN王总的授权 来对于我们每周四的绿萝时间 直达CSDN 直播内容进行总结概括 让
  • emacs 选中对齐快捷键

    Alt H 选中段落 Ctrl Alt 对齐
  • Go并发编程

    目录 一些基本概念 并发任务单元的状态 并发任务单元 进程 线程 协程 同步 异步 并发和并行 并发编程 创建并发任务 WaitGroup 等待goroutine结束 WaitGroup Wait WaitGroup Add 获取CPU数量