发布于

理解阻塞 / 非阻塞、同步 / 异步

作者

阻塞 / 非阻塞,同步 / 异步这些名词经常被泛用于各类概念,比如接口的调用风格、程序的执行流、进程 / 线程之间的关系等。不同编程背景的人在不同的语境下对这些概念有着不同的理解,在实际交流中比较容易鸡同鸭讲,有必要理清这些概念在不同语境下的确切含义。

阻塞与非阻塞

阻塞与非阻塞经常在 I/O 相关的上下文中(网络 I/O,文件 I/O 等)被提及,主要用于描述“一个 I/O 接口的调用是否会阻塞调用该接口的线程”。比如 read() 系统调用在默认条件下会阻塞当前调用线程,直到内核将数据准备好并从内核缓冲区拷贝到用户缓冲区为止。而如果我们通过 fcntl() 修改所读取文件的描述符为非阻塞模式,那么 read() 则会变为非阻塞调用:如果当前没有数据可读,则直接返回当前不可读事件,由用户程序自行决定是否继续重试或放弃读取。select()poll()epoll_wait() 等在默认情况下也是阻塞的,但我们可以通过设置超时时间的方式来实现非阻塞风格的调用。

在并发编程的上下文中,我们也经常用阻塞与非阻塞描述某个线程的当前状态:阻塞状态表示线程在等待某些必要条件的到位(锁、信号量、同步信号等),在此之前无法继续执行后续逻辑。而非阻塞状态则表示线程目前可以正常执行预定的程序流程。

同步与异步

同步与异步是相对的概念,同步主要用来描述任务流的顺序执行,而异步则用于描述任务流的乱序执行。在 I/O 相关的上下文中,同步与异步经常和阻塞与非阻塞一同提起,例如“同步阻塞 I/O”、“同步非阻塞 I/O”等。在这类语境下,同步主要指“同步 I/O”,其主要包括阻塞 I/O,非阻塞 I/O 和基于非阻塞 I/O 的多路复用,而此时异步则主要指异步 I/O。

同步 I/O 的“同步”主要体现在最终 read() 获取数据的过程是一个同步等待的过程(等待数据从内核缓冲区拷贝到用户缓冲区)。异步 I/O 的“异步”主要体现在“检查是否当前有数据可读”与“将数据从内核缓冲区复制到用户缓冲区”这两个过程都不需要调用线程去等待,而是由后台线程完成并通过回调函数执行对应处理逻辑。由于 I/O 操作委托给了后台线程执行,因而不会阻塞当前线程的程序流,自然不存在所谓“异步非阻塞”的说法。

同样的,在并发编程的上下文中,我们可以通过阻塞的方式来实现同步,比如利用锁机制和信号量来(ReentrantLockCountDownLatchCyclicBarrier 等)同步多个任务流。当然,我们也可以通过非阻塞的方式来实现异步操作,比如将某个任务委托各其它对象(协程、线程池等)去执行并在最后通过信号或回调函数等方式获取任务执行结果。