Golang(6)-协程和通道

Go从入门到精通(19)-协程(goroutine)与通道(channel) 作为一门 21 世纪的语言,Go 原生支持应用之间的通信(网络,客户端和服务端,分布式计算)和程序的并发。程序可以在不同的处理器和计算机上同时执行不同的代码段。Go 语言为构建并发程序的基本代码块是 协程 (goroutine) 与通道 (channel)。他们需要语言,编译器,和runtime的支持。Go 语言提供的垃圾回收器对并发编程至关重要。不要通过共享内存来通信,而通过通信来共享内存。通信强制协作。一个应用程序是运行在机器上的一个进程;进程是一个运行在自己内存地址空间里的独立执行体。 阅读详情


更多关于Golang核心编程知识的文章请看:Golang核心编程(0)-目录页


优雅的并发编程范式,完善的并发支持,出色的并发性能是Go语言区别于其他语言的一大特色。

执行体是个抽象的概念,在操作系统层面有多个概念与之对应,比如操作系统自己掌管的进程(process)、进程内的线程(thread)以及进程内的协程(coroutine,也叫轻量级线程)。与传统的系统级线程和进程相比,协程的最大优势在于其“轻量级”,可以轻松创建上百万个而不会导致系统资源衰竭,而线程和进程通常最多也不能超过1万个。这也是协程也叫轻量级线程的原因。

一、协程

Go 语言在语言级别支持轻量级线程,叫goroutine。Go 语言标准库提供的所有系统调用操作(当然也包括所有同步 IO 操作),都会出让 CPU 给其他goroutine。这让事情变得非常简单,让轻量级线程的切换管理不依赖于系统的线程和进程,也不依赖于CPU的核心数量。

1.1、goroutine简单使用

goroutine是Go语言中的轻量级线程实现,由Go运行时(runtime)管理。你将会发现,它的使用出人意料得简单。

假设我们需要实现一个函数Add(),它把两个参数相加,并将结果打印到屏幕上,具体代码如下:

func Add(x, y int) {
	z := x + y
	fmt.Println(z)
}

那么,如何让这个函数并发执行呢?具体代码如下:

	go Add(1, 1)

go 是Go语言中最重要的关键字,这一点从Go语言本身的命名即可看出。
在一个函数调用前加上 go 关键字,这次调用就会在一个新的goroutine中并发执行。当被调用的函数返回时,这个goroutine也自动结束了

二、并发通信

现有如下的例子,add.go:

package main
import "fmt"
func Add(x, y int) {
	z := x + y
	fmt.Println(z)
}
func main() {
	for i := 0; i < 10; i++ {
	go Add(i, i)
}
}

执行是结果是什么呢?屏幕上什么都没有,程序没有正常工作。

是什么原因呢?明明调用了10次 Add() ,应该有10次屏幕输出才对。要解释这个现象,就涉及Go语言的程序执行机制了。

Go程序从初始化 main package 并执行main()函数开始,当 main() 函数返回时,程序退出,且程序并不等待其他goroutine(非主goroutine)结束。

对于上面的例子,主函数启动了10个goroutine,然后返回,这时程序就退出了,而被启动的执行 Add(i, i) 的goroutine没有来得及执行,所以程序没有任何输出。

在循环外加上一条代码,令main()函数先等待,就可以看到打印的结果:
time.Sleep(1000)

2.1、传统并发通信方式

在工程上,有两种最常见的并发通信模型:共享数据和消息。
共享数据是指多个并发单元分别保持对同一个数据的引用,实现对该数据的共享。被共享的数据可能有多种形式,比如内存数据块、磁盘文件、网络数据等。在实际工程应用中最常见的无疑是内存了,也就是常说的共享内存。

2.2、Go的并发通信方式

Go语言提供的是另一种通信模型,即以消息机制而非共享内存作为通信方式:

