在这篇文章中我们来讨论一下到底什么是同步,什么是异步,以及在编程中这两个概念到底意味着什么,这些是进一步掌握高性能、高并发技术的基础,因此非常关键。
相信很多同学遇到同步异步这两个词的时候大脑瞬间就像红绿灯失灵的十字路口一样陷入一片懵逼的状态:
mengbi
是的,这两个看上去很像实际上也很像的词汇给博主造成过很大的困扰,这两个词背后所代表的含义到底是什么呢?
我们先从工作场景讲起。
苦逼程序员
假设现在老板分配给了你一个很紧急并且很重要的任务,让你下班前必须写完(万恶的资本主义)。为了督促进度,老板搬了个椅子坐在一边盯着你写代码。
你心里肯定已经骂上了“WTF,你有这么闲吗?盯着老子,你就不能去干点其他事情吗?”
老板仿佛接收到了你的脑电波一样:“我就在这等着,你写完前我哪也不去,厕所也不去”
1600911423466
这个例子中老板交给你任务后就一直等待什么都不做直到你写完,这个场景就是所谓的同步。
第二天,老板又交给了你一项任务。
不过这次就没那么着急啦,这次老板轻描淡写“小伙子可以啊,不错不错,你再努力干一年,明年我就财务自由了,今天的这个任务不着急,你写完告诉我一声就行”。
这次老板没有盯着你写代码而是转身刷视频去了,你写完后简单的和老板报告了一声“我写完了”。
1600911037338
这个例子老板交代完任务就去忙其它事情,你完成任务后简单的告诉老板任务完成,这就是所谓的异步。
值得注意的是,在异步这种场景下重点是在你写代码的同时老板在自己刷剧,这两件事在同时进行,因此这就是为什么一般来说异步比同步高效的本质所在,不管同步异步应用在什么场景下。
因此,我们可以看到同步这个词往往和任务的“依赖”、“关联”、“等待”等关键词相关,而异步往往和任务的“不依赖”,“无关联”,“无需等待”,“同时发生”等关键词相关。
By the way,如果遇到一个在身后盯着你写代码的老板,三十六计走为上策。
打电话与发邮件
作为一名苦逼的程序员是不能只顾埋头搬砖的,平时工作中的沟通免除不了,其中一种高效的沟通方式是吵架。。。啊不,是电话。
通常打电话时都是一个人在说另一个人听,一个人在说的时候另一个人等待,等另一个人说完后再接着说,因此在这个场景中你可以看到,“依赖”、“关联”、“等待”这些关键词出现了,因此打电话这种沟通方式就是所谓的同步。
1600923556187
另一种码农常用的沟通方式是邮件。
邮件是另一种必不可少沟通方式,因为没有人傻等着你写邮件什么都不做,因此你可以慢慢悠悠的写,当你在写邮件时收件人可以去做一些像摸摸鱼啊、上个厕所、和同时抱怨一下为什么十一假期不放两周之类有意义的事情。
同时当你写完邮件发出去后也不需要干巴巴的等着对方什么都不做,你也可以做一些像摸鱼之类这样有意义的事情。
1600923768618
在这里,你写邮件别人摸鱼,这两件事又在同时进行,收件人和发件人都不需要相互等待,发件人写完邮件的时候简单的点个发送就可以了,收件人收到后就可以阅读啦,收件人和发件人不需要相互依赖、不需要相互等待。
你看,在这个场景下“不依赖”,“无关联”,“无需等待”这些关键词就出现了,因此邮件这种沟通方式就是异步的。
同步调用
现在终于回到编程的主题啦。
既然现在我们已经理解了同步与异步在各种场景下的意义(I hope so),那么对于程序员来说该怎样理解同步与异步呢?
我们先说同步调用,这是程序员最熟悉的场景。
一般的函数调用都是同步的,就像这样:
funcA() {
// 等待函数funcB执行完成
funcB();
// 继续接下来的流程
}
funcA调用funcB,那么在funcB执行完前,funcA中的后续代码都不会被执行,也就是说funcA必须等待funcB执行完成,就像这样:
1600925448485
从上图中我们可以看到,在funcB运行期间funcA什么都做不了,这就是典型的同步。
注意,一般来说,像这种同步调用,funcA和funcB是运行在同一个线程中的,这是最为常见的情况。
但值得注意的是,即使运行在两个不能线程中的函数也可以进行同步调用,像我们进行IO操作时实际上底层是通过系统调用(关于系统调用请参考《程序员应如何理解系统调用》)的方式向操作系统发出请求的,比如磁盘文件读取:
read(file, buf);
这就是我们在《读取文件时,程序经历了什么》中描述的阻塞式I/O,在read函数返回前程序是无法继续向前推进的
read(file, buf);
// 程序暂停运行,
// 等待文件读取完成后继续运行
如图所示:
1600925867319
只有当read函数返回后程序才可以被继续执行。
当然,这也是同步调用,但是和上面的同步调用不同的是,函数和被调函数运行在不同的线程中。
因此我们可以得出结论,同步调用和函数与被调函数是否运行在同一个线程是没有关系的。
在这里我们还要再次强调,同步方式下函数和被调函数无法同时进行。
同步编程对程序员来说是最自然最容易理解的。
但容易理解的代价就是在一些场景下,注意,是在某些场景不是所有场景哦,同步并不是高效的,因为任务没有办法同时进行。
接下来我们看异步调用。
异步调用
有同步调用就有异步调用。
关于重要的异步调用,你可以参考这里。
同步 vs 异步
我们以常见的Web服务来举例说明这一问题。
一般来说Web Server接收到用户请求后会有一些典型的处理逻辑,最常见的就是数据库查询(当然,你也可以把这里的数据库查询换成其它I/O操作,比如磁盘读取、网络通信等),在这里我们假定处理一次用户请求需要经过步骤A、B、C然后读取数据库,数据库读取完成后需要经过步骤D、E、F,就像这样:
# 处理一次用户请求需要经过的步骤:
A;
B;
C;
数据库读取;
D;
E;
F;
其中步骤A、B、C和D、E、F不需要任何I/O,也就是说这六个步骤不需要读取文件、网络通信等,涉及到I/O操作的只有数据库查询这一步。
一般来说这样的Web Server有两个典型的线程:主线程和数据库处理线程,注意,这讨论的只是典型的场景,具体业务实际上可会有差别,但这并不影响我们用两个线程来说明问题。
首先我们来看下最简单的实现方式,也就是同步。
这种方式最为自然也最为容易理解:
// 主线程
main_thread() {
A;
B;
C;
发送数据库查询请求;
D;
E;
F;
}
// 数据库线程
DataBase_thread() {
while(1) {
数据库读取;
}
}
这就是最为典型的同步方法,主线程在发出数据库查询请求后就会被阻塞而暂停运行,直到数据库查询完毕后面的D、E、F才可以继续运行,就像这样:
1600994106960
从图中我们可以看到,主线程中会有“空隙”,这个空隙就是主线程的“休闲时光”,主线程在这段休闲时光中需要等待数据库查询完成才能继续后续处理流程。
在这里主线程就好比监工的老板,数据库线程就好比苦逼搬砖的程序员,在搬完砖前老板什么都不做只是紧紧的盯着你,等你搬完砖后才去忙其它事情。
显然,高效的程序员是不能容忍主线程偷懒的。
是时候祭出大杀器了,这是什么大杀器呢,关于这个问题的答案你可以参考这里。
总结
在这篇文章中我们从各种场景分析了同步与异步这两个概念,但是不管在什么场景下,同步往往意味着双方要相互等待、相互依赖,而异步意味着双方相互独立、各行其是。希望本篇能对大家理解这两个重要的概念有所帮助。