14 go语言(golang) - 并发编程goroutine和channel

前言

并发编程是指在计算机程序中同时执行多个计算任务的技术。这种编程方式旨在利用多核处理器的计算能力,提高程序的执行效率和响应速度。而Go 语言的并发编程主要依赖于两个核心概念:Goroutine 和 Channel。

Goroutine

Goroutine 是 Go 语言中的一种轻量级线程管理机制。它是 Go 并发编程的核心特性之一,允许开发者以非常简单和高效的方式实现并发操作。

1、特点

  1. 轻量级:与传统的操作系统线程相比,goroutine 非常轻量。一个典型的 goroutine 只占用几 KB 的内存,而不是 MB 级别。这使得在同一时间可以运行成千上万个 goroutine,而不会对系统资源造成太大压力。

  2. 调度管理:Go 运行时包含了自己的调度器,用于管理 goroutines 的执行。这个调度器会将多个 goroutines 映射到少数几个 OS 线程上,从而有效地利用多核处理器。

  3. 简单启动:启动一个新的 goroutine 非常简单,只需要在函数调用前加上 go 关键字即可。

  4. 自动栈增长:goroutines 使用的是可增长的栈,这意味着它们开始时使用很小的内存,并根据需要动态扩展。这种特性进一步提高了其效率和灵活性。

  5. 与 Channel 配合使用:虽然 goroutines 可以独立工作,但通常会与 Channels 一起使用,以便在不同的 goroutines 间进行通信和同步。

2、示例

package main

import (
	"fmt"
	"testing"
)

func Test1(t *testing.T) {
	for i := 0; i < 10; i++ {
		go fmt.Printf("go线程 %d \n", i)
	}

	fmt.Println("主流程")
}

输出:

=== RUN   Test1
主流程
go线程 2 
go线程 3 
go线程 5 
--- PASS: Test1 (0.00s)
go线程 0 
go线程 7 
go线程 6 
go线程 8 
PASS
go线程 4 

多执行几次发现会少了一些打印,如 1和9线程

改进:

func Test2(t *testing.T) {
	for i := 0; i < 10; i++ {
		go fmt.Printf("go线程 %d \n", i)
	}

	time.Sleep(1 * time.Second)
	fmt.Println("主流程")
}

输出:

=== RUN   Test2
go线程 9 
go线程 0 
go线程 1 
go线程 2 
go线程 3 
go线程 4 
go线程 7 
go线程 8 
go线程 5 
go线程 6 
主流程
--- PASS: Test2 (1.00s)
PASS

3、waitGroup

考虑如何优雅的等待子线程执行完成,而不是简单的让主线程等待,常用的方法是使用 sync.WaitGroupWaitGroup 提供了一种简单而有效的方式来等待一组 goroutines 完成它们的工作,而不需要显式地让主线程休眠。它通过一个计数器来跟踪 goroutines 的数量。你可以增加或减少这个计数器,并且可以阻塞直到计数器变为零,这表示所有被跟踪的 goroutines 都已完成。

基本步骤

  1. 创建 WaitGroup 实例:在你的程序中创建一个 WaitGroup 实例。
  2. 增加计数:每启动一个新的 goroutine,就调用 Add(1) 来增加 WaitGroup 的计数。
  3. 标记完成:在每个 goroutine 内部,任务完成时调用 Done() 来减少 WaitGroup 的计数。
  4. 等待所有任务完成:在主线程中调用 Wait() 方法,它会阻塞直到所有被追踪的任务都标记为完成。
// 模拟工作的函数
func exec(num int, wg *sync.WaitGroup) {
	defer wg.Done() // 确保工作结束后通知 WaitGroup
	go fmt.Printf("go线程 %d \n", num)
}

func Test3(t *testing.T) {
	group := sync.WaitGroup{}

	// 启动多个goroutine并使用WaitGroup追踪它们
	for i := 0; i < 10; i++ {
		group.Add(1) // 增加WaitGroup计数
		go exec(i, &group)
	}

	group.Wait() // // 阻塞主线程,直到所有线程都执行完毕

	fmt.Println("主流程")
}

