Golang协程
目录
进程、线程、多线程、协程、用户态、内核态的概念
核心场景:把计算机想象成一个 「大商场」
1. 进程 (Process)
2. 线程 (Thread)
3. 多线程 (Multithreading)
4. 协程 (Coroutine) / Goroutine
5. 用户态 (User Space) vs 内核态 (Kernel Space)
关系总结(一张图看懂)
终极比喻
Goroutine
场景:处理一个工作任务(比如,处理100份文件)
1. 传统多线程(其他语言,比如 Java/C++)
2. Go 语言的 Goroutine
核心总结:
一句话终极比喻:
Go关键字实现协程
创建协程
用协程创建一个形参为空,返回值为空的函数
用协程创建一个有形参,有返回值的函数
在学习协程之前要先了解下面几个词的概念,由ai生成比较通俗易懂的解释:
进程、线程、多线程、协程、用户态、内核态的概念
核心场景:把计算机想象成一个 「大商场」
1. 进程 (Process)
-
比喻: 商场里的一个独立店铺
-
比如一家餐厅、一家服装店、一家书店。
-
-
特点:
-
资源独立: 每个店铺都有自己的空间(内存)、收银台(资源)、员工(线程)。餐厅不会用书店的厨房,服装店不会卖书店的书。
-
相互隔离: 一个店铺着火(崩溃)了,一般不会影响其他店铺。
-
切换成本高: 从一个店铺跑到另一个店铺,需要花点时间(上下文切换开销大)。
-
2. 线程 (Thread)
-
比喻: 店铺里的一个员工
-
餐厅里有厨师、服务员、收银员。
-
-
特点:
-
共享资源: 同一个店铺的所有员工,共享这个店铺的厨房、收银台、餐桌(共享进程的内存和资源)。
-
协作工作: 员工们可以同时做不同的事(并发),比如厨师炒菜,服务员端盘子。
-
是执行任务的基本单位: 活最终是靠员工(线程)来干的。
-
3. 多线程 (Multithreading)
-
比喻: 一个店铺里雇佣了多个员工同时工作
-
餐厅同时有3个厨师炒菜,5个服务员接待客人。
-
-
特点:
-
提高效率: 可以同时服务更多顾客(处理更多任务),店铺(进程)的吞吐量变大。
-
需要管理: 员工之间需要协调,比如不能两个厨师抢同一个锅(需要锁机制)。
-
创建和管理有开销: 雇佣正式员工(创建线程)需要办手续,发工资(占用系统资源)。
-
4. 协程 (Coroutine) / Goroutine
-
比喻: 一个「超级实习生」或「兼职学生」
-
他不是一个正式员工(线程),但能干员工的活。
-
-
特点:
-
极低成本: 雇佣一个实习生(创建协程)手续非常简单,成本极低(内存占用KB级别)。你可以轻松雇几百个实习生。
-
超级灵活: 当某个实习生需要等货(等待IO,比如等网络数据),他会立刻主动让出位置,去干别的活,绝不摸鱼。
-
由店长管理: 这些实习生不由商场经理(操作系统)直接管理,而是由你们店铺自己的店长(用户态调度器,如Go的调度器)来调度。
-
5. 用户态 (User Space) vs 内核态 (Kernel Space)
这是理解为什么协程更高效的关键!
-
内核态 (Kernel Space): 商场总管理处
-
这是商场的核心权力机构,拥有最高权限,管理整个商场的水电、安全、所有店铺的协调。
-
特点: 权力大,但进去办事流程复杂,速度慢。
-
对应操作: 线程的创建、销毁、调度(哪个线程在哪个CPU核心上运行)都需要「总管理处」亲自审批和操作。这是一个很「重」的操作。
-
-
用户态 (User Space): 你自己的店铺内部
-
这是你的地盘,你在自己店铺里怎么安排工作,自己说了算。
-
特点: 权限小,但效率高,非常灵活。
-
对应操作: 协程的创建、销毁、调度都是在你的程序内部(你的店铺里)完成的,不需要惊动「总管理处」。这是一个很「轻」的操作。
-
关键区别:
-
创建一个正式员工(线程)=> 需要向商场总管理处(内核态) 打报告、走流程,开销大。
-
创建一个实习生(协程)=> 只需要店长(用户态调度器) 在自己店里安排一下,开销极小。
关系总结(一张图看懂)
text
商场 (计算机) | ├── 店铺A (进程A) │ ├── 正式员工1 (线程1) -- 由【商场总管理处】(内核态)调度 │ ├── 正式员工2 (线程2) -- 由【商场总管理处】(内核态)调度 │ └── 一群实习生 (协程) -- 由【店长】(用户态调度器)管理,跑在几个正式员工身上 | ├── 店铺B (进程B) │ └── ...
流程演进:
-
早期: 一个店铺只有一个员工(单线程进程),效率低。
-
发展: 一个店铺雇多个员工(多线程进程),效率高了,但雇佣和管理成本(创建/切换线程的开销)也高了。
-
现代(Go的方案): 店铺只雇少数几个核心正式员工(少数几个线程),但雇佣一大群成本极低、极其灵活的「超级实习生」(Goroutine)。店长(Go调度器)在自己店里(用户态)高效地安排这些实习生的工作,让他们充分压榨几个正式员工的能力,从而用极低的成本实现了极高的并发。
终极比喻
-
进程 = 一个独立的公司/店铺
-
线程 = 公司里的正式员工
-
多线程 = 公司里雇了很多正式员工一起干活
-
协程 = 公司里雇了一大群「超级实习生」,成本低,效率高,由部门经理直接管理
-
用户态 = 公司内部管理
-
内核态 = 政府/工商局管理
Goroutine
你可以把 Goroutine 想象成 “公司里的员工”。
我们来对比一下传统的“多线程编程”和 Go 语言的“Goroutine”。
场景:处理一个工作任务(比如,处理100份文件)
1. 传统多线程(其他语言,比如 Java/C++)
-
比喻: 这就像是雇佣正式员工。
-
特点:
-
成本高: 每雇佣一个正式员工(创建一个线程),公司都要付出很高的成本(占用大量内存和系统资源)。
-
管理难: 你不能随便雇佣成百上千个正式员工,否则人力资源部(操作系统)会忙不过来,公司(你的程序)也会被拖垮。
-
工作方式: 一个员工处理一份文件,如果文件很多,你只能开几个线程,让它们排队等着操作系统来调度。
-
结果: 虽然也能多人同时工作(并发),但员工数量(线程数)有限,创建和管理的开销都很大。
2. Go 语言的 Goroutine
-
比喻: 这就像是雇佣“超级实习生”。
-
特点:
-
成本极低: 创建一个 Goroutine 只需要极小的内存(初始栈只有 2KB),相当于雇一个实习生的成本几乎可以忽略不计。你轻松就能启动成千上万个 Goroutine。
-
一个“经理”管理所有实习生: Go 语言运行时自带一个“调度经理”(Scheduler)。这个经理只占用几个正式员工的名额(只跑在几个操作系统线程上),但他非常聪明,负责管理成千上万个“超级实习生”(Goroutine)。
-
超级高效: 当一个实习生(比如 G1)在等待(比如等硬盘读取文件),经理就会立刻让他休息,并把 CPU 资源分配给另一个准备好干活的实习生(G2)。这个切换速度非常非常快,就像经理在不停地给实习生们分配微小的任务一样。
-
协作式: 实习生们很懂事,会在适当的时候(比如遇到等待操作时)主动告诉经理:“经理,我先歇会,你去让别人干吧!” 这大大减少了管理的摩擦。
-
结果: 你可以轻松发动“人海战术”,派出成千上万个成本极低的“超级实习生”去处理海量的小任务,而且有一个聪明的“经理”在背后高效调度,保证 CPU 永远在干活,而不是在空闲等待。
核心总结:
-
Goroutine 是什么?
-
它是 Go 语言中的轻量级线程。你可以把它理解为一个超级便宜、高效的“工作任务执行者”。
-
-
它为什么牛?
-
轻量: 资源占用极小,可以轻松创建上百万个。
-
高效的调度: 由 Go 语言自身的调度器管理,而不是完全依赖操作系统,切换成本非常低。
-
简单的语法: 你只需要在普通的函数调用前加一个
go
关键字,就能让这个函数在一个新的 Goroutine 中并发执行。
// 普通函数调用,会按顺序执行 processFile("file1.txt") processFile("file2.txt") // 等上一行执行完才执行// 使用 Goroutine,两个函数调用会并发执行 go processFile("file1.txt") // 丢给一个“实习生”去做 go processFile("file2.txt") // 再丢给另一个“实习生”去做 // 主函数会继续向下执行,不会等待它们
-
-
你需要关心什么?
-
当你有成千上万个“实习生”在同时处理数据时,你需要注意他们之间的通信和协作。比如,两个实习生不能同时修改同一份数据,否则会乱套。这就是为什么 Go 语言提供了 Channel(通道) 这个强大的工具,让 Goroutine 之间可以安全、有序地传递数据和同步状态。
-
Channel 就像一个传送带或者公司内部的工作交接区。一个实习生把处理完的结果放到传送带上,另一个实习生从传送带上取走继续处理。这样既完成了协作,又避免了冲突。
-
一句话终极比喻:
Goroutine 就是让你能用“开一个线程”的成本,去发动一场“千军万马”的并发任务。
Go关键字实现协程
创建协程
可以看到主线程和协程是并行执行的,不分先后
package mainimport ("fmt""time"
)func newTask() {i := 0for {i++fmt.Printf("new Goroutine: i = %d\n", i)time.Sleep(1 * time.Second)}
}func main() {//创建协程执行newTask函数go newTask()i := 0for {i++fmt.Printf("main Goroutine: i = %d\n", i)time.Sleep(1 * time.Second)}
}------------------------------------------------PS D:\GoProject\firstGoProject> go run firstGoProject.go
main Goroutine: i = 1
new Goroutine: i = 1
new Goroutine: i = 2
main Goroutine: i = 2
main Goroutine: i = 3
new Goroutine: i = 3
new Goroutine: i = 4
main Goroutine: i = 4
main Goroutine: i = 5
new Goroutine: i = 5
new Goroutine: i = 6
main Goroutine: i = 6
main Goroutine: i = 7
new Goroutine: i = 7
new Goroutine: i = 8
main Goroutine: i = 8
main Goroutine: i = 9
new Goroutine: i = 9
new Goroutine: i = 10
main Goroutine: i = 10
main Goroutine: i = 11
new Goroutine: i = 11
new Goroutine: i = 12
main Goroutine: i = 12
用协程创建一个形参为空,返回值为空的函数
退出协程需要通过 runtime.Goexit() 来操作
package mainimport ("fmt""time"
)func main() {//用go创建承载一个形参为空,返回值为空的一个函数go func() {defer fmt.Println("A.defer")func() {defer fmt.Println("B.defer")fmt.Println("B")}()fmt.Println("A")}()//死循环for {time.Sleep(1 * time.Second)}
}--------------------------------------------------
PS D:\GoProject\firstGoProject> go run firstGoProject.go
B
B.defer
A
A.defer===============================================================================
func main() {//用go创建承载一个形参为空,返回值为空的一个函数go func() {defer fmt.Println("A.defer")//退出当前协程return//下面代码不会被执行func() {defer fmt.Println("B.defer")fmt.Println("B")}()fmt.Println("A")}()//死循环for {time.Sleep(1 * time.Second)}
}--------------------------------------------------
PS D:\GoProject\firstGoProject> go run firstGoProject.go
A.defer===============================================================================
func main() {//用go创建承载一个形参为空,返回值为空的一个函数go func() {defer fmt.Println("A.defer")func() {defer fmt.Println("B.defer")//尝试在子函数中退出协程return //实际上只会退出当前子函数,不会退出协程fmt.Println("B")}()fmt.Println("A")}()//死循环for {time.Sleep(1 * time.Second)}
}--------------------------------------------------
PS D:\GoProject\firstGoProject> go run firstGoProject.go
B.defer
A
A.defer===============================================================================
func main() {//用go创建承载一个形参为空,返回值为空的一个函数go func() {defer fmt.Println("A.defer")func() {defer fmt.Println("B.defer")//在go协程的子函数退出整个协程操作runtime.Goexit()fmt.Println("B")}()fmt.Println("A")}()//死循环for {time.Sleep(1 * time.Second)}
}--------------------------------------------------
PS D:\GoProject\firstGoProject> go run firstGoProject.go
B.defer
A.defer
用协程创建一个有形参,有返回值的函数
package mainimport ("fmt""time"
)func main() {//用go创建一个有形参,有返回值的函数//注意这里协程和main函数是异步执行的,在main函数中并不能通过定义一个bool类型拿到返回值go func(a int, b int) bool {fmt.Println("a = ", a, "b = ", b)return true}(10, 20)//死循环for {time.Sleep(1 * time.Second)}
}--------------------------------------PS D:\GoProject\firstGoProject> go run firstGoProject.go
a = 10 b = 20