概述

Go并发模型独树一帜,简洁、高效。Go语言最小执行单位称为协程(goroutine),运行时可以创建成千万上个协程,这在Java、C等线程模型中是不可想象的,并发模型是Go的招牌能力之一。很多文章描述协程是轻量级的线程,并不准确,两者在底层有本质区别。线程是由操作系统维护,以Linux为例,系统调用创建线程,并由操作系统调度执行,在内核空间管理、与进程共享PCB对象、共享堆空间、独立调用栈和寄存器,是操作系统最小的调度对象,软中断触发操作系统切换调度。协程是由Go运行时维护,与操作系统线程不是对等关系,多个协程简共享堆栈空间,在用户空间维护,由Go运行时自行调度。不依赖系统中断可以做了非常轻量级。

调度可简单理解就是如何安排任务,合理高效的调度任务,可显著提升性能和降低复杂度。以Linux网络Io模型为例,经过多年的发展也就出现五种模型(阻塞 I/O、非阻塞 I/O、多路复用 I/O、信号驱动 I/O、异步 I/O)。传输层不变、TCP/IP协议栈不变、应用层协议不变、操作系统不变、硬件配置不变,不同Io模型性能差别非常大,这就是调度的威力。操作系统对线程的调度是自闭环的,不提供用户侧的控制接口,并行线程数与CPU数一致,线程切换是很重的操作,没有优化空间,完全寄托于操作系统进程管理能力。协程运行在线程之上,由go运行时维护,创建、同步、销毁、调度等,全部用户空间完成,可以做到和函数栈调用一样轻量级。在Go底层与操作系统交互还是线程模型,从操作系统视角根本看不到协程的存在,并行线程数也没有改变,复杂度也并没有降低,只是从用户侧转移到了Go运行时,总有人要负重前行。go并发模型并没有提升性能,更大作用是降低并发编程难度,降低开发人员心智。

Go的调度模型有专用名词:GPM

  • G,表示协程,用户通过go指令创建,数量不受限制
  • P,类似CPU,内部维护了队列,G只有加入到P队列后才能被调度,数量由Go自己维护,可通过GOMAXPROCS指定数量
  • M,OS线程抽象,负责调度任务,和某个P绑定,从P的中不断取出G,切换堆栈并执行,数量不可指定,由Go Runtime调整

基本使用

一如既往的简洁,使用go指令就可以丝滑的创建一个协程,新协程将会由go运行时调度。

func main() {
go func() {
fmt.Println("hello world")
}()
}

注意,上面代码大概率无法正常工作,不能打印出字符串。匿名函数在新协程中调度执行,main函数在主协程继续执行,两者协程会并发执行。这引出了协程重要特性,go主协程有特权,当主协程执行完毕就退出程序,不管是否存在用户协程。main执行结束退出程序,此时匿名函数还没来得及打印字符串。Java主线程也有类似的特性,但是开放了daemon属性可控制,Go则没有提供控制API。

要顺利打印字出字符串,主协程需要等待用户协程执行结束,本质是协程之间协调问题。

func main() {
go func() {
fmt.Println("hello world")
}()
time.Sleep(time.Second) // 主协程睡眠1秒
}

主协程睡眠了1秒,好像很可以工作了,但这是很low的解决方法,极度不靠谱。只要涉及并发编程,就绕不开同步机制,这是并发编程的核心内容,也是并发编程的复杂度所在,独立章节介绍。

另一个方法可以阻塞主协程,等待通知后继续执行

func main() {
wg := new(sync.WaitGroup)
wg.Add(1) go func() {
fmt.Println("hello world")
}() wg.Wait() // 进入等待
}

程序会锁死并panic崩溃退出。主线程进入了等待,却没有收到通知,go运行时可以发现死锁状态,类似逻辑在Java中不会退出,将永远阻塞,因为通知底层依赖操作系统中断机制,Java编译器无法识别死锁问题。而go在用户空间调度,由自己处理调度、同步,大部分死锁问题在编译时候就可以发现。这也可以看出两种调度模型的区别,不过JDK20也支持了虚拟线程,与go协程类似在空间实现调度。

修正后代码如下

func main() {
wg := new(sync.WaitGroup)
wg.Add(1) go func() {
fmt.Println("hello world")
wg.Done() // 通知
}() wg.Wait() // 进入等待
}

使用管道也可以实现通知

func main() {
notice := make(chan bool)
go func() {
fmt.Println("hello")
notice <- true
}() <-notice // 读取时堵塞,直到读取成功
}

当然也可以使用具名函数启动协程

func working()  {
fmt.Println("hello")
}
go working()

方法启动协程

type Person struct {
Name string
} func (p *Person) GetName() {
fmt.Println(p.Name)
} p := &Person{Name: "name"}
go p.GetName() // 启动协程

在协程执行的函数返回值将被丢弃,无法接收。如果需要返回值,只能使用一些特殊的方法

使用管道接收

func working(resultChannel chan int) {
....
resultChannel<- res; // 将结果写入管道
}

使用指针接收,调用函数时候传入指针,把结果写入指针指向的内存,这种叫传入传出参数,在C语言中比较常见

func working(data *int) {
....
*data = res // 结果写入指针指向内存
}

与其他语言一样,Go没有提供主动中断协程的API,大多数使用chan+select实现优雅退出,需要小心处理,容易出现协程泄漏问题。另外任何协程中出现panic,整个程序会崩溃,可根据情况按需捕获