注意事项

  • 确保 Done 调用:始终确保每个启动的 goroutine 在其逻辑结束时调用了 Done() 方法。这通常通过使用关键字 defer 来实现,以确保即使发生错误也能正确递减。
  • 避免重复 Add 和 Done 操作:不要对同一组操作多次进行 Add 或 Done 调用,否则可能导致不一致状态和死锁。

Channel

在 Go 语言中,Channel 是一种用于 goroutine 之间进行通信的机制。它可以让一个 goroutine 发送特定类型的值到 Channel 中,然后另一个 goroutine 从该 Channel 接收值,从而实现数据的安全传递和同步。

1、基本特性

  1. 类型化:Channel 是类型化的,这意味着你需要指定要传输的数据类型。
  2. 阻塞行为
    • 发送阻塞:当一个 goroutine 向 Channel 发送数据时,如果没有其他 goroutine 正在等待接收这个数据,那么发送操作会被阻塞,直到有接收者。
    • 接收阻塞:同样地,当一个 goroutine 尝试从 Channel 接收数据时,如果没有其他 goroutine 正在向这个 Channel 发送数据,那么接收操作会被阻塞,直到有新的数据可用。
  3. 方向性:Channel 可以是双向的,也可以是单向(只读或只写)的。

2、创建和使用 Channel

func Test4(t *testing.T) {
	ch := make(chan int)

	for i := 0; i < 10; i++ {
		go send(i, ch)
	}

	for i := 0; i < 10; i++ {
		value := <-ch
		fmt.Printf("接受到消息:%d\n", value)
		time.Sleep(time.Second)
	}
}

func send(i int, ch chan int) {
	fmt.Printf("-- 准备发送:%d\n", i)
	ch <- i
	fmt.Printf("-- 发送成功!%d\n", i) // 这行打印会被阻塞
}

输出:

=== RUN   Test4
-- 准备发送:9
-- 发送成功!9
接受到消息:9
-- 准备发送:2
-- 准备发送:6
-- 准备发送:1
-- 准备发送:8
-- 准备发送:3
-- 准备发送:4
-- 准备发送:5
-- 准备发送:0
-- 准备发送:7
接受到消息:2
-- 发送成功!2
接受到消息:6
-- 发送成功!6
接受到消息:1
-- 发送成功!1
接受到消息:8
-- 发送成功!8
接受到消息:3
-- 发送成功!3
接受到消息:4
-- 发送成功!4
接受到消息:5
-- 发送成功!5
接受到消息:0
-- 发送成功!0
接受到消息:7
-- 发送成功!7
--- PASS: Test4 (10.01s)
PASS

3、带缓冲区的 Channel

带缓冲区的 Channels 可以存储一定数量的数据,而不会立即导致 sender 或 receiver 被阻塞。创建带缓冲区 Channels 时,可以指定其容量。

func Test5(t *testing.T) {
  
	ch := make(chan int, 5)

	for i := 0; i < 10; i++ {
		go send(i, ch)
	}

	for i := 0; i < 10; i++ {
		value := <-ch
		fmt.Printf("接受到消息:%d\n", value)
		time.Sleep(time.Second)
	}
}

输出:

=== RUN   Test5
-- 准备发送:9
-- 发送成功!9
接受到消息:9
-- 准备发送:0
-- 发送成功!0
-- 准备发送:1
-- 发送成功!1
-- 准备发送:2
-- 发送成功!2
-- 准备发送:3
-- 发送成功!3
-- 准备发送:7
-- 发送成功!7
-- 准备发送:8
-- 准备发送:6
-- 准备发送:4
-- 准备发送:5
接受到消息:0
-- 发送成功!8
接受到消息:1
-- 发送成功!6
接受到消息:2
-- 发送成功!4
接受到消息:3
-- 发送成功!5
接受到消息:7
接受到消息:8
接受到消息:6
接受到消息:4
接受到消息:5
--- PASS: Test5 (10.01s)
PASS

长度(Length)

  • 定义:Channel 的长度是指当前 Channel 中已存储的数据元素的数量。
  • 获取方式:可以使用内置函数 len(ch) 来获取 Channel 的当前长度。

容量(Capacity)

  • 定义:Channel 的容量是指它最多可以容纳多少个数据元素。对于无缓冲的 channel,容量为零;对于带缓冲的 channel,容量是在创建时指定的。
  • 获取方式:可以使用内置函数 cap(ch) 来获取 Channel 的总容量。
