本文所有的内容基于linux平台,同时也是基于作者自身的理解,有不当之处,还请指出,同时 仅供参考
在解释这三个的概念之前,让我们先有个 不太正确,但又有一定道理的认识吧:
- 线程,人称 轻量级进程
- 协程,人称 轻量级线程
到这里,其实你就能够推断出他们三者之间的 量级 关系了:进程 > 线程 > 协程
那自然而然,你就能够继续推出,X的创建成本和X之间的切换成本 应该是与 X的量级 成正相关的,即 量级 越大,成本 越高
事实上,正常情况下也确实是如此,恭喜你,你已经初步了解了他们之间的区别了
在继续了解之前,你需要了解 用户态和内核态 内核态与用户态
线程
线程,是程序执行的最小单位,我们将程序 自然执行 的顺序称为 执行流,那么,一个 线程,其实就是一个 执行流 了
在 一般情况 下,我们的程序只有一个 执行流
以C语言为例,就是,我们从 main 函数进入,然后执行 main函数内的内容,最后从 main 函数返回,程序就结束了
#include <stdio.h>
int main(void)
{
printf("hello world\n");
return 0;
}
因为程序都是从 main 函数启动,所以这个 执行流 又被称为 主线程
那有 主线程,是不是还有其他 从线程?有没有 从线程 我不知道,不过确实是可以有其他 线程
// Usage: gcc -std=c11 -o main main.c -pthread && ./main
#include <stdio.h>
#include <assert.h>
#include <threads.h>
int work(void *arg)
{
(void) arg; // ignore
printf("hello, this is new thread\n");
return 0;
}
int main(void)
{
printf("hello, this is main thread\n");
thrd_t thread;
// create a thread
assert(thrd_create(&thread, work, NULL) == thrd_success);
// main thread wait the new thread
assert(thrd_join(thread, NULL) == thrd_success);
return 0;
}
在这个例子中,我们新创建了一个 线程,其实它的效果和你调用一个普通函数没有差别,但他们不一样的地方在于:
- 在main函数中调用普通函数,一个执行流,main函数只有等调用函数结束后才能继续执行
- 在main函数中创建另一个线程并执行,两个执行流,main函数在创建并启动完新线程后,可以继续执行,同时新线程也能同时执行
是不是察觉到区别了,没错,多个执行流 就能够实现 并发 甚至 并行 的效果(并发是通过快速的切换模拟同时进行,并行是真正的同时进行)
好了,到这里,相信你已经能够理解 线程和执行流 的关系了
进程
进程,是资源分配的最小单位
为什么我们要先讲 线程 呢?原因是,进程其实就包括了主线程以及其他资源,执行流 依赖的是 CPU,而这也是一种资源,所以我希望大家能够对 执行流 和 主线程 有一定的了解,才能理解 进程是资源分配的基本单位,执行也是从进程开始的
进程要讲起来的话需要非常大的篇幅,其中就是关于它所拥有的资源的介绍,我们不打算介绍,简单将其理解为变量和内存即可(和我们的例子相关)
函数的执行信息(包括调用者,被调用者,以及他们的局部变量等)会被存储在栈上,以一种逻辑结构 栈帧 的形式存储,前文我们提到,我们除了 主线程,还可以创建其他的 线程,这里我们将其规范一下,进程可以拥有多个线程
想想看,每个 线程 都是一个 执行流,那必然都需要有自己的执行环境,所以我们就将 进程 的 内存资源 划分一部分作为 每个线程的私有栈,这样他们的私有信息就不会交杂在一起,造成污染了,同样的,既然 这些线程都属于一个进程,那么就意味着他们能够同时访问这个进程中的某一部分资源,这部分资源就称作 公共资源
前文提到过,多个执行流,即多线程 可以实现 并行 的效果,也就是同时进行,所以在 多线程 环境下,我们有多个 同时进行 的 执行流,但是 公共资源 只有一份,这就意味着会产生 竞争,此时对数据的保护就尤为重要了
到这里,我相信你对 进程和资源 以及 多线程 有了更深的了解,那你是否想过 多进程 呢?
多进程 就意味着,有多份 独立 的资源,因为每个进程内的 执行流 只能在自己的环境中执行,不会出现 跨进程 的情况,这主要得益于 虚拟内存 的机制,感兴趣的同学可以进一步查找资料
我差点忘了,我们还没有举例子,现在来几个例子吧
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int data = 0; // 公共变量
int main(void)
{
pid_t pid = fork(); // 从此刻起,我们有了两个进程,他们是相同的代码,并进入不同的分支执行
if (pid == 0) {
// child process
data = 1;
printf("child process: data = %d\n", data);
exit(0);
}
// parent process
wait(NULL);
printf("parent process: data = %d\n", data);
return 0;
}

