同步、异步、阻塞、非阻塞、回调函数
同步、异步、阻塞、非阻塞、回调函数
一、同步、异步和回调函数
1. 概念
程序在执行过程中会存在函数调用,区分同步和异步的关键点在于函数调用后主程序如何运行。
-
同步:函数调用后,主程序等待着函数返回才会继续往下运行。
-
异步:函数调用后,主程序不等待函数返回就继续往下运行。
下图示例的程序中,在调用sum
函数时,主程序等待着sum
函数返回才继续往下运行,这就是同步。
下图示例的程序中,在调用delete_file
函数时,主程序并没有等待delete_file
函数返回就继续往下运行了,这就是异步。
既然异步的情况下,主程序不管子函数什么时候结束就继续往下执行了,那主程序什么时候知道子函数执行结束了呢?答案就是回调函数。
那什么是回调函数呢?
我在上班的时候,接收的任务都是上司的左膀右臂下发的,他们把任务告诉我,我去实验室干活,干完以后他们让我发个 vx 消息告诉他们我干完了。他们不会守着我干活,也在忙自己的事。这个场景里面的左膀右臂就是主程序,我就是子函数,vx 通知他们就是回调函数。回调函数是主程序向子函数传输的 1 个函数指针(关于函数指针请 RTFW),用来告诉子函数在它完成后该执行哪些操作来通知主程序。
2. 举例
加班对于程序员来说是家常便饭(FUCK),某天晚上 11 点,你准备想下个早班,刚准备关鸡卸下戴了一天的面具重新做人,你直属上司突然从墙后面冒出来跟你说,”马工,现在还早,今晚加个班再多写几个 bug 吧“。说完,你上司从兜里拿出一包瓜子坐在你旁边嗑起来,行情不好,你又是个被世俗污染过的人(没个性),只好认命加班。
你上司(主程序)在你(子函数)旁边嗑瓜子等着你写 bug,等你写完才准你下班,这就是同步。
第二天晚上,你已经连续加班 29 天了,再差 1 天就满 1 个月了,你决定不要达成这项可耻的成就,于是你打算偷偷溜走,没想到老板在电梯门口把你堵住了。”马工啊,这个月你确实辛苦了,但是程序明天上线,要不,你今晚加班守一下上线情况?“。你心里跑过一万只 ikun,但是也只能回去达成加班 1 个月成就,但是老板肯定是不用加班的,于是他走进电梯下班回家了。
你老板(主线程)要你加班,但是他自己开奔驰回家了,管你(子函数)加班到几点(不守着你),是死是活,等你写完 bug 后给老板发微信留个言报平安,这就是异步。发 vx 给老师报项目上线一切正常,这就是回调函数。
二、阻塞和非阻塞
1. 概念
阻塞、非阻塞同样是看主程序执行过程中,对函数调用后的情况来判断。区分阻塞和非阻塞的关键点在于函数调用后 CPU 的使用权是否会被转移。
- 阻塞:函数调用后,由于 I/O 等操作导致了 CPU 暂停执行本段程序。最常见的是磁盘读写操作导致的 CPU 被切换去执行其他进程。发生阻塞的情况基本上都涉及到系统调用
- 非阻塞:函数调用后,CPU 不会因为此次调用而被切换去执行其他进程。
2. 举例
写代码的时候会使用到系统调用(RTFW),涉及到磁盘 I/O 的系统调用通常都会产生阻塞,比如read
、write
、recv
等函数。
为什么会产生阻塞呢?
这是因为 CPU 和磁盘的速度差距有如天壤之别导致的(如果 CPU 的速度比作是战斗机,那么磁盘的读写速度就是肯德坤)。为了最大化 CPU 的执行效率,CPU 不会空等着磁盘读写完成,而是切换到其他进程去执行,等磁盘读写完成后再返回来继续执行主程序剩下的代码。由于 CPU 的使用权被转移,导致本程序被暂停执行,这种情况下就说程序由于这个函数调用被阻塞了。
int main(int argc, char* argv[]) {
FILE* file = open("file_path");
int res = read(file); // 此时程序被阻塞 n 秒
process(file); // 后面的程序在 read 函数返回后才能继续执行
int res = write(file);
}
非阻塞就是指主程序在函数调用时不会导致 CPU 切换至其他进程上去执行(除非时间片切换)
long sum(int a, int b) { // 调用 sum 求 1-10000 亿的和
long sum = 0;
for (int i = a; i <= b; ++i) {
sum += i;
}
return sum;
}
如果调用 sum 求 1-10000 亿的和,可能要计算很久(应该也不用很久,几分钟?但是几分钟对于 CPU 来说已经算很久了),但是由于 CPU 一直在子函数里面运行(CPU 并没有被转移使用权去执行其他进程),所以成调用 sum 函数不会导致程序被阻塞,即 sum 函数是不阻塞的。
总结:阻塞和非阻塞与 CPU 的执行流是否被暂停有关。
三、关系
同步、异步、阻塞、非阻塞这 4 者的关系分为同步阻塞、同步非阻塞、异步非阻塞。一般没有异步阻塞,除非出现程序 bug。
1. 同步阻塞
int main(int argc, char* argv[]) {
FILE* file = open("file_path");
int res = read(file); // 此时程序被阻塞 n 秒
process(res); // 后面的程序在 read 函数返回后才能继续执行
int res = write(file);
}
还是上面那个例子,只有在read
函数返回以后才能读取到结果,进而对结果进行处理,所以这是同步阻塞的。
2. 同步非阻塞
同步不一定是阻塞的,但阻塞一定是同步的。
long sum(int a, int b);
int main(int argc, char* argv[]) {
int a = 1;
int b = 100000000000000000000000000000000000000000;
long res = sum(a, b);
printf("hello,world");
}
上面的 sum 函数可能要执行好几分钟,并且得 sum 函数执行完成以后才会打印 hello,world,虽然执行时间长,但是 CPU 使用权没有被转移,所以这是同步非阻塞的。
3. 异步非阻塞
我们对同步阻塞的程序增加一个需求,那就是在读取文件以后,需要让用户进行输入。
int main(int argc, char* argv[]) {
FILE* file = open("file_path");
int res = read(file); // 此时程序被阻塞 n 秒
process(res); // 后面的程序在 read 函数返回后才能继续执行
int res = write(file);
// 处理用户输入
int in = input();
process(in);
}
用户永远是上帝,如果现在还是使用read
这种导致阻塞的函数,并且这段代码是你写的话,那么你即将得到解脱,黑眼圈、焦虑症、晚睡早起等症状都会得到解决。
为了让自己能继续打工(继续主动折磨自己),你决定修改代码为异步非阻塞。
假设你的操作系统提供了一个异步非阻塞的_read
函数,那么你可以这样写你的代码:
void callback();
int main(int argc, char* argv[]) {
FILE* file = open("file_path");
int res = _read(file, callback); // 异步调用,callback 被当作回调函数传入 _read
// 处理用户输入
int in = input();
process(in);
}
此时由于_read
函数是异步调用的,那么调用_read
后会立即被返回,就可以马上处理用户的输入啦!
这时候有同学就会问,那我怎么知道_read
函数什么时候结束?注意看,我们给_read
函数传入了回调函数callback
,这是库函数的开发者提供的接口,如果你使用的操作系统支持异步调用的话一定会有这样的标准接口。既然这样,那么_read
函数在读取完磁盘的数据以后就会调用我们写的callback
函数,那么我们就可以在callback
函数里面去写我们拿到磁盘数据以后处理数据的操作逻辑(在主程序中已经省略掉了处理数据的代码)。当然这只是一个例子来说明异步调用,正常情况说callback
函数会有参数(一般是指针)来携带出系统调用的数据,以供我们后期处理。
那么本篇讲解同步、异步、阻塞、非阻塞、回调函数的文章就到这里啦!
欢迎留言纠错!