func TestChannelCapAndLen(t *testing.T) {
	ch := make(chan string, 3)
	ch <- "a"
	ch <- "b"
	fmt.Println("容量为:", cap(ch))
	fmt.Println("长度为:", len(ch))
	fmt.Println("读取一个元素:", <-ch)
	fmt.Println("新的长度为:", len(ch))
}

输出:

=== RUN   TestChannelCapAndLen
容量为: 3
长度为: 2
读取一个元素: a
新的长度为: 1
--- PASS: Test9 (0.00s)
PASS

4、单向 Channel

单向 Channel 用于限制函数对 channel 的访问权限,只能用于 send 或 receive 操作。这种限制可以帮助避免错误地使用 channel。

func sendOnly(i int, ch chan<- int) {
	fmt.Printf("-- 准备发送:%d\n", i)
	ch <- i
	fmt.Printf("-- 发送成功!%d\n", i)
}

func receiveOnly(ch <-chan int) {
	value := <-ch
	fmt.Printf("接受到消息:%d\n", value)
	time.Sleep(time.Second)
}

func Test6(t *testing.T) {
	ch := make(chan int)

	for i := 0; i < 10; i++ {
		go sendOnly(i, ch)
	}

	for i := 0; i < 10; i++ {
		receiveOnly(ch)
	}
}

5、关闭Channel

如果channel没有关闭,消费者仍然在等待数据,则可能导致死锁

func Test7(t *testing.T) {
	ch := make(chan int)

	for i := 0; i < 10; i++ {
		go sendOnly(i, ch)
	}

	// 消费者仍然在等待数据,则可能导致死锁
	for value := range ch {
		fmt.Printf("接受到消息:%d\n", value)
		time.Sleep(time.Second)
	}
}

报错:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
testing.(*T).Run(0xc00010c4e0, {0x3d35a17?, 0x125836780012db50?}, 0x3da35f0)
	/Users/fangyirui/sdk/go1.23.2/src/testing/testing.go:1751 +0x3ab
testing.runTests.func1(0xc00010c4e0)
	/Users/fangyirui/sdk/go1.23.2/src/testing/testing.go:2168 +0x37
testing.tRunner(0xc00010c4e0, 0xc00012dc70)
	/Users/fangyirui/sdk/go1.23.2/src/testing/testing.go:1690 +0xf4
testing.runTests(0xc000010030, {0x3e7cbe0, 0x9, 0x9}, {0x3c86030?, 0x3c85c9a?, 0x0?})
	/Users/fangyirui/sdk/go1.23.2/src/testing/testing.go:2166 +0x43d
testing.(*M).Run(0xc00007a0a0)
	/Users/fangyirui/sdk/go1.23.2/src/testing/testing.go:2034 +0x64a
main.main()
	_testmain.go:61 +0x9b

goroutine 5 [chan receive]:
awesomeProject/_28goroutine.Test7(0xc00010c680?)
	/Users/fangyirui/GolandProjects/awesomeProject/_28goroutine/1_test.go:114 +0x105
testing.tRunner(0xc00010c680, 0x3da35f0)
	/Users/fangyirui/sdk/go1.23.2/src/testing/testing.go:1690 +0xf4
created by testing.(*T).Run in goroutine 1
	/Users/fangyirui/sdk/go1.23.2/src/testing/testing.go:1743 +0x390

关闭 Channel 是一个重要的操作,它通知接收者不会再有新的数据发送到这个 Channel 上。

  1. 通知完成:通过关闭一个 Channel,可以向接收者表明没有更多的数据会被发送。这对于需要知道何时停止读取数据的消费者来说非常有用。
  2. 避免死锁:如果所有发送者都退出了,而消费者仍然在等待数据,则可能导致死锁。通过显式地关闭 Channel,可以避免这种情况。

改进:使用内置函数 close 来关闭一个 channel

func sendOnlyV2(i int, ch chan<- int, wg *sync.WaitGroup) {
	fmt.Printf("-- 准备发送:%d\n", i)
	ch <- i
	fmt.Printf("-- 发送成功!%d\n", i)
	wg.Done()
}

