Goroutine的概念
协程的概念:juejin.cn/post/746181...
Goroutine就是go语言对于协程的支持,go的并发只会用到goroutine,并不需要我们去考虑多进程或者多线程
一般OS线程栈大小为2MB-8MB,且线程在创建和上下文切换的时候是需要消耗资源的,会带来性能损耗
所以我们在使用多线程的时候,我们往往会通过池化技术,即创建线程池来管理一定数量的线程
而在Go语言中,我们用多Goroutine去执行这个函数就可以了,并不需要我们来维护类似线程池的东西
也不需要使用者去关心协程是怎么调度和切换的,因为这些都已经有Go语言内置的调度器帮我们做了
Goroutine的使用
Goroutine使用起来非常方便,通常我们会将需要并发的任务封装成一个函数,然后加上go关键字就等于开启了一个Goroutine
go
func()
go func()
主协程概念
和Java一样,Go程序的入口也是main函数。在程序开始执行的时候Go程序会为main()函数创建一个默认的Goroutine
我们称之为主协程,我们后来认为的创建了一些Goroutine,都是在这个主Goroutine的基础上进行的
go
package main
import "fmt"
func myGroutine() {
fmt.Println("myGroutine")
}
func main() {
go myGroutine()
fmt.Println("main end")
}
输出结果:
ini
main end
为什么只输出main end呢?明明是多协程任务,为什么只打印了主协程里的end,没有打印我们开启的协程输出的myGroutine
这是因为:当main()函数结束返回的时候主Goroutine已经结束了,当主协程退出的时候,其他剩余的协程不管是否运行完也一起退出
当主协程结束时,我们开启的协程还没有执行到fmt.Println("myGroutine"),所以我们创建的协程也就跟着退出了
多协程调用WaitGroup
就是Java里面的CountDownLatch,等待所有的协程执行完毕后主协程才能走下去
go
package main
import (
"fmt"
"sync"
"time"
)
func myGoroutine(name string, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 5; i++ {
fmt.Printf("myGroutine %s\n", name)
time.Sleep(10 * time.Millisecond)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go myGoroutine("goroutine1", &wg)
go myGoroutine("goroutine2", &wg)
wg.Wait()
fmt.Println("main end")
}
输出结果:
ini
myGoroutine goroutine2
myGoroutine goroutine1
myGoroutine goroutine1
myGoroutine goroutine2
myGoroutine goroutine1
myGoroutine goroutine2
myGoroutine goroutine2
myGoroutine goroutine1
myGoroutine goroutine1
myGoroutine goroutine2
main end
Java线程池对比
- 线程池的基本使用,直接对应的就是Go开启的Goroutine,Javaer应该一眼就能懂
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池(5个线程)
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交一个Runnable任务
executor.execute(() -> {
System.out.println("任务正在执行,线程: " + Thread.currentThread().getName());
});
}
}
- CountDownLatch多线程调用
java
import java.util.concurrent.*;
public class ThreadPoolWithCountDownLatch {
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 创建CountDownLatch,计数为5
CountDownLatch latch = new CountDownLatch(5);
// 提交5个任务
for (int i = 1; i <= 5; i++) {
final int taskId = i;
executor.execute(() -> {
try {
System.out.println("任务" + taskId + "开始执行");
// 模拟任务执行时间
Thread.sleep(1000 + taskId * 200);
System.out.println("任务" + taskId + "执行完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 每个任务完成后计数器减1
latch.countDown();
}
});
}
System.out.println("主线程等待所有任务完成...");
// 主线程等待,直到计数器减到0
latch.await();
System.out.println("所有任务已完成,主线程继续执行");
}
}
·
协程池的实现
Go语言虽然有着高效的GMP调度模型,理论上支持成千上万的Goroutine,但是不代表我们完完全全不需要管理Goroutine
什么东西都是过多会导致一些问题,Goroutine过多,会对调度、GC以及系统内存都会造成压力,这样会使我们的服务性能不升反降
那Java通过线程池来管理线程,那我们也可以用池化技术,构造一个协程池,把进程中的协程控制在一定数量
我们实现一个协程池不需要像Java一样需要去管理线程的复用。协程用完就丢,不需要复用

