基本概念

◇ 进程是应用程序启动的实例,拥有自己的代码段、内存空间、所打开文件等系统资源。
◇ 线程从属于进程,是程序的实际执行者,拥有自己的执行栈空间、程序计数器、寄存器状态,这些常被称为”线程上下文”,其余进程资源是进程内所有线程共享的。
◇ 进程是资源分配的最小单位,线程是 CPU 调度的最小单位。
◇ CPU 调度线程时涉及到线程上下文的保存与恢复,上下文信息存储在系统内核的”线程控制块(Thread Control Block, TCB)”中,其中的程序计数器、寄存器状态直接保存值,执行栈则保存的是地址指针。线程执行栈原始内容保存在所属进程的内存空间中。
◇ 线程的五种状态及转换如下图:

◇ 多线程间的协作涉及到如下性能问题:
(1)同步锁。
(2)线程上下文的切换。
◇ 纤程(fiber)是在用户空间模仿出的线程。
(1)纤程与线程的相同点:纤程有独立于创建它代码的生命周期,有调度器(scheduler)控制谁是下一个运行的纤程,有纤程间同步机制。
(2)纤程与线程的区别:纤程采用非抢占式上下文切换,即当前执行的任务会一直执行到主动放弃时间片。
(3)多个纤程可运行在同一个或不同的线程上,当不同线程上的纤程有队列任务等共享数据时,也会涉及 mutex 等同步问题,只是纤程挂起时,属于同一线程的其他纤程仍然可以运行。
(4)同一线程上多个纤程不会同时运行,避免了互相竞争资源。
(5)同一线程上多个纤程不会运行于多个 CPU 核上。
◇ 协程(coroutine /,kəʊruː’tiːn/)是支持挂起和恢复的子程序,用于执行非抢占式多任务(non-preemptive multitasking, cooperative multitasking)。
(1)协程与调用它的代码处在同一个生命周期。
(2)协程在行为上更像函数而不是线程:协程将调用和恢复的控制权交由调用者管理,被调用后代码执行的控制权立即转移到协程,直到协程主动挂起,代码执行控制权会自动回到调用者。
(3)互相协作的多个协程只能运行在同一个线程上,这些协程不会同时运行,不存在竞争资源的同步问题。
(4)协程切换与函数调用的最大区别在于切换入口点数量,普通函数只有一个入口点,协程则有多个。
◇ 纤程与协程的参考文档:StackOverflow 问答链接问答中提到的论文
◇ 线程的调度是操作系统完成的。纤程和协程的调度则由应用程序或运行时库完成,所以被称为”用户空间的线程”。
◇ 纤程与协程有时被视为同一概念,特别是当纤程运行在同一个线程上的时候,具体看语境。

JS 中的协程

◇ ES6 提供的 Generator 可实现协程,以下使用 Generator 方式实现的生产者-消费者示例,主程序是生产者视角,由生产者驱动消费者消费产品。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function* consume() {
console.log('consume start');
while (true) {
var product = yield;

// 结束标识
if (typeof product == 'symbol' && Symbol.keyFor(product) === 'END') {
break;
}
console.log(`consume ${product}`);
}
console.log('consume exit');
}

const consumeIterator = consume();

function produce() {
consumeIterator.next(); // 启动消费者
let count = 1;
while (count <= 5) {
console.log(`produce ${count}`);
consumeIterator.next(count);
count++;
}
consumeIterator.next(Symbol.for('END'));
}
produce();

◇ Generator 机制由相应的 JS 引擎实现,对于 Generator 函数,解释器会在 yield 位置适时停止,并在记录执行上下文时候额外带上当前执行到函数体的具体位置,用于再次调用 next()方法的时候恢复到上次暂停的函数体内位置继续执行。