同步与异步、阻塞与非阻塞

四个概念

  • 同步(Synchronous)
  • 异步(Asynchronous)
  • 阻塞(Blocking)
  • 非阻塞(Non-blocking)

从很多的博文中,我发现很多人认为同步等同于阻塞,异步等同于非阻塞,其实这种理解是不对的。

一  理解同步和异步

概念:同步、异步关注的是消息通信机制(synchronous communication/ asynchronous communication)。

同步、异步分为两种情况:多线程/多进程并发的同步异步 和 I/O操作的同步异步

1.1 并发的同步异步

并发的同步异步解决的问题就是多进程/多线程间的消息通信问题。

同步:当调用者A发起一个调用B,在没得到结果之前,该调用B就不返回,这样导致的结果是调用者A需要一直等待,直到被调用者B返回处理结果,才能继续后续的操作。

例如,B/S模式中,客户端提交请求—>服务端处理请求—>任务完毕返回结果。整个处理期间,客户端不能做其他任何事。

异步:当调用者A发起一个调用B,被调用者B直接返回,但是此时并没有返回操作处理结果(比如说IO读写操作),等操作任务处理完成后,通过状态改变、通知或者回调函数等方式主动通知调用者A,也就是说调用者A被动接受处理结果。

例如,对于上面的例子,客户端提交请求后,不需要等待服务端返回处理结果而是继续处理其他的流程,不需要去关心服务端处理请求的过程,当服务端处理完成后,会通知客户端处理结果。B/S模式的 ajax 请求就是异步请求过程。

这个通知机制可以有以下三种方式:

状态

即监听被调用者的状态(轮询),调用者需要每隔一定时间检查一次,效率会很低。

通知

当被调用者执行完成后,发出通知告知调用者,无需消耗太多性能。

回调

与通知类似,当被调用者执行完成后,会调用调用者提供的回调函数。

  • 并发的同步和异步的区别

同步和异步的区别:请求发出后,是否需要等待处理结果,才能继续执行其他的操作。对于同步来说,调用者主动等待调用结果;对于异步来说,调用者是被动等待调用结果。

二、I/O操作的同步异步

对于I/O操作来说,会涉及到操作系统的内核(kernel)系统调用。因为I/O操作都是通过调用内核提供的系统调用来完成的。

当进行I/O读写操作时,涉及到内核空间和用户空间。内核空间对应的是内核态,用户空间对应的是用户态。

I/O读写操作,会分成下面两个阶段:

  • 1 等待数据准备 (Waiting for the data to be ready)
  • 2 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

等待数据准备的理解:第1个阶段-等待数据准备 (Waiting for the data to be ready) 这个阶段做的事情是指将接收到的数据先存放到内核空间中的缓冲区当中。因为内核空间和用户空间是两个独立的内存空间,当然缓冲区大小是有水位限制的,当内核缓冲区满了,我们就认为数据准备已经就绪了,然后就开始第二阶段了,即将数据从内核拷贝到进程中,具体来说就是将内核缓冲区中的数据复制到进程中的用户缓冲区中。

读就绪:文件描述符的内核接收缓冲区的数据字节数大于等于其低水位标记的当前大小。

写就绪:文件描述符的内核发送缓冲区的可用空间字节数大于等于其低水位标记的当前大小。

低水位标记都是由应用程序制定。例如,应用程序指定接收低水位为64个字节,则接受缓冲区有64个字节才算 fd 读就绪。

  • 我们以收发数据的 recv & send 函数为例对同步I/O进行说明

调用 send 函数时,完成数据传输后才能从函数返回(确切地说,只有把数据完全传输到输出缓冲后才能返回);而调用 recv 函数时,只有读到期望大小的数据后才能返回,这就是同步I/O的处理方式。

究竟是哪些部分是同步的呢?

同步的关键是函数的调用及返回时刻,以及数据传输的开始和完成时刻。

  • 调用send函数的瞬间开始传输数据,send函数执行完(返回)的时刻完成数据传输。
  • 调用recv函数的瞬间开始接收数据,recv函数执行完(返回)的时刻完成数据接收。 

可以通过下图 1 解释上述两句话的含义(上述语句和图中的 “完成传输” 都是指数据完全传输到输出缓冲(即接收缓冲))。从图1中可以看到,同步I/O 是指 I/O 函数的返回时刻与数据的收发时刻是一致的

同步与异步、阻塞与非阻塞_第1张图片 图1  调用同步的I/O函数

那异步I/O的含义又是什么呢?下图 2 给出了解释,可以与图1进行对比。

同步与异步、阻塞与非阻塞_第2张图片 图2  调用异步I/O函数

从上图 2 中可以看到,异步 I/O 是指 I/O 函数的返回时刻与数据收发的完成时刻不一致

  • 同步I/O的缺点及异步方式的解决方案

        从上图1中可以很容易找到同步I/O的缺点:“进行I/O的过程中函数无法返回,所以导致调用该函数的进程或线程不能执行其他任务!”而在图2中,无论数据是否完成交换过程都返回函数,这就意味着可以进程或线程可以继续执行其他任务。所以说,“异步方式能够比同步方式更有效地使用CPU资源”。

同步I/O操作:I/O请求操作会导致请求进程阻塞,直到I/O操作完成。I/O模型有:阻塞I/O、非阻塞I/O、I/O复用、信号驱动I/O。

