进程,是计算机中已运行程序的实体。程序本身只是指令、数据及其组织形式的描述,进程才是程序的真正运行实例。
操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
协程通过在线程中实现调度,避免了陷入内核级别的上下文切换造成的性能损失,进而突破了线程在IO上的性能瓶颈。
协程是在语言层面实现对线程的调度,避免了内核级别的上下文消耗。
Python的协程源于yield指令。yield有两个功能:
协程是对线程的调度,yield类似惰性求值方式可以视为一种流程控制工具,实现协作式多任务。协程的yield语句写在表达式右边(func = yield),可以产出值,也可以不产出值,如果yield后面没有表达式,则生成器产出None。协程也可以从调用方接受数据如send(data)。 在语言层面对协程的引用来源于pep342,pep342详细介绍了协程的使用:
Coroutines are a natural way of expressing many algorithms, such as simulations, games, asynchronous I/O, and other forms of event-driven programming or co-operative multitasking.
重要的几点大概如下:
将yield视为一个表达式而不是一个指令
增加send方法启动生成器和发送值
增加throw方法抛出异常
增加close方法用于退出
在Python3.5正式引入了 Async/Await表达式,使得协程正式在语言层面得到支持和优化,大大简化之前的yield写法。 例如:
import asyncio
async def hello_world():
print("Hello World!")
asyncio.run(hello_world())
Go的协程是天生在语言层面支持,和Python类似都是采用了关键字,而Go语言使用了go这个关键字,可能是想表明协程是Go语言中最重要的特性。
package main
import (
"fmt"
)
func Add(a, b int) {
c := a + b
fmt.Println(c)
}
func main() {
for i := 0; i < 10; i++ {
go Add(i, i)
}
}
但是上面的程序并不会打印出结果,因为程序没有等待Add函数结束,就已经结束了。而协程之间的通信,Go采用了channel关键字。
协程的思想源于进程和协程都是属于系统内核级的,开销特别巨大,并且在并发模式下需要各种各样的锁来保证程序运行正常。后来在Node.js里引入了基于回调的非阻塞/异步IO,但是给开发者带来了无尽的“回调地狱”:
asyncFunc1(opt, (...args1) => {
asyncFunc2(opt, (...args2) => {
asyncFunc3(opt, (...args3) => {
asyncFunc4(opt, (...args4) => {
// some operation
});
});
});
});
也正是因为如此“丑陋”的代码存在,Python和Go引入了消息调度系统模型,也就是协程,来避免锁的影响和进程/线程开销大的问题。协程从本质上来说是一种用户态的线程,不需要系统来执行抢占式调度,而是在语言层面实现线程的调度。具体的消息调度模型可以参考我之前写的文章Actor模型速览。 因为协程不再使用共享内存/数据,而是使用通信来共享内存/锁,因为在一个超级大系统里具有无数的锁,共享变量等等会使得整个系统变得无比的臃肿,而通过消息机制来交流,可以使得每个并发的单元都成为一个独立的个体,拥有自己的变量,单元之间变量并不共享,对于单元的输入输出只有消息。开发者只需要关心在一个并发单元的输入与输出的影响,而不需要再考虑类似于修改共享内存/数据对其它程序的影响。