消息机制认为每个并发单元是自包含的、独立的个体,并且都有自己的变量,但在不同并发单元间这些变量不共享。每个并发单元的输入和输出只有一种,那就是消息。这有点类似于进程的概念,每个进程不会被其他进程打扰,它只做好自己的工作就可以了。不同进程间靠消息来通信,它们不会共享内存。

Go语言提供的消息通信机制被称为channel。

所以,在Go语言中有一句口号:

不要通过共享内存来通信,而应该通过通信来共享内存。

三、channel

channel是Go语言在语言级别提供的goroutine间的通信方式。我们可以使用channel在两个或多个goroutine之间传递消息。channel是进程内的通信方式,因此通过channel传递对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等。

channel是类型相关的。也就是说,一个channel只能传递一种类型的值,这个类型需要在声明channel时指定。

一个例子说明channel:

package main
import "fmt"
func Count(ch chan int) {
	ch <- 1
	fmt.Println("Counting")
}
func main() {
	chs := make([]chan int, 10)
	for i := 0; i < 10; i++ {
		chs[i] = make(chan int)
		go Count(chs[i])
	}
	for _, ch := range(chs) {
		<-ch
	}
}

在这个例子中,我们定义了一个包含10个channel的数组(名为 chs ),并把数组中的每个channel分配给10个不同的goroutine。在每个goroutine的 Add() 函数完成后,我们通过 ch <- 1 语句向对应的channel中写入一个数据。在这个channel被读取前,这个操作是阻塞的。在所有的goroutine启动完成后,我们通过 <-ch 语句从10个channel中依次读取数据。在对应的channel写入数据前,这个操作也是阻塞的。这样,我们就用channel实现了类似锁的功能,进而保证了所有goroutine完成后主函数才返回。

3.1、基本语法

一般channel的声明形式为:

var chanName chan ElementType

与一般的变量声明不同的地方仅仅是在类型之前加了chan关键字。 ElementType 指定这个channel所能传递的元素类型。举个例子,我们声明一个传递类型为 int channel:

var ch chan int

或者,我们声明一个 map ,元素是 bool 型的channel:

var m map[string] chan bool

定义一个channel也很简单,直接使用内置的函数 make() 即可:

ch := make(chan int)

在channel的用法中,最常见的包括写入和读出。将一个数据写入(发送)至channel的语法很直观,如下:

ch <- value

向channel写入数据通常会导致程序阻塞,直到有其他goroutine从这个channel中读取数据。从channel中读取数据的语法是

value := <-ch

如果channel之前没有写入数据,那么从channel中读取数据也会导致程序阻塞,直到channel中被写入数据为止。我们之后还会提到如何控制channel只接受写或者只允许读取,即单向channel。

3.2、select

早在Unix时代, select 机制就已经被引入。通过调用 select() 函数来监控一系列的文件句柄,一旦其中一个文件句柄发生了IO动作,该 select() 调用就会被返回。后来该机制也被用于实现高并发的Socket服务器程序。Go语言直接在语言级别支持 select 关键字,用于处理异步IO问题。

select 的用法与 switch 语言非常类似,由 select 开始一个新的选择块,每个选择条件由
case 语句来描述。与 switch 语句可以选择任何可使用相等比较的条件相比, select 有比较多的限制,其中最大的一条限制就是每个 case 语句里必须是一个IO操作,大致的结构如下:

select {
	case <-chan1:
	// 如果chan1成功读到数据,则进行该case处理语句
	case chan2 <- 1:
	// 如果成功向chan2写入数据,则进行该case处理语句
	default:
	// 如果上面都没有成功,则进入default处理流程
}

可以看出, select 不像 switch ,后面并不带判断条件,而是直接去查看 case 语句。每个case 语句都必须是一个面向channel的操作。比如上面的例子中,第一个 case 试图从 chan1 读取一个数据并直接忽略读到的数据,而第二个 case 则是试图向 chan2 中写入一个整型数1,如果这两者都没有成功,则到达 default 语句。

3.3、缓冲机制