异步I/O操作:I/O请求操作不会导致请求进程阻塞。I/O模型有:异步I/O。

知识补充】 Linux下的异步IO系统调用使用的是AIO_开头的API函数,如read()对应的异步函数是aio_read()函数。

  • I/O的同步和异步的区别

I/O同步:进程/线程必须等到I/O操作完成,才能继续往下执行,在此期间,进程/线程处于阻塞状态。

I/O异步:I/O操作不会导致进程/线程阻塞,进程/线程将整个I/O操作交给内核来完成,进程/线程可以继续执行其他的操作。等到I/O操作完成后,内核(kernel)会通知对应的进程/线程我的I/O操作已完成。通知的方式可以是:设置一个用户空间特殊的变量值 或者 触发一个 signal 或者 产生一个软中断 或者 调用应用程序的回调函数。

阻塞、非阻塞

阻塞和非阻塞强调的是进程/线程在等待调用结果(消息、返回值)时的状态。

阻塞:在调用结果返回之间,当前进程/线程会被挂起,不能继续往下执行,被调函数只有在得到结果之后才会返回。这点和同步函数是一样的。阻塞模式会导致进程/线程挂起。

<说明> 进程/线程被挂起,指的是进程/线程进入阻塞状态(blocking),在这个状态下,CPU不会给该线程分配时间片,即线程暂停运行,不会占用CPU资源了。

<备注> 可以去了解下进程/线程的生命周期中的5种状态:创建态、就绪态、运行态、阻塞态、死亡态。

阻塞与同步的区别:对于同步来说,当前线程可能仍然是处于激活状态,只是由于调用没有返回结果而已,CPU仍然会给当前线程分配时间片,但是线程在同步等待时什么都干不了,白白占用着CPU的时间片资源,这就造成了CPU资源的浪费。

对于函数调用来说,A调用B,A被挂起直到B返回结果给A,A继续执行。调用结果返回前,当前进程/线程挂起无法处理其他任务,一直等待调用结果返回。

例如,在网络编程中的connect、accept、recv、recvfrom、write、send函数,都是阻塞式的函数,当进程/线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回,具体表现就是程序一动不动,毫无反应了。

非阻塞:在发起一个调用后,无论结果怎样(有结果就返回结果,没有结果就返回错误。总之,它不会阻塞当前线程,而是会立刻返回)。但是要每隔一段时间来check一下之前的操作是否完成。其实,这样的过程就叫做轮询(poll)。非阻塞模式不会导致进程/线程挂起。

对于函数调用来说,A调用B,B会立刻返回一个结果给A,A不会被阻塞,A可以继续执行其他操作。调用结果返回前,当前进程/线程不挂起, 可以去处理其他任务。

举例:以打电话为例分析。

阻塞方式(block):你拨通某人的电话,但是此人不在,于是你拿着电话等他回来,其间不能再用电话。

非阻塞方式(nonblock):你拨通某人的电话,但是此人不在,于是你挂断电话,待会儿再打。至于到时候他回来没有,只有打了电话才知道。即所谓的“轮询(poll)”。

异步是指,你拨通某人的电话,但是此人不在,于是你叫接电话的人告诉那人(leave a message),回来后给你打电话(call back)。

函数调用的阻塞方式与非阻塞方式

阻塞方式:默认情况下,read & write 函数为阻塞方式,如果将参数 flag 设置为 0,recv/send 函数也采用阻塞方式,即如果没有数据可操作,则该进程/线程将被阻塞,当有数据时才会继续执行并返回。

非阻塞方式:如果没有数据可接收就立即返回 -1,表示接收失败,并修改系统全局变量 errno 的值为 EAGAIN,表示数据未准备好。errno 是 Linux 系统下保存当前错误状态值的一个公共变量,如果当前系统调用出错,则会设置 errno 为相应的值并告诉用户进程错误编号和原因,可以用以下代码打印错误信息:

printf("%d %s\n", errno, strerror(errno));

perror("string");

        在 socket 文件描述符读写时,以非阻塞方式调用 recv() 函数返回时如果没有数据可读,函数返回值为 -1,并将修改 errno 变量的值为 EAGAIN,表示数据为准备好。

  • 设置以非阻塞方式处理 socket 文件描述符数据有以下方法。

(1)使用 recv() 函数接收数据时使用 MSG_DONTWAIT 标志,这将使某个单次接收操作为非阻塞方式,如下例所示:

recv(sockfd, buf, BUF_SIZE, MSG_DONTWAIT);

(2)如果设置 socket 文件描述符的属性为非阻塞,这将导致后续所有针对该文件描述符的操作(如 read、write、send、recv)都为非阻塞,设置函数可以选用 setsockoptfcntl 以及 ioctl 函数,如下例所示:

fcntl(sockfd, F_SETFL,O_NONBLOCK);

同步、异步 与 阻塞、非阻塞是两种不同的概念

同步异步指的是通信模式,即被调用者结果返回时通知进程/线程的一种通知机制。

阻塞和非阻塞指的是调用结果返回前进程/线程的状态。

下图是同步、异步、阻塞和非阻塞的概念区别

参考

同步、异步、阻塞与非阻塞的理解与使用场景

同步/异步,阻塞/非阻塞概念深度解析

同步和异步、阻塞和非阻塞,以及五种I/O模型

你可能感兴趣的:(#,并发编程,并发编程,阻塞与非阻塞,同步与异步)