Python协程与Go协程深度解析:机制、调度与生态对比
本文深入对比Python与Go语言中协程的核心差异。Python协程基于EventLoop模型,采用非抢占式调度,依赖async/await语法,受GIL限制且生态存在阻塞库兼容问题;Go协程基于M:N线程映射,具备半抢占式调度特性,原生支持CSP并发模型并通过channel通信。文章详细解析两者在状态管理、资源占用及多核利用率上的不同表现。
进程、线程与协程的基础概念辨析
进程的定义
进程是计算机中正在运行的程序实体。程序本身仅由指令、数据及其组织形式构成,而进程则是这些程序在系统中的实际运行实例。
线程的定义
线程是操作系统进行运算调度的最小单位。它被包含在进程内部,是进程中实际执行工作的基本单元。
进程与线程的关系
一条线程代表进程中一个单一顺序的控制流。一个进程可以并发包含多个线程,每条线程并行执行不同的任务。由于CPU的最小调度单元是线程而非进程,因此单进程多线程架构能够有效利用多核CPU的性能。
协程的定义
协程通过在线程内部实现调度逻辑,避免了陷入内核级别上下文切换所带来的性能损耗,从而突破了传统线程在I/O操作上的性能瓶颈。
协程与线程的关系
协程是在语言层面实现对线程的调度,从而避免了内核级别的上下文切换消耗。
Python协程与调度机制
Python的协程概念源于yield指令。yield主要具备两个功能:
• yield item用于产出一个值,并将其反馈给next()的调用方。 • 作出让步,暂停执行生成器,将控制权交还给调用方继续工作,直到需要获取另一个值时再调用next()。
import asyncio import datetime async def display_date(): loop = asyncio.get_running_loop() end_time = loop.time() + 5.0 while True: print(datetime.datetime.now()) if (loop.time() + 1.0) >= end_time: break await asyncio.sleep(1) asyncio.run(display_date())
协程本质上是对线程的调度,yield类似于惰性求值的方式,可视为一种流程控制工具,用于实现协作式多任务处理。在Python 3.5中正式引入Async/Await表达式,使得协程在语言层面得到正式支持和优化,大幅简化了此前基于yield的复杂写法。
线程由内核进行抢占式调度,这确保了每个线程都有执行的机会。
而协程运行在同一个线程中,由语言运行时中的EventLoop(事件循环)负责调度。
与大多数语言类似,在Python中,协程的调度是非抢占式的。这意味着一个协程必须主动让出执行机会,其他协程才有机会运行。
让出执行权的关键字是await。如果一个协程发生阻塞且持续不让出CPU,那么整个线程将会卡住,导致没有任何并发效果。
PS: 作为服务端,EventLoop最核心的技术是IO多路复用,所有来自客户端的请求均由IO多路复用函数处理;作为客户端,EventLoop的核心在于利用Future对象延迟执行,并使用send函数激发协程挂起,等待服务端处理完成返回后再调用CallBack函数继续后续流程。
Go的协程在语言层面天生支持,与Python类似都采用了关键字,但Go语言使用了go关键字,这可能意在表明协程是Go语言中最重要的特性之一。
Go协程之间的通信,Go采用了channel关键字。
Go实现了两种并发形式:
• 多线程共享内存。如Java或C++等在多线程中共享数据(例如数组、Map、某个结构体或对象)时,通过锁来访问。 • Go语言特有且推荐的方式:CSP(communicating sequential processes,通信顺序进程)并发模型。
Go的CSP并发模型实现涉及M, P, G机制:
package main
import (
"fmt"
)
//Go 协程(goroutines)和协程(coroutines)
//Go 协程意味着并行(或者可以以并行的方式部署),协程一般来说不是这样的
//Go 协程通过通道来通信;协程通过让出和恢复操作来通信
// 进程退出时不会等待并发任务结束,可用通道(channel)阻塞,然后发出退出信号
func main() {
jobs := make(chan int)
done := make(chan bool) // 结束标志
go func() {
for {
j, more := <-jobs // 利用more这个值来判断通道是否关闭,如果关闭了,那么more的值为false,并且通知给通道done
fmt.Println("----->:", j, more)
if more {
fmt.Println("received job", j)
} else {
fmt.Println("end received jobs")
done <- true
return
}
}
}()
go func() {
for j := 1; j <= 3; j++ {
jobs <- j
fmt.Println("sent job", j)
}
close(jobs) // 写完最后的数据,紧接着就close掉
fmt.Println("close(jobs)")
}()
fmt.Println("sent all jobs")
<-done // 让main等待全部协程完成工作
}
通过在函数调用前使用关键字go,我们即可让该函数以goroutine方式执行。goroutine是一种比线程更加轻盈、更省资源的协程。
Go语言通过系统线程来多路派遣这些函数的执行,使得每个用go关键字执行的函数可以运行成为一个独立的协程。当一个协程阻塞时,调度器会自动把其他协程安排到另外的线程中去执行,从而实现程序的无等待并行化运行。而且调度的开销非常小,一颗CPU调度的规模不下于每秒百万次,这使得我们能够创建大量的goroutine,从而可以很轻松地编写高并发程序,达到我们想要的目的。 ---- 某书
协程的4种状态
• Pending(挂起) • Running(运行) • Done(完成) • Cancelled(取消)
与系统线程之间的映射关系
Go的协程本质上还是系统的线程调用,而Python中的协程是EventLoop模型实现,所以虽然都叫协程,但并不是一个东西。Python中的协程是严格的1:N关系,也就是一个线程对应了多个协程。虽然可以实现异步I/O,但是不能有效利用多核(GIL)。而Go中是M:N的关系,也就是N个协程会映射分配到M个线程上,这样带来了两点好处:
• 多个线程能分配到不同核心上,CPU密集的应用使用goroutine也会获得加速。 • 即使有少量阻塞的操作,也只会阻塞某个worker线程,而不会把整个程序阻塞。
PS: Go中很少提及线程或进程,也就是因为上面的原因。
两种协程对比:
• async是非抢占式的,一旦开始采用async函数,那么你整个程序都必须是async的,不然总会有阻塞的地方(一遇阻塞对于没有实现异步特性的库就无法主动让调度器调度其他协程了),也就是说async具有传染性。 • Python整个异步编程生态的问题,之前标准库和各种第三方库的阻塞性函数都不能用了,requests不能用了,redis.py不能用了,甚至open函数都不能用了。所以Python协程的最大问题不是不好用,而是生态环境不好。 • goroutine是go与生俱来的特性,所以几乎所有库都是可以直接用的,避免了Python中需要把所有库重写一遍的问题。 • Goroutine中不需要显式使用await交出控制权,但是Go也不会严格按照时间片去调度goroutine,而是会在可能阻塞的地方插入调度。Goroutine的调度可以看做是半抢占式的。
PS: python异步库列表 [https://github.com/timofurrer/awesome-asyncio]
--------------------------------------------------------------------------------
Do not communicate by sharing memory; instead, share memory by communicating.(不要以共享内存的方式来通信,相反,要通过通信来共享内存) -- CSP并发模型
--------------------------------------------------------------------------------
扩展与总结
Erlang和Golang都采用了CSP(Communicating Sequential Processes)模式(Python中的协程是EventLoop模型)。但是Erlang是基于进程的消息通信,Go是基于goroutine和channel的通信。Python和Go都引入了消息调度系统模型,来避免锁的影响和进程/线程开销大的问题。协程从本质上来说是一种用户态的线程,不需要系统来执行抢占式调度,而是在语言层面实现线程的调度。因为协程不再使用共享内存/数据,而是使用通信来共享内存/锁,因为在一个超级大系统里具有无数的锁,共享变量等等会使得整个系统变得无比的臃肿,而通过消息机制来交流,可以使得每个并发的单元都成为一个独立的个体,拥有自己的变量,单元之间变量并不共享,对于单元的输入输出只有消息。开发者只需要关心在一个并发单元的输入与输出的影响,而不需要再考虑类似于修改共享内存/数据对其它程序的影响。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