go
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
type Task struct {
f func() error // 具体的任务逻辑
}
func NewTask(funcArg func() error) *Task {
return &Task{
f: funcArg,
}
}
type Pool struct {
RunningWorkers int64 // 运行着的worker数量
Capacity int64 // 协程池worker容量
JobCh chan *Task // 用于worker取任务
sync.Mutex
}
func NewPool(capacity int64, taskNum int) *Pool {
return &Pool{
Capacity: capacity,
JobCh: make(chan *Task, taskNum),
}
}
func (p *Pool) GetCap() int64 {
return p.Capacity
}
func (p *Pool) incRunning() { // runningWorkers + 1
atomic.AddInt64(&p.RunningWorkers, 1)
}
func (p *Pool) decRunning() { // runningWorkers - 1
atomic.AddInt64(&p.RunningWorkers, -1)
}
func (p *Pool) GetRunningWorkers() int64 {
return atomic.LoadInt64(&p.RunningWorkers)
}
func (p *Pool) run() {
p.incRunning()
go func() {
defer func() {
p.decRunning()
}()
for task := range p.JobCh {
task.f()
}
}()
}
// AddTask 往协程池添加任务
func (p *Pool) AddTask(task *Task) {
// 加锁防止启动多个 worker
p.Lock()
defer p.Unlock()
if p.GetRunningWorkers() < p.GetCap() { // 如果任务池满,则不再创建 worker
// 创建启动一个 worker
p.run()
}
// 将任务推入队列,等待消费
p.JobCh <- task
}
func main() {
// 创建任务池
pool := NewPool(3, 10)
for i := 0; i < 20; i++ {
// 任务放入池中
pool.AddTask(NewTask(func() error {
fmt.Printf("I am Task\n")
return nil
}))
}
time.Sleep(1e9) // 等待执行
}
拓展 - 有栈协程和无栈协程
协程的实现方式分为有栈协程和无栈协程两种。有栈协程指每个协程会保存单独的上下文(执行栈、寄存器等)
有栈协程的唤醒和挂起就是拷贝、切换上下文,无栈协程指单个线程内的所有协程都共享一个执行栈,协程的切换就是简单的函数返回
-
有栈协程
函数运行在调用栈上,把函数作为一个协程,那么协程的上下文就是这个函数及其嵌套函数的(连续的)栈帧和寄存器的值
如果我们进行协程的调度,也就是保存当前正在运行的协程上下文,然后恢复下一个将要运行的协程的上下文
因为保存上下文和普通函数执行的上下文是一样的,所以有栈协程可以在任意嵌套函数中挂起 (无栈协程不行)
有栈协程的优点在易用性上,通常只需要调用对应的方法,就可以切换上下文挂起协程
在有栈协程调度的时候,需要频繁的切换上下文,开销比较大
从实现上看,有栈协程更接近于内核级线程,都需要为每个线程保存单独的上下文
区别在于有栈协程的调度是由应用程序自行实现的,对内核是透明的,而内核级线程的调度由系统内核完成的
-
无栈协程
相比于有栈协程直接切换栈帧的思路,无栈协程在不改变函数调用栈的情况下,采用类似生成器的思路实现了上下文切换
通过编译器将生成器改写为对应的迭代器类型 (内部是一个状态机)
无栈协程需要在编译器将代码编译为对应的状态机代码,挂起的位置在编译器确定
无栈协程的优点在于不需要保存单独的上下文,内存占用低,切换成本也低,性能高
缺点就是需要编译器提供语义支持,无栈协程的实现是通过编译器对语法糖做了支持