计算机有两种阻塞,一是 cpu 阻塞,二是 io 阻塞。cpu 阻塞就是 cpu 密集计算,io 阻塞比如等待网络响应,等待磁盘响应,纯粹是浪费时间。线程机制和异步机制都可避免 io 阻塞,但 cpu 阻塞的负面效果就只有线程可以避免了。
在现代计算机语言里,大量线程切换虽然会有性能问题,但是线程用起来简单,而且线程能在固定时间切换,可保证实时性,不是 io 高并发就直接用线程吧。async 和多线程并不是二选一,在同一应用中,可以根据情况两者一起使用。
https://course.rs/advance/async/getting-started.html:
有大量 IO 任务需要并发运行时,选 async 模型
有部分 IO 任务需要并发运行时,选多线程,如果想要降低线程创建和销毁的开销,可以使用线程池
有大量 CPU 密集任务需要并行运行时,例如并行计算,选多线程模型,且让线程数等于或者稍大于 CPU 核心数
无所谓时,统一选多线程
async 和多线程的性能对比
操作 async 线程
创建 0.3 微秒 17 微秒
线程切换 0.2 微秒 1.7 微秒
可以看出,async 在线程切换的开销显著低于多线程,对于 IO 密集的场景,这种性能开销累计下来会非常可怕!
通过线程机制的不断切换线程可让所有线程得到执行,即使 cpu 在进行密集计算导致 cpu 占用 100% 也能保证整个系统的流畅度。
异步是在线程之内的,没有异步机制的话一个线程里就不能同时跑多个任务,多个任务只能挨个进行,就像执行普通函数那样。异步机制不会主动切换任务,只能让任务自己退出来才能进行下一个任务,如果异步里的任何一个任务占据 cpu 时间过长就会显的该异步很卡。
异步机制本质上就是非阻塞状态机(switch case),单片机常用,这是一个系统化的工程,需要让其所有功能都是非阻塞的,也就是让所有的阻塞的功能都要通过不断查询来获得结果,然后再将这些需要查询的功能每个都单独放一个 case 里。
比如一个 delay_ms() 函数,如果在线程的保存数据并切换线程的机制下实现就是查询时间到没到,没到就切换线程,到了就继续该线程。而非阻塞状态机里的实现也是查询时间,但要根据其返回值来判断是否到没到时间,到了就手动加一步,下次轮询该任务就可以执行下一步了,没到就退出,就可以轮询下一个任务了。
异步机制的简单实现:
uint32_t current_time = 0;
struct delay_t
{
bool working;
uint64_t end_time;
};
// 时间到了返回 true
bool delay_ms(delay_t* self, uint64_t ms)
{
// 初始化一次
if (self->working == false)
{
self->working = true;
self->end_time = current_time + ms;
return false;
}
// 如果时间没到
if (current_time < self->end_time)
return false;
self->working = false;
return true; // 时间到了
}
void task_1()
{
static uint8_t step = 0;
static delay_t delay = {0};
switch (step)
{
case 0:
printf("task_1");
step++;
break;
case 1:
if(delay_ms(&delay, 500))
step = 0;
break;
}
}
void task_2()
{
static uint8_t step = 0;
static delay_t delay = {0};
switch (step)
{
case 0:
printf("task_2");
step++;
break;
case 1:
if(delay_ms(&delay, 1000))
step = 0;
break;
}
}
// 类似 delay_ms() 这样阻塞的都要改成轮询式
int main()
{
while (true)
{
current_time++; // 模拟当前时间
task_1();
task_2();
}
}