func working() {
defer func() {
if err := recover(); err != nil { // 捕获错误,程序不会panic
fmt.Println("error:", err)
}
}() ... // 业务逻辑
} go working() // 启动协程

go并发 - goroutine的更多相关文章

  1. 《Go语言实战》摘录:6.2 并发 - goroutine

    6.2 goroutine

  2. Go中的并发编程和goroutine

    并发编程对于任何语言来说都不是一件简单的事情.Go在设计之初主打高并发,为使用者提供了goroutine,使用的方式虽然简单,但是用好却不是那么容易,我们一起来学习Go中的并发编程. 1. 并行和并发 ...

  3. go实例之轻量级线程goroutine、通道channel与select

    1.goroutine线程 goroutine是一个轻量级的执行线程.假设有一个函数调用f(s),要在goroutine中调用此函数,请使用go f(s). 这个新的goroutine将与调用同时执行 ...

  4. golang高并发的理解

    前言 GO语言在WEB开发领域中的使用越来越广泛,Hired 发布的<2019 软件工程师状态>报告中指出,具有 Go 经验的候选人是迄今为止最具吸引力的.平均每位求职者会收到9 份面试邀 ...

  5. go学习笔记-并发

    并发 goroutine goroutine是Go并行设计的核心.goroutine说到底其实就是协程,但是它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这 ...

  6. golang中并发sync和channel

    golang中实现并发非常简单,只需在需要并发的函数前面添加关键字"go",但是如何处理go并发机制中不同goroutine之间的同步与通信,golang 中提供了sync包和channel ...

  7. golang学习笔记----并发

    并发模型 并发目前来看比较主流的就三种: 多线程:每个线程一次处理一个请求,线程越多可并发处理的请求数就越多,但是在高并发下,多线程开销会比较大. 协程:无需抢占式的调度,开销小,可以有效的提高线程的 ...

  8. golang高并发

    golang 为什么能做到高并发 goroutine是go并行的关键,goroutine说到底就是携程,但是他比线程更小,几十个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这 ...

  9. Go并发编程

    概述 简而言之,所谓并发编程是指在一台处理器上"同时"处理多个任务. 随着硬件的发展,并发程序变得越来越重要.Web服务器会一次处理成千上万的请求.平板电脑和手机app在渲染用户画 ...

  10. golang:并发编程总结

    并行和并发 并发编程是指在一台处理器上"同时"处理多个任务. 宏观并发:在一段时间内,有多个程序在同时运行. 微观并发:在同一时刻只能有一条指令执行,但多个程序指令被快速的轮换执行 ...

随机推荐

  1. oracle 11g手工建库步骤(初学者)

    要建立的数据库ORACLE_SID=test1sys和system的密码为oracle1.建立相应的目录mkdir /u01/app/oracle/oradata/test1mkdir /u01/ap ...

  2. 文心一言 VS 讯飞星火 VS chatgpt (72)-- 算法导论7.1 3题

    三.请简要地证明:在规模为 n 的子数组上,PARTITION 的时间复杂度为 Θ(n). 文心一言: 首先,我们需要明确PARTITION函数的具体定义.PARTITION函数通常用于快速排序算法中 ...

  3. HTML5CSS3提高

    1 HTML5新特性 1.1 概述 HTML5 的新增特性主要是针对于以前的不足,增加了一些新的标签.新的表单和新的表单属性等. 这些新特性都有兼容性问题,基本是 IE9+ 以上版本的浏览器才支持,如 ...

  4. arrch架构部署redis,报错: ignore-warnings ARM64-COW-BUG

    arrch架构服务器redis部署完成后,启动报错.做个记录. arrch架构的redis安装包 下载链接:https://pan.baidu.com/s/1TMXNpMvMDWRFD1f5km7Mw ...

  5. Vu3+Element-Plus根据路由配置生成菜单导航栏

    先看效果,整体界面结构如下 点击左侧菜单栏,右侧切换显示不同页面内容. Vue3使用路由–南河小站 1 路由配置 路由配置如下: const routes = [ { path: "&quo ...

  6. 获取API接口返回的商品详情数据后该如何使用

    获取API接口返回的商品详情数据后,我们可以使用以下方式将其处理和利用: 数据展示:我们可以将API接口返回的商品详情数据以列表.表格.图形等形式展示给用户,以便他们更好地了解商品的基本信息.特征.评 ...

  7. Mybatis自动生成mapper和实体类

    准备工作:需要俩个jar包: (1)连接数据库的jar包:mysql-connector-java-5.1.16-bin.jar (2)实现需求的插件Generator:mybatis-generat ...

  8. kmp的简单应用

    Smiling & Weeping ---- 我只为你一个人写过月亮 题目链接:P4824 [USACO15FEB] Censoring S - 洛谷 | 计算机科学教育新生态 (luogu. ...

  9. 修改经过Spring Gateway的Json数据

    背景 使用Spring Cloud Gateway作为网关时经常会需要对报文内的json数据进行修改,但是目前看到的实现方法看起来都很复杂,这里提供一种使用Spring官方提供的ModifyReque ...

  10. 循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(4) -- 实现DataGrid数据的导入和导出操作

    在我们设计软件的很多地方,都看到需要对表格数据进行导入和导出的操作,主要是方便客户进行快速的数据处理和分享的功能,本篇随笔介绍基于WPF实现DataGrid数据的导入和导出操作. 1.系统界面设计 在 ...