func Test8(t *testing.T) {
	wg := sync.WaitGroup{}
	ch := make(chan int)

	for i := 0; i < 10; i++ {
		wg.Add(1)
		go sendOnlyV2(i, ch, &wg)
	}

	// 启动另一个goroutine来等待所有send操作完成后再关闭channel,不然channel关闭后还没发送完数据会报错
	go func() {
		wg.Wait()
		// 发送完毕,关闭channel
		close(ch)
		fmt.Println("channel 已经关闭")
	}()

	for value := range ch {
		fmt.Printf("接受到消息:%d\n", value)
		time.Sleep(time.Second)
	}
}

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

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

相关文章

【爬虫】Firecrawl对京东热卖网信息爬取(仅供学习)

项目地址 GitHub - mendableai/firecrawl: &#x1f525; Turn entire websites into LLM-ready markdown or structured data. Scrape, crawl and extract with a single API. Firecrawl更多是使用在LLM大模型知识库的构建&#xff0c;是大模型数据准备中的一环&#xff08;在…

Admin.NET框架前端由于keep-alive设置缓存导致的onUnmount未触发问题

bug版本&#xff1a;next分支&#xff0c;基于.NET6版本&#xff1b; 问题描述&#xff1a; 1、添加keep-alive后&#xff0c;在其下运行的组件会出现onActived(被关注时)和onDeactived(取消关注时)生命周期&#xff0c;而组件原有生命周期为onMounted(被创造时)和onUnmounted(…

机器学习day7-线性回归3、逻辑回归、聚类、SVC

7欠拟合与过拟合 1.欠拟合 模型在训练数据上表现不佳&#xff0c;在新的数据上也表现不佳&#xff0c;常发生在模型过于简单无法处理数据中的复杂模式时。 特征&#xff1a; 训练误差较高 测试误差也高 模型过于简化&#xff0c;不能充分学习训练数据中的模式 2.过拟合 …

【鸿蒙开发】第二十二章 IPC与RPC进程间通讯服务

目录 1 IPC与RPC通信概述 2 实现原理 3 约束与限制 4 使用场景 5 开发步骤 5.1 Native侧开发步骤 5.2 ArkTS侧开发步骤 6 远端状态订阅开发实例 6.1 使用场景 6.1.1 Native侧接口 6.2 ArkTS侧接口 6.3 Stub感知Proxy消亡&#xff08;匿名Stub的使用&#xff09; 1 …

【开发小技巧11】用经典报表实现badge list效果,根据回显内容用颜色加以区分

之前使用badge list实现首页指标数据回显&#xff0c;但是无法根据对应数据进行个性化动态展示&#xff0c;那要如何解决呢&#xff1f;下面就来看看如何通过经典报表实现badge list效果&#xff0c;根据回显内容用颜色加以区分。 普通经典报表 想要做成类似这样的效果并且能…

rust中解决DPI-1047: Cannot locate a 64-bit Oracle Client library问题

我们在使用rust-oracle crate连接oracle进行测试的过程中&#xff0c;会发现无法连接oracle&#xff0c;测试运行过程中抛出“DPI-1047: Cannot locate a 64-bit Oracle Client library”错误。该问题是由于rust-oracle需要用到oracle的动态连接库&#xff0c;我们通过安装orac…

cocos creator 3.8 一些简单的操作技巧,材质的创建 1

这是一个飞机的3D模型与贴图 导入到cocos中&#xff0c;法线模型文件中已经包含了mesh、material、prefab&#xff0c;也就是模型、材质与预制。界面上创建一个空节点Plane&#xff0c;将模型直接拖入到Plane下。新建材质如图下 Effect属性选择builtin-unlit&#xff0c;不需…

python oa服务器巡检报告脚本的重构和修改(适应数盾OTP)有空再去改

Two-Step Vertification required&#xff1a; Please enter the mobile app OTPverification code: 01.因为巡检的服务器要双因子认证登录&#xff0c;也就是登录堡垒机时还要输入验证码。这对我的巡检查服务器的工作带来了不便。它的机制是每一次登录&#xff0c;算一次会话…

数据集-目标检测系列- 荷花 莲花 检测数据集 lotus>> DataBall

数据集-目标检测系列- 荷花 莲花 检测数据集 lotus>> DataBall DataBall 助力快速掌握数据集的信息和使用方式&#xff0c;会员享有 百种数据集&#xff0c;持续增加中。 贵在坚持&#xff01; 数据样例项目地址&#xff1a; * 相关项目 1&#xff09;数据集可视化项…

