同步、异步、阻塞、非阻塞、回调函数

同步、异步、阻塞、非阻塞、回调函数

一、同步、异步和回调函数

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 的系统调用通常都会产生阻塞,比如readwriterecv等函数。

为什么会产生阻塞呢?

这是因为 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函数会有参数(一般是指针)来携带出系统调用的数据,以供我们后期处理。

那么本篇讲解同步、异步、阻塞、非阻塞、回调函数的文章就到这里啦!

欢迎留言纠错!

posted @ 2024-05-28 22:33  一转身已万水千山  阅读(54)  评论(0编辑  收藏  举报