之前我们示范创建的都是不带缓冲的channel,这种做法对于传递单个数据的场景可以接受,但对于需要持续传输大量数据的场景就有些不合适了。接下来我们介绍如何给channel带上缓冲,从而达到消息队列的效果。

c := make(chan int, 1024)

在调用 make() 时将缓冲区大小作为第二个参数传入即可,比如上面这个例子就创建了一个大小为1024的 int 类型 channel ,即使没有读取方,写入方也可以一直往channel里写入,在缓冲区被填完之前都不会阻塞。

3.4、超时机制

在之前对channel的介绍中,我们完全没有提到错误处理的问题,而这个问题显然是不能被忽略的。在并发编程的通信过程中,最需要处理的就是超时问题,即向channel写数据时发现channel已满,或者从channel试图读取数据时发现channel为空。如果不正确处理这些情况,很可能会导致整个goroutine锁死。

Go语言没有提供直接的超时处理机制,但我们可以利用 select 机制。虽然 select 机制不是专为超时而设计的,却能很方便地解决超时问题。因为 select 的特点是只要其中一个 case 已经完成,程序就会继续往下执行,而不会考虑其他 case 的情况。

// 首先,我们实现并执行一个匿名的超时等待函数
timeout := make(chan bool, 1)
go func() {
	time.Sleep(1e9) // 等待1秒钟
	timeout <- true
}()
// 然后我们把timeout这个channel利用起来
select {
	case <-ch:
	// 从ch中读取到数据
	case <-timeout:
	// 一直没有从ch中读取到数据,但从timeout中读取到了数据
}

这样使用 select 机制可以避免永久等待的问题,因为程序会在 timeout 中获取到一个数据后继续执行,无论对 ch 的读取是否还处于等待状态,从而达成1秒超时的效果。

3.5、关闭channel

关闭channel非常简单,直接使用Go语言内置的 close() 函数即可:
close(ch)在介绍了如何关闭channel之后,我们就多了一个问题:如何判断一个channel是否已经被关闭?我们可以在读取的时候使用多重返回值的方式:

x, ok := <-ch

这个用法与 map 中的按键获取 value 的过程比较类似,只需要看第二个 bool 返回值即可,如果返回值是 false 则表示 ch 已经被关闭。

四、同步锁和读写锁

Go语言包中的 sync 包提供了两种锁类型: sync.Mutex 和 sync.RWMutex 。 Mutex 是最简单的一种锁类型,同时也比较暴力,当一个goroutine获得了 Mutex 后,其他goroutine就只能乖乖等到这个goroutine释放该 Mutex 。基于同步锁机制。

