Go后端开发 – goroutine && channel
文章目录
一、goroutine介绍
1.协程的由来
- 多进程操作系统
解决了阻塞的问题

存在切换成本


设计复杂


- 将一个线程分为用户线程和内核线程,CPU只能看到内核线程


- 使用协程调度器调度多个协程,形成N:1关系

- 多个线程管理多个协程,M:N,语言的重点就在于协程调度器的优化

2.Golang对协程的处理
- goroutine内存更小,可灵活调度

- Golang早期对调度器的处理

协程队列中有锁,对协程的运行进行保护,协程获取锁后,会从队首获取goroutine执行,剩余goroutine会向队列前方移动

当前goroutine执行完后,会放回队列尾部

老的调度器有几个缺陷:

- GMP:Golang对调度器的优化
处理器是用来处理goroutine的,保存每个goroutine的资源

每个P都会管理一个goroutine的本地队列,M想获取协程需要从P获取;P的数量由GOMAXPROCS确定;
一个P同时只能执行一个协程;
还存在一个goroutine的全局队列,存放等待运行的协程;
当前程序最高并行的goroutine的数量就是P的数量;
Golang调度器的设计策略 - 复用线程:
work stealing机制
M1在执行G1,而M2是空闲的

M2就会从M1的本地队列中进行偷取,G3就会在M2中执行

hand off机制
若目前有两个M,M1中的G1被阻塞了,M2即将运行G3

此时会创建/唤醒一个thread M3,并将M1绑定的P切换到M3,阻塞的G1继续和M1绑定

- 利用并行
利用GOMAXPROCS限定P的个数

- 抢占
普通调度器的协程和CPU是绑定的,其他的协程会等待使用CPU资源,只有该协程主动释放资源,才会解绑CPU,为其他协程提供资源

goroutine是轮询机制,如果有其他协程等待资源,那么每个协程最高轮训10ms,时间一到,无论是否释放资源,新的协程都会抢占CPU资源

- 全局G队列
work stealing机制可以从全局队列中偷取协程

3.协程并发
协程:coroutine。也叫轻量级线程。
与传统的系统级线程和进程相比,协程最大的优势在于“轻量级”。可以轻松创建上万个而不会导致系统资源衰竭。而线程和进程通常很难超过1万个。这也是协程别称“轻量级线程”的原因。
一个线程中可以有任意多个协程,但某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源。
多数语言在语法层面并不直接支持协程,而是通过库的方式支持,但用库的方式支持的功能也并不完整,比如仅仅提供协程的创建、销毁与切换等能力。如果在这样的轻量级线程中调用一个同步 IO 操作,比如网络通信、本地文件读写,都会阻塞其他的并发执行轻量级线程,从而无法真正达到轻量级线程本身期望达到的目标。
在协程中,调用一个任务就像调用一个函数一样,消耗的系统资源最少!但能达到进程、线程并发相同的效果。
在一次并发任务中,进程、线程、协程均可以实现。从系统资源消耗的角度出发来看,进程相当多,线程次之,协程最少。
4.Go并发
- Go 在语言级别支持协程,叫goroutine。Go 语言标准库提供的所有系统调用操作(包括所有同步IO操作),都会出让CPU给其他goroutine。这让轻量级线程的切换管理不依赖于系统的线程和进程,也不需要依赖于CPU的核心数量。
- 有人把Go比作21世纪的C语言。第一是因为Go语言设计简单,第二,21世纪最重要的就是并行程序设计,而Go从语言层面就支持并发。同时,并发程序的内存管理有时候是非常复杂的,而Go语言提供了自动垃圾回收机制。
- Go语言为并发编程而内置的上层API基于顺序通信进程模型CSP(communicating sequential processes)。这就意味着显式锁都是可以避免的,因为Go通过相对安全的通道发送和接受数据以实现同步,这大大地简化了并发程序的编写。
- Go语言中的并发程序主要使用两种手段来实现。goroutine和channel。
5.Goroutine
- goroutine是Go语言并行设计的核心,有人称之为go程。 Goroutine从量级上看很像协程,它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。
- 一般情况下,一个普通计算机跑几十个线程就有点负载过大了,但是同样的机器却可以轻松地让成百上千个goroutine进行资源竞争。
创建Goroutine
- 只需在函数调⽤语句前添加 go 关键字,就可创建并发执行单元。开发⼈员无需了解任何执行细节,调度器会自动将其安排到合适的系统线程上执行。
- 在并发编程中,我们通常想将一个过程切分成几块,然后让每个goroutine各自负责一块工作,当一个程序启动时,主函数在一个单独的goroutine中运行,我们叫它
main goroutine。新的goroutine会用go语句来创建。而go语言的并发设计,让我们很轻松就可以达成这一目的。
实例:
package main
import (
"fmt"
"time"
)
//子goroutine
func newTask() {
i := 0
for {
i++
fmt.Println("new Goroutine : i =", i)
time.Sleep(1 * time.Second)
}
}
//主goroutine
func main() {
//创建一个go程,去执行newTask流程
go newTask()
i := 0
for {
i++
fmt.Println("main Goroutine : i =", i)
time.Sleep(1 * time.Second)
}
}

- 上述实例中,newTask是由创建的goroutine执行的,与main函数的goroutine是并发执行的
- main函数的goroutine是主goroutine;newTask的goroutine是子goroutine,其内存空间是依赖于main的主goroutine的,如果main退出,其他goroutine会被杀掉
使用匿名方法创建goroutine:
- 匿名函数:
func () {
//函数体
}() //匿名函数{}后面加()代表直接调用
实例:
package main
import (
"fmt"
"time"
)
// 主goroutine
func main() {
//用go创建承载一个形参为空,返回值为空的一个函数
go func() {
defer fmt.Println("A.defer")
func() {
defer fmt.Println("B.defer")
//退出当前goroutine
fmt.Println("B")
}()
fmt.Println("A")
}()
//死循环
for {
time.Sleep(1 <

本文聚焦Go后端开发,详细介绍了goroutine和channel。先阐述了协程的由来、Golang对协程的处理及并发优势,说明了goroutine的创建、退出等操作。接着介绍channel,包括其定义、无缓冲和有缓冲类型的特点,还提及关闭、与range和select的使用,以及单向channel的应用。
最低0.47元/天 解锁文章
2万+





