概述

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. error: failed to push some refs to 'https://gitee.com/xxxxxxxxx/xxxxxxxt'

    原因是ReadMe文件不在本地中, 此时我们要执行git pull --rebase origin master命令README.md拉到本地, 任何然后执行git push origin maste ...

  2. 用 Python 自动创建 Markdown 表格 - 每天5分钟玩转 GPT 编程系列(4)

    目录 1. 他们居然问我要 Prompts 2. 让 GPT-4 来写代码 2.1 我对 DevChat 说 2.2 DevChat 回答 2.3 我又对 DevChat 说 2.4 DevChat ...

  3. [gin]数据解析和绑定

    前言 go version: 1.18 本文主要包含JSON.Form.Uri.XML的数据解析与绑定. JSON数据解析与绑定 go代码 package main import ( "ne ...

  4. 微信的 h5 支付和 jsapi 支付

    目录 申请商户号 申请商户证书 设置APIv3密钥 下载 SDK 开发包 下载平台证书 关联 AppID 账号 开通 H5 支付 H5支付流程 开通 JSAPI 支付 JSAPI 支付流程 通用微信支 ...

  5. 【路由器】OpenWrt 手动编译 ipk

    目录 .ipk 文件 编译准备 编译 .ipk 文件 更新 feeds 配置平台 获取交叉编译链 添加需要编译的第三方软件包 参考资料 .ipk 文件 .ipk 文件是可以通过 OpenWrt 的包管 ...

  6. C++算法之旅、06 基础篇 | 第三章 图论

    常用代码模板3--搜索与图论 - AcWing DFS 尽可能往深处搜,遇到叶子节点(无路可走)回溯,恢复现场继续走 数据结构:stack 空间:需要记住路径上的点,\(O(h)\). BFS使用空间 ...

  7. Longest Divisors Interval

    Smiling & Weeping ----总有一个人, 一直住在心底, 却消失在生活里. Given a positive integer n, find the maximum size ...

  8. 如何像 Sealos 一样在浏览器中打造一个 Kubernetes 终端?

    作者:槐佳辉.Sealos maintainer 在 Kubernetes 的世界中,命令行工具(如 kubectl 和 helm)是我们与集群交互的主要方式.然而,有时候,我们可能希望能够在 Web ...

  9. Spring扩展接口(1):ApplicationContextInitializer

    在此系列文章中,我总结了Spring扩展接口,以及各个扩展点的使用场景.并整理出一个bean在spring中从被加载到初始化到销毁的所有可扩展点的顺序调用图.这样,我们也可以看到bean是如何一步步加 ...

  10. oracle监听配置与防火墙问题

    在建好pdb容器后,需配置网络,才能从客户端连接服务器端 1.首先查看pdb容器的服务名 lsnrctl status ... Service "19cdb" has 1 inst ...