同步与异步、阻塞与非阻塞、同步调用和异步调用的概念

同步与异步

首先我们要明确的是,同步和异步都是针对两个或者两个以上的事物来说的。比如当我们在网上购物看中一件物品,然后去浏览该商品详情的时候,首先页面会先发送一个请求,后台服务器查询对应商品的相关数据,然后前端详情页面才根据返回数据展示该商品的详细信息。而此时你的网速比较差,一个详情页面等了将近一分钟才全部展示完成,这时候你问这个请求是同步还是异步?答案显然是同步请求,它给我们最直观的表现形式就是页面一直显示在加载中,商品的详情页面渲染必须要等待后台服务器返回商品详情数据后才能进行。也就是说下一个操作必须要等待上一个操作完成才能进行,它依赖于上一个操作的返回结果。

你可能会问,在同步的情况下,当一个事物正在进行操作的时候,其它的事物此时在干嘛呢?这个实际上并没有明确的规定,其实同步更多的是关注事物一个一个的串行执行的过程,保证不会交叉执行,至于某个时刻处于什么状态并不关心。这在计算机中大部分时候其它事物都是处于一个等待的状态,而我们人则要灵活得多,在我们日常生活中常用的同步手段就是排队,比如我们上下班坐地铁进行安检的时候,需要依次排队安检进站乘车,但是你在排队的过程是在看手机、聊天还是什么也不做都可以,安检人员并不会在意你在做什么,这种就是由于安检资源有限导致的同步。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-60PQa7Bc-1637423876603)(https://segmentfault.com/img/remote/1460000022478669)]

对于同步这里有两个点需要注意,一是同步的范围,有时候并不需要全局的大范围的去同步,只需要在特定的操作同步即可,这样可以提升执行效率,比如 Java 语言中的同步方法和同步代码块。另一个是同步的粒度,并不是在一些大的操作粒度上才需要同步操作,小的粒度操作也需要同步操作,只是有的小粒度操作天然就已经是同步操作,并不需要我们人为的去添加同步操作控制。比如 Java 语言中的同步都是针对有两个或者两个以上线程的程序来说的,因为单线程的程序里它天然就是同步的。
异步则完全相反,在异步情况下多个事务可以同时进行,互不影响,你进行你的,我进行我的,谁都不用关心谁。总的来说就是:

  • 同步 两个事物相互依赖,并且一个事物必须以依赖于另一事物的执行结果。比如在事物 A->B 事件模型中,你需要先完成事物 A 才能执行事物 B。也就是说,同步调用在被调用者未处理完请求之前,调用不返回,调用者会一直等待结果的返回。
  • 异步 两个事物完全独立,一个事物的执行不需要等待另外一个事物的执行。也就是说,异步调用可以返回结果不需要等待结果返回,当结果返回的时候通过回调函数或者其他方式带着调用结果再做相关事情。

可以看出同步与异步是从行为角度描述事物的,你品,你细品。(PS:这里的多个事务可以指代不同的操作、不同的方法或者不同的代码语句等等

阻塞与非阻塞

所谓阻塞,简单来说就是发出一个请求不能立刻返回响应,要等所有的逻辑全处理完才能返回响应。非阻塞反之,发出一个请求立刻返回应答,不用等处理完所有逻辑。阻塞与非阻塞指的是单个线程内遇到同步等待时,是否在原地不做任何操作。
堵车就是阻塞与非阻塞最好的例子,在一线城市生活过的朋友应该都有体会,在交通正常的时候汽车可以正常通行,就是非阻塞,上下班高峰的时候经常发生堵车,交通正常的时候半个小时车程,高峰期可能需要二、三个小时才能到。。。而且一旦发生交通堵塞,所有马路上的车子都一动不动,只能在车子里等待,就是阻塞,当然大多数人不会选择干等,他们会玩手机或者和朋友聊天等等,同样的在计算机里,阻塞就意味着停止执行停下来等待,非阻塞表明操作可以继续向下执行,但是在发生阻塞的时候计算机可就没有像人这么灵活了,通常计算机的处理方式就是挂起当前线程,然后干等着,阻塞结束后才继续执行该线程。可以看出阻塞和非阻塞描述的当前事物的状态(等待调用结果时的状态)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vysvLOAu-1637423876605)(https://segmentfault.com/img/remote/1460000022478670)]

结合前面介绍的同步与异步,两两组合就会有四种情况,分别是同步阻塞同步非阻塞异步阻塞异步非阻塞。下面通过车道的例子来形象的解释这几种状态:

  • 同步阻塞 只有一个车道,不能超车,所有车子依次行使,一次只能通过一辆车,尴尬的是这个车道还堵车了。
  • 同步非阻塞 只有一个车道,不能超车,所有车子依次行使,一次只能通过一辆车,不过比较幸运这个车道没有堵车,可以正常通行。
  • 异步阻塞 有两个或两个以上车道,每条马路都可以通行,不同车道上的车子可以并行行使,尴尬的是所有的车道都堵车了。
  • 异步非阻塞 有两个或两个以上车道,每条马路都可以通行,不同车道上的车子可以并行行使,不过比较幸运的是没有一个车道堵车,都可以正常通行。

对应到我们计算机里也是一样的,同步阻塞相当于只有一个线程,而且该线程处于阻塞(Blocked)状态,同步非阻塞相当于只有一个线程,而且该线程处于运行(Running)状态。异步阻塞相当于有多个线程,而且所有线程都处于阻塞(Blocked)状态,异步非阻塞相当于有多个线程,而且所有线程都在正常运行。

同步调用和异步调用

同步和异步主要用于修饰方法。当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法;当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务,调用者不用等待该方法执行完毕,我们称这个方法为异步方法。异步的好处在于非阻塞(调用线程不会暂停执行去等待子线程完成),因此我们把一些不需要立即使用结果、较耗时的任务设为异步执行,可以提高程序的运行效率。

同步调用是以一种阻塞式调用

比如说:古代的长城的烽火传递信息,现在我们假设每个烽火只能看到相邻的烽火状态,每个烽火的状态只有亮和暗。

现在有A、B、C、D四个烽火,A首先点亮,B看到A的烽火亮了,立马去点火,花了2秒点亮。但是这时候负责C烽火的人在睡觉,可是这时候所有人都在等待C点亮,终于C睡了2个小时候看到了B点亮,然后去点亮。D由于长期没有点亮,导致烽火出现问题,因此整个过程都在等待D的完成。这种就是典型的阻塞机制,无论如何我们只能等待上一个任务的完成,如果没有完成我们只能继续等待,这样造成的问题是,我们一直在浪费系统资源。

异步调用

异步调用是一种类似消息或事件的机制,不过它的调用方向刚好相反,接口的服务在收到某种讯息或发生某种事件时,会主动通知客户方(即调用客户方的接口)。

依然是上面的例子:

现在我们有一个将臣F,他专门负责告诉每个烽火需要去点亮

A、B、C、D四个烽火,将臣先告诉了A,然后不等A点亮,他继续告诉了B、C和D。最后A在2小时后告诉了F我完成了,B在1小时后告诉了F我完成了,C在1.5小时后告诉F完成了,而D在3小时后告诉F完成了。F收到这些信息后,才知道整个过程完成了。

socket缓冲区以及阻塞模式详解

以socket这边的情况来理解一下阻塞模式。

socket缓冲区

每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。

write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。

TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。

read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。

TCP套接字的I/O缓冲区示意图
图:TCP套接字的I/O缓冲区示意图

这些I/O缓冲区特性可整理如下:

  • I/O缓冲区在每个TCP套接字中单独存在;
  • I/O缓冲区在创建套接字时自动生成;
  • 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
  • 关闭套接字将丢失输入缓冲区中的数据。

输入输出缓冲区的默认大小一般都是 8K,可以通过 getsockopt() 函数获取:

unsigned optVal;
int optLen = sizeof(int);
getsockopt(servSock, SOL_SOCKET, SO_SNDBUF, (char*)&optVal, &optLen);
printf("Buffer length: %dn", optVal);

运行结果:
Buffer length: 8192

这里仅给出示例,后面会详细讲解。

阻塞模式

对于TCP套接字(默认情况下),当使用 write()/send() 发送数据时:

  1. 首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据。

  2. 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒。

  3. 如果要写入的数据大于缓冲区的最大长度,那么将分批写入。

  4. 直到所有数据被写入缓冲区 write()/send() 才能返回。

当使用 read()/recv() 读取数据时:

  1. 首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。

  2. 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 read()/recv() 函数再次读取。

  3. 直到读取到数据后 read()/recv() 函数才会返回,否则就一直被阻塞。

这就是TCP套接字的阻塞模式。所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性。

TCP套接字默认情况下是阻塞模式,也是最常用的。当然你也可以更改为非阻塞模式,后续我们会讲解。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>