在上述的例子中,我们验证了,每个进程都有自己的资源(即在父子进程中都改变公共变量,并不会影响对方)
接下来在看看多线程的情况吧
#include <stdio.h>
#include <assert.h>
#include <threads.h>
int data = 0;
int work(void *arg)
{
(void) arg;
data += 1;
printf("new thread: data = %d\n", data);
return 0;
}
int main(void)
{
thrd_t thread1, thread2;
// create two new threads
assert(thrd_create(&thread1, work, NULL) == thrd_success);
assert(thrd_create(&thread2, work, NULL) == thrd_success);
// main thread wait unitl other threads finish
assert(thrd_join(thread1, NULL) == thrd_success);
assert(thrd_join(thread2, NULL) == thrd_success);
printf("main thread: data = %d\n", data);
return 0;
}

可以看到在这个例子中,线程 对公共变量的修改反映在了 其他线程 上,这就说明这个 公共变量 不是线程私有的
协程
协程,一种特殊的控制流,我们这里引入了一个新的概念 --- 控制流,还记得我们之间的 执行流 吗,不知道你有没有注意到 自然执行,其实简单的 自上而下,而 控制流 就是改变 执行流 的执行顺序,比如常用的 if/while/for
这里我们说 协程是一种特殊的控制流,这只是我的理解方式,因为本质上,协程 是在一个 执行流 中改变执行的顺序,所以应该是一种 控制流,但是它特殊的地方在于,他将这种 改变的方法 封装成了一个 伪执行流,而不是传统的 条件判断,这么说还是有点抽象了,让我们举个例子吧
这是我自己实现的一个简单的协程
#include "coroutine.h"
void *counter(void *arg)
{
(void) arg;
int n = 4;
for (int i = 0; i < n; i++) {
printf("[%zu] %d\n", coroutines.cur_co_id, i);
// 停止执行,返回到main函数中
coroutine_yield(NULL);
}
coroutine_finish();
return NULL;
}
int main(void)
{
coroutine_init();
// 创建协程,每个协程的启动函数是 'counter'
struct Coroutine *co1 = coroutine_create(counter);
struct Coroutine *co2 = coroutine_create(counter);
struct Coroutine *co3 = coroutine_create(counter);
int dead = 0;
while (1) {
// 切换不同的协程执行,如果状态都是死亡,就跳出循环
coroutine_resume(co1); dead += (co1->status == DEAD) ? 1 : 0;
coroutine_resume(co2); dead += (co2->status == DEAD) ? 1 : 0;
coroutine_resume(co3); dead += (co3->status == DEAD) ? 1 : 0;
if (dead == 3) break;
}
printf("well done\n");
coroutines_destroy();
return 0;
}
如果你之前看懂了 线程 的例子的话,那么这里的例子应该没什么问题,你可以对比一下,你会发现,两者的区别在于
-- 创建线程之后,该干嘛干嘛,线程会 自动 执行,
-- 创建协程之后,你需要 手动 的进行切换,停止等等
我们看看效果,可以看到,输出的效果是不是有点 并发 的影子,但我们只使用了一个 执行流,我们通过对执行环境的切换,从而进入不同的 协程 中,这其实和 线程 很像,但它只有一个 执行流,所以看起来就类似 全局goto 的感觉,只不过它将其封装成了 协程,更加的优雅,容易管理

对比
如果你能看到这里,我想,你应该是 一头雾水 的,:-),这不是你们不行,而是因为我在写的时候,总感觉有很多可以说,但是又不能全部写下来,所以总感觉比较杂乱,你可以试着说说 进程、线程、协程 之间的联系和区别,可能只能记住每一个小节开头的那句话,这是我的问题
然后在这里,我们总结总结他们的区别和联系
需要注意的是,本文聚焦的是 一对一 的线程模型以及 有栈 协程
linux中的线程 这篇文章的末尾介绍了下线程实现模型
进程和线程
-
扮演的角色
- 进程是资源分配的最小单位
- 线程是程序执行的基本单位
-
独立性对比
- 进程之间由于虚拟内存机制的原因,独立性更强
- 线程之间由于共享进程的一部分资源,关联性更强
-
安全性对比
- 进程之间的独立性更强,某个进程的问题不容易扩散到其他进程,安全性更强
- 线程之间的关联性更强,由于资源共享的原因,对于共享资源的访问会造成并发冲突,且非常容易扩散,安全性较低
-
切换成本
- 进程的量级更大,上下文切换包括其所拥有的资源以及执行环境
- 线程的量级更小,上下文切换只需要切换自己私有的部分和执行环境,公共部分由进程保留
线程和协程
-
扮演的角色
- 线程是程序执行的基本单位
- 协程是一种特殊的控制流
-
切换方式
- 线程之间的切换是抢占式的,由系统进行调度
- 协程之间的切换是协作式的,手动进行调度
-
切换成本
- 线程的切换是由系统进行调度的,即需要陷入内核态,在进行上下文切换,成本更高
- 协程的切换是手动调度的,不需要陷入内核态,全部在用户态发生,成本更低
-
安全性对比
协程是在单个 执行流 中使用,并不会产生 并发 问题,因此更加安全
总结
事实上他们的联系和区别还有挺多方面的,比如在不同的线程实现模型下,他们的表现可能又不同,具体情况具体分析,这里的对比只能给出几个方面来分析他们之间的区别和联系,并不是说只有这些
再次强调,仅供参考,有错误的话,还请指出
posted on
浙公网安备 33010602011771号