RWMutex 相对友好些,是经典的单写多读模型。在读锁占用的情况下,会阻止写,但不阻止读,也就是多个goroutine可同时获取读锁(调用 RLock() 方法;而写锁(调用 Lock() 方法)会阻止任何其他goroutine(无论读和写)进来,整个锁相当于由goroutine独占。基于读写锁机制。

对于这两种锁类型,任何一个 Lock() 或 RLock() 均需要保证对应有 Unlock() 或 RUnlock()调用与之对应,否则可能导致等待该锁的所有goroutine处于饥饿状态,甚至可能导致死锁,这种经典的死锁问题被称为饥饿死锁问题,锁的典型使用模式如下:

var l sync.Mutex
func foo() {
	l.Lock()
	defer l.Unlock()
//...
}
汇川伺服驱动器参数优化实战指南 本文详细介绍了汇川伺服驱动器参数优化的实战技巧,涵盖控制模式选择、电子齿轮比调整、刚性匹配与振动抑制等关键内容。通过实际案例和实用技巧,帮助工程师快速掌握伺服驱动器参数调校方法,提升设备运行精度和稳定性。特别针对高精度定位和高速响应场景提供了专业指导。 阅读详情

相关推荐

NU1680无线充电接收器在TWS耳机中的应用实战:I2C调压与QI协议适配

本文详细解析了NU1680无线充电接收器在TWS耳机充电仓设计中的应用实战。重点阐述了其无固件设计、I2C接口动态调压功能如何解决不同QI协议发射器的兼容性难题,并分享了硬件设计、PCB布局、与发射端联调及安全测试等关键环节的实践经验,助力开发者快速实现稳定高效的无线充电方案。

weixin_28432777的博客 233

golang中map并发读写问题及解决方法

一、map并发读写问题 如果map由多协程同时读和写就会出现fatalerror:concurrentmapreadandmapwrite的错误 如下代码很容易就出现map并发读写问题 funcmain(){ c:=make(map[string]int) gofunc(){//开一个协程写map forj:=0;j<1000000;j++{ c[fmt.Sprintf("%d",j)]...

oguro的博客 3678

PT32F030与MT6826S SPI通讯实战:从硬件连接到数据交互全流程解析

本文详细解析了PT32F030与MT6826S通过SPI通讯的全流程,包括硬件连接、电路设计、SPI外设初始化配置以及数据交互协议实现。通过实战案例和代码示例,帮助开发者快速掌握PT32F030与MT6826S的SPI通讯技术,实现精准的角度和速度数据采集。

weixin_29189373的博客 420

golang 中slice 、map、chan作为函数参数分析

写这篇文章之前考虑一个问题: go里面都是值传递,不存在引用传递? 先来总结一下slice、map、chan的特性: slice: func makeslice64(et *_type, len64, cap64 int64) unsafe.Pointer type slice struct { array unsafe.Pointer len int cap int...

chen 5839

Python调用Google Trends官方数据接口实战指南

Google Trends作为全球最权威的搜索热度分析工具,其数据本质是公开、结构化、可编程访问的时序指标,而非需爬取的网页内容。其底层基于标准HTTP协议与JSON API交互,核心原理在于复现用户浏览器行为——通过/explore接口获取token,再凭token请求/widgetdata/multiline等端点获取归一化(0-100)时间序列数据。该技术路径规避了Selenium渲染依赖与非官方库的脆弱性,具备高稳定性、易调试、可监控等工程优势,广泛应用于竞品监测、内容选题验证、地域营销策略制定等业

weixin_29789925的博客 422

FPGA实战:手把手教你配置4个Aurora 8B/10B IP核共享时钟(附完整工程文件)

本文详细介绍了在Xilinx FPGA上配置四个Aurora 8B/10B IP核并实现时钟共享的实战指南。从工程规划、IP核基础配置到共享逻辑的实现,再到多核时钟域与复位架构的设计,以及时序约束与调试技巧,提供了完整的解决方案和可复用的工程文件。特别强调了GT参考时钟的共享和复位信号的处理,帮助工程师高效解决多通道高速串行通信的设计挑战。

weixin_30879169的博客 416

管道的细节和课堂练习

package main import “fmt” func main(){ //1)创建一个intChan,最多可以存放3个int,演示存3数据到intChan,然后在取出这三个int var intChan chan int intChan = make(chan int,3) intChan <- 10 intChan <- 20 intChan<-10 //因为intCh...

qq_43187004的博客 201

arduino esp8266_Arduino传感器教程 第22章ESP8266 控制 5V 继电器

ESP8266控制5V继电器目录一、NodeMCU二、工作原理本文将使用亚马逊 AlexaEcho 和 ESP8266 控制 5V 继电器。NodeMCUNodeMCU 是一个开源的 IoT 物联网硬件开发板,由于它支持 WIFI 功能且使用方法十分类似 Arduino 开发板,所以近些年获得了越来越多来自全世界的创客朋友们的青睐。NodeMCU 尺寸与 Arduino Nano 类似。...

weixin_39611722的博客 3457

百万级并发新解法:golang协程如何吊打 Rust 异步生态?

在探讨Golang协程如何与Rust异步生态在百万级并发场景下进行对比时,需要明确的是,两种语言各自有其独特的优势和适用场景。然而,若从某些角度来看,Golang的协程机制在处理高并发任务时确实展现出了显著的优势。

北桥源木 599

基于Termux的Android平台OpenClaw部署:移动端AI助理实现

本文介绍了在Android设备上通过Termux部署OpenClaw AI助理的方案。Android设备的便携性、Termux提供的完整Linux环境以及多样化的硬件配置使其成为理想的部署平台。文章详细分析了不同级别Android设备的硬件规格(从入门级到旗舰级),并提供了最低/推荐配置要求。部署步骤包括Termux安装初始化、Python环境配置以及性能优化建议。该方案充分利用Android设备的传感器和电池供电优势,为移动办公、学习等场景提供了低成本、高便携性的AI助理解决方案。

博客 3015

C#里用HttpListener实现WEB API接口服务端

本文介绍了一个基于WinForm的HTTP服务实现方案。该方案使用HttpListener类创建了一个支持JSON POST请求处理的轻量级Web服务器,主要特点包括:1) 纯线程方式实现请求监听和处理;2) 提供标准化的JSON响应格式;3) 支持自定义请求处理逻辑;4) 包含完整的日志记录功能。代码示例展示了如何初始化服务、处理不同类型的请求(包括JSON POST),并提供了线程安全的UI更新机制。该实现适用于需要简单HTTP接口的Windows桌面应用程序,可作为程序间通信的基础设施。

大坡3D软件开发 229

Golang协程详解

golang 协程引入,原理讲解,GMP调度模型详解,调度器讲解与流程分析

资深系统架构,深耕高并发研发与 AI 大模型工程化落地。技术栈以 Python (AI/NLP/RAG/Agent) 与 Golang (高并发架构) 为双核心,兼具 PHP 大型应用经验。专注算法落地、复杂系统设计及团队技术赋能 3909

用 ABAP RAP + XCO 把 Excel 导入业务对象:从弹窗 Action 到 EML 落库与附件清理的完整实践

摘要:本文介绍了在SAP RAP框架下实现Excel批量导入功能的完整方案。通过XCO库的XLSX模块解析Excel文件,结合RAP的行为定义和Fiori Elements界面集成,构建了一个支持试运行、数据校验和自动合并的业务导入功能。方案采用RAP标准范式,包括Action参数设计、EML事务处理、附件管理以及异常消息反馈,实现了从文件上传到数据落库的端到端流程,既保证了系统稳定性,又提升了业务用户体验。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 278

Golang 空通道考究

最近看到了一些帖子和博客在讨论golang里面对空通道进行读写操作。本人也比较好奇,就实验了一下,发现和大家的描述都有出入。

qingchuwudi的专栏 752

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

Swoole v6.2 已悄然构建起媲美 Golang/Node.js 的完整 PHP 异步并发编程生态体系

Swoole v6.2 已悄然构建起媲美 Golang/Node.js 的完整 PHP 异步并发编程生态体系

希望我的博客,能帮上你解决学习中工作中所遇到的问题,也期待与您一起探讨技术问题,共同成长。 106

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

Golang协程和通道

协程(Coroutine)协程是一种轻量级的并发执行单元,通常由编程语言本身的运行时系统进行调度和管理。协程通常在一个线程内执行,共享相同的地址空间和资源。协程间通常通过通道(Channel)实现数据交换和协作。协程的创建、销毁以及切换都由运行时系统自动完成,开销非常小,可以创建成千上万个协程而不会导致系统负载过高。

2021dragon的博客 2991

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

【Golang】:协程和通道

本文介绍了Go语言中的协程和通道机制。协程(Goroutine)是用户态的轻量级线程,由Go运行时调度,具有创建/切换成本低的特点,通过GMP模型实现并发与并行。通道(Channel)用于协程间通信,分为缓冲和非缓冲两种,支持发送/接收、关闭和遍历操作。文章还详细说明了协程的生命周期状态、调度流程(包括主动/被动/抢占调度),以及通道的特殊用法(只读/只写)和select语句的多路监听、超时控制等应用场景。这些机制共同构成了Go语言高并发编程的核心基础。

元清的博客 1388

Golang 协程与通道

在 Go 语言中,协程被称为 goroutines。goroutine 是 Go 的并发执行单元,它比传统的线程更轻量级,允许你以非常低的开销启动成千上万个并发任务通道(channel)是一种特殊的类型,用于在不同的 goroutines 之间进行通信和同步。通道可以被想象成一个传递数据的管道,它可以帮助确保并发程序的数据同步,避免竞态条件。

e5pool的博客 1303

golang 协程、通道相关面试题

通道(channel)是一种在多个协程之间进行通信的机制,它提供了同步和通信的功能。通过通道,一个协程可以向另外一个协程发送数据,并且另外一个协程可以从通道中接收数据。(1)创建通道:使用make 函数创建通道;//创建一个新通道//在通道中发送消息:c <- 1//在通道中接收消息msg := <-c(4)chan关键字的作用是什么?chan用于创建一个新的通道。

wamp0001的博客 936

【魅力golang】之-通道

昨天发布了golang的最大特色之一--协程,与协程密不可分的是通道(channel),用来充当协程间相互通信的角色。通道是一种内置的数据结构,所以才造就了golang强大的并发能力。Golang 中的通道是实现协程间通信的核心工具,简化了并发编程的复杂性。通道通过 CSP 模型实现协程间的数据传递,在消息队列、协程同步、超时控制等场景中表现出色。通过通道,协程可以安全、同步地交换数据,避免直接操作共享内存带来的复杂性。通道可以实现简单的消息队列,用于协程间的任务分发。:Go 的通道实现了。

网络风云的博客 5986

Go简单实现协程池

首先就是进程、线程、协程讲解老三样。进程: 本质上是一个独立执行的程序,进程是操作系统进行资源分配和调度的基本概念,操作系统进行资源分配和调度的一个独立单位。线程: 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程中可以并发多个线程,每条线程执行不同的任务,切换受系统控制。 协程: 又称为微线程,是一种用户态的轻量级线程,协程不像线程和进程需要进行系统内核上的上下文切换,协程的上下文切换是由用户自己决定的,有自己的上下文,所以说是轻量级的线程,也称之为用户级别的线

爪哇盘古 3781

一、Go基础知识24、协程机制详解

GO:协程:什么是协程、协程的基本操作、协程的一些高级操作、GO的协程机制原理、协程通道实现方式、协程通道基本原理、通道及协程的限制、CSP

风不归的乐园 3297

『GoLang』协程与通道

Go语言协程与通道的初始,新手其实这么多都嫌多了,知道基本用法就好,然后以后遇到了,要理解PV操作原理再根据文档写代码会很轻松,这种不能硬着头皮学

七秒钟的记忆 957

go 协程与通道

go 协程与通道

wgchen 743

go 协程.通道.select

中无法存储数据, 所有接收端先于 发送端开始操作,当发送端 发送数据,接收端立马取走数据。通道 channel 是一个管道,可以想现象成 Go协程之间通讯的管道。指针接收器内部属性的改变对于 调用者是可见的, 值接收器内部属性的改变 对于调用者不可见。t Type 接收器(可以是结构体或非机构体类型,接收器可在方法内部访问) 可以缺省。,此时通道里面已经有一个数据,此时再往里面发送数据 就会报错。创建通道的时候 可以设置第二个参数 容量,当容量为0的时候。相同的方法名 可以定义在不同的类型(结构)上。

唐僧的专栏 1458

【Golang】✔️走进 Go 语言✔️ 第十六课 协程 & 通道

【Golang】✔️走进 Go 语言✔️ 第十六课 协程 & 通道.

我是小白呀的博客 1709
上一篇: Golang(5)-接口
下一篇: Golang(7)-net/http包的使用
BoringRong
BoringRong 领域专家: 后端开发技术领域 领域专家: 后端开发技术领域
博客等级 码龄9年 2万+粉丝 386原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BoringRong

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值