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并发编程 的相关文章

  • 使用Docker registry镜像创建私有仓库

    2015 01 25 wcdj 摘要 安装Docker后 可以通过官方提供的registry镜像来简单搭建一套本地私有仓库环境 本文记录简单的搭建过程 1 使用registry启动私有仓库的容器 docker run d p 5000 50
  • 【Golang入门】Golang第一天心得

    生活所迫 入门一下Go 很奇葩的第一点 接口 package main import fmt 定义一个接口 type Shape interface Area float64 定义一个矩形类型 type Rectangle struct W
  • Go开发命令行程序指南

    近期在Twitter上看到一个名为 Command Line Interface Guidelines 的站点 1 这个站点汇聚了帮助大家编写出更好命令行程序的哲学与指南 这份指南基于传统的Unix编程原则 2 又结合现代的情况进行了 与时
  • Golang适合高并发场景的原因分析

    典型的两个现实案例 我们先看两个用Go做消息推送的案例实际处理能力 360消息推送的数据 16台机器 标配 24个硬件线程 64GB内存 Linux Kernel 2 6 32 x86 64 单机80万并发连接 load 0 2 0 4 C
  • Golang-使用 goroutine 运行闭包的“坑”

    介绍 在 Go 语言中 函数支持匿名函数 闭包就是一种特殊的匿名函数 它可以用于访问函数体外部的变量 需要注意的是 在 for range 中 使用 goroutine 执行闭包时 经常会掉 坑 因为匿名函数可以访问函数体外部的变量 而 f
  • Go_关键字、编译、转义字符

    关键字 关键字是指被go语言赋予了特殊含义的单词 共25个 关键字不能用于自定义名字 只能在特定语法结构中使用 break default func interface select case defer go map struct cha
  • 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 调用相关方
  • go 进阶 go-zero相关: 七. 拦截器与熔断拦截器

    目录 一 拦截器的基础使用 1 服务端拦截器 2 客户端拦截器 二 拦截器底层底层执行原理 三 go zero默认添加的拦截器 客户端 1 熔断器拦截器 BreakerInterceptor 服务端 一 拦截器的基础使用 在go zero
  • go 进阶 gin实战相关: 五. gin_scaffold 企业脚手架

    目录 一 gin scaffold 企业级脚手架 二 gin scaffold 脚手架安装及使用演示 文件分层解释 开始使用 1 配置开启go mod 功能 2 下载 安装 gin scaffold 3 整合 golang common 4
  • Golang三剑客之Pflag、Viper、Cobra

    如何构建应用框架 想知道如何构建应用框架 首先你要明白 一个应用框架包含哪些部分 在我看来 一个应用框架需要包含以下 3 个部分 命令行参数解析 主要用来解析命令行参数 这些命令行参数可以影响命令的运行效果 配置文件解析 一个大型应用 通常
  • 48.Go简要实现令牌桶限流与熔断器并集成到Gin框架中

    文章目录 一 简介 二 限流器与熔断器在微服务中的作用 1 限流器 对某个接口单位时间内的访问量做限制 2 熔断器 当服务连续报错 超过一定阈值时 打开熔断器使得服务不可用 三 具体实现 1 限流器实现逻辑 以令牌桶算法为例 2 限流器集成
  • 【go语言开发】loglus日志框架的使用

    本文将简单介绍loglus框架的基本使用 并给出demo 文章目录 前言 Loglus常见用法 自定义日志级别 使用字段钩子 输出到多个位置 使用钩子实现自定义日志处理 demo
  • go-zero开发入门-API网关鉴权开发示例

    本文是 go zero开发入门 API网关开发示例 一文的延伸 继续之前请先阅读此文 在项目根目录下创建子目录 middleware 在此目录下创建文件 auth go 内容如下 鉴权中间件 package middleware impor
  • “go mod tidy”之错误“not a valid zip file”

    执行 go mod tidy 时 遇到如下错误 rpc imports github com zeromicro go zero zrpc imports github com zeromicro go zero zrpc resolver
  • 【golang】go执行shell命令行的方法( exec.Command )

    所需包 import os exec cmd 的用法 cmd exec Command ls lah ls是命令 后面是参数 e cmd Run 多个参数的要分开传入 如 ip link show bond0 cmd
  • go语言实现文件夹上传前后端代码案例

    go语言实现文件夹上传前后端代码案例 前端用于上传的测试界面 如果上传的文件夹有子文件要遍历子文件夹创建出子文件夹再进行拷贝 需要获取文件名和对应的路径 将文件的相对路径和文件对象添加到FormData中 这几行代码很关键 for let
  • Golang拼接字符串性能对比

    g o l a n g golang g o l an g
  • Golang拼接字符串性能对比

    g o l a n g golang g o l an g
  • golang 生成一年的周数

    GetWeekTimeCycleForGBT74082005 获取星期周期 中华人民共和国国家标准 GB T 7408 2005 参数 year 年份 GB T 7408 2005 func GetWeekTimeCycleForGBT74

随机推荐

  • 使用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数量