操作系统——揭开盖子

计算机执行时——取指执行 es:bx等于从0x9000开始&#xff0c;到0x90200结束

CTF 攻防世界 Web: SSRF Me write-up

题目名称-SSRF ME captcha 解码 目录扫描没有发现有用结果&#xff0c;根据提示 url 可能用来访问内部资源&#xff0c;根据题目名称可以猜测 ssrf。 其中 Captcha 用到 md5 加密截取&#xff0c;而且在每一次刷新网页时候会改变&#xff0c;可以写代码爆力枚举 Captcha 的值…

医学图像语义分割:前列腺肿瘤、颅脑肿瘤、腹部多脏器 MRI、肝脏 CT、3D肝脏、心室

医学图像语义分割&#xff1a;前列腺肿瘤、颅脑肿瘤、腹部多脏器 MRI、肝脏 CT、3D肝脏、心室 语义分割网络FCN&#xff1a;通过将全连接层替换为卷积层并使用反卷积上采样&#xff0c;实现了第一个端到端的像素级分割网络U-Net&#xff1a;采用对称的U形编解码器结构&#xff…

WPF窗体基本知识-笔记-命名空间

窗体程序关闭方式 命名空间:可以理解命名空间的作用为引用下面的控件对象 给控件命名:一般都用x:Name,也可以用Name但是有的控件不支持 布局控件(容器)的类型 布局控件继承于Panel的控件,其中下面的border不是布局控件,panel是抽象类 在重叠的情况下,Zindex值越大的就在上面 Z…

pytorch官方FasterRCNN代码详解

本博文转自捋一捋pytorch官方FasterRCNN代码 - 知乎 (zhihu.com)&#xff0c;增加了其中代码的更详细的解读&#xff0c;以帮助自己理解该代码。 代码理解的参考Faster-RCNN全面解读(手把手带你分析代码实现)---前向传播部分_手把手faster rcnn-CSDN博客 1. 代码结构 作为 to…

大数运算(加减乘除和输入、输出模块)

为什么会有大数呢&#xff1f;因为long long通常为64位范围约为 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807&#xff0c;最多也就19位&#xff0c;那么超过19位的如何计算呢&#xff1f;这就引申出来大数了。 本博客适合思考过这道题&#xff0c;但是没做出来或…

Excel的图表使用和导出准备

目的 导出Excel图表是很多软件要求的功能之一&#xff0c;那如何导出Excel图表呢&#xff1f;或者说如何使用Excel图表。 一种方法是软件生成图片&#xff0c;然后把图片写到Excel上&#xff0c;这种方式&#xff0c;因为格式种种原因&#xff0c;导出的图片不漂亮&#xff0c…

LLM: AI Mathematical Olympiad (下)

文章目录 一、SC-TIR策略&#xff08;工具整合推理&#xff09;二、SC-TIR原理三、避免过拟合四、代码分析1、Main函数2、SC-TIR control flow3、Extract answer4、Execute completion 总结 本文较长分成两个部分分析 | ू•ૅω•́)ᵎᵎᵎ 第一部分&#xff1a;预备知识介绍和…

06、Spring AOP

在我们接下来聊Spring AOP之前我们先了解一下设计模式中的代理模式。 一、代理模式 代理模式是23种设计模式中的一种,它属于结构型设计模式。 对于代理模式的理解: 程序中对象A与对象B无法直接交互,如:有人要找某个公司的老总得先打前台登记传达程序中某个功能需要在原基…

前端vue调试样式方法

1.选中要修改的下拉框&#xff0c;找到对应的标签的class样式 2.在浏览器中添加width宽度样式覆盖原有的样式&#xff0c;如果生效后说明class对了&#xff0c;则到vue页面的strye中添加覆盖样式 <style> :deep(.el-select){width: 180px; } </style>3.寻找自定义…

刷写树莓派系统

一. 树莓派做smb文件服务器参考视频 click here 二. 在官网上下载适合自己树莓派型号的镜像文件 三. 使用官方的riprpi-imager刷写软件 可以自动将TF卡(micro sd卡)格式化为适合树莓派系统运行的文件格式Fat32。就无需自己手动格式化进行刷写。 四. 出现文件验证失败 先把…