阻塞从何而来?阻塞和异步回调的实现

阻塞和非阻塞、同步和异步,这些是在编程学习中常常会遇上的一个概念,本文假设你对它们已经有了基础的了解。同时,也假设下面的叙述操作中,我们写的代码不包含多线程内容,即程序当下只有一条主线程。

调用一个socket.accept会阻塞当前线程、游戏引擎通过回调实现异步加载资源,等等操作我们似乎不难理解它们的使用。高大上的概念也就一开始唬唬人嘛!但仔细一想,我们在使用它们时被告知:这是个阻塞调用,请小心使用、这是个异步操作,请提供回调操作——它们的使用方法就像是天经地义一般。那么,是谁规定一个方法是同步/异步的、调用它会发生阻塞/不会发生阻塞,它们又是怎么具体实现的?

由操作系统实现

先抛出答案——阻塞调用和异步操作,最终是由操作系统(以下简称OS)实现。这里讨论的不是多线程中通过锁等机制来阻止另一个线程的阻塞,而是类似单线程调用socekt后,代码就不再往下执行,在调用处等待数据返回的这种阻塞。

如果我们一路追寻那些提供给我们的阻塞方法或是异步方法,可能是一个库提供给我们的,这个库里的方法可能又是另一个库提供的,最终我们会在一个地方停下,由OS提供的接口——系统调用。

阻塞与否是由系统调用决定的。OS会根据调用函数的参数来决定它是阻塞的还是异步的。如果一个调用函数接受一个回调函数作为参数,那么这个调用就是异步的,因为当操作完成后,OS会调用这个回调函数来通知结果。如果一个调用函数不接受回调函数作为参数,那么这个调用就是阻塞的,因为当操作完成后,OS会返回一个结果,而不是调用回调函数。

调用一个阻塞的系统调用会发生什么?

阻塞意味着等待,通常是涉及到IO操作,等待外设的输入、文件的加载读取、等待网络数据等等。我们可以简单地描述一个阻塞调用:调用一个阻塞方法后,方法不会立刻返回,期望的数据需要等待多久,方法就等待多久。

而OS实现这个“方法等待”的方式,是挂起线程。我们知道OS管理调度所有的系统/用户线程,当进行一个系统调用时,OS会从“用户态”进入“内核态”,由OS持有CPU资源以运行系统线程。当调用一个阻塞的系统调用时(通常是IO请求,请求处理完毕时,会通过名为中断的机制通知OS内核数据已准备好/操作已完成),OS会将调用它的线程挂起,该线程不再参与竞争CPU资源,线程处于挂起状态时永远也不会得到执行。这就能解释为什么我们的程序“停下来等待”,因为它根本就不执行了。当线程请求的数据到达或是阻塞操作完毕,OS才会将挂起线程的挂起状态取消,线程再次执行时,请求的阻塞数据/操作已到达,我们的代码可以愉快的继续执行下去了。

根据底层OS的不同,上面的这些操作在具体实现细节上也大有不同。究其“阻塞”线程的原因,就是CPU太快了。CPU处理指令的速度是非常快的,而IO设备/IO操作的速度则慢得多。当CPU处理一个IO请求时,需要等待的时间对CPU来说漫长的可怕。为了充分利用CPU,OS的设计实现就是将等待的线程挂起,转而处理其他不需要等待的操作,当等待的目标达成,再回过头来执行原先等待的线程。

异步操作呢?

同样也可以简单地描述一个异步调用操作:调用一个异步方法后,方法立刻返回,等期望的数据到达后再通知程序处理。这里的通知操作,通常是通过异步回调实现的。

使用异步时,程序直观的感受就是,调用一个异步操作,传入处理的回调函数,然后继续执行下面的代码。思考异步实现的原理时,很容易进入一个误区:在OS或是解释器(如果使用的语言是解释执行的话),总归有个地方要生成一个线程来被阻塞,从而实现主线程继续执行吧?

事实上,异步操作并不需要线程实现。

再次考虑一个IO请求,请求的数据需要等待一定时间才能得到,在阻塞调用的情况下,主线程会被挂起无法往下执行,但这次我们使用OS提供的异步调用接口,意味着我们进行了一个异步调用。OS提供的异步调用需要主线程代码提供一个回调作为参数,触发异步调用时,OS将记录这个回调和请求,处理请求相关的IO操作,然后return,且并不会将主线程挂起。在漫长的IO等待时间中,主线程的代码可以从异步调用处继续往下执行。当IO请求操作完毕,OS通过触发记录的回调,通知主线程IO请求完毕,可以正式处理数据了。

听起来这个流程和我们平时调用异步方法时的流程差不多啊。OS和硬件驱动也是程序,这不是套娃吗,异步在OS层面又是怎么实现的?

答案是上面也提到过的OS的中断机制。中断是指打断CPU目前执行的程序,转而执行中断处理程序的一种机制,它的实现依赖于计算机底层架构和硬件的实现。通过中断机制,OS得以实现异步,而不需要通过轮询去不断检查某个硬件的状态。

那驱动呢?作为和IO设备打交道的程序,其中一定充满了需要等待的操作吧。事实上,OS通过IO请求数据包(IRP)向驱动发出命令,驱动在处理IRP时,不允许阻塞操作,所有不能立刻完成的请求,必须以异步方式运行。这样就避免了OS调用驱动导致自身被阻塞(这里的阻塞并不是指线程挂起,因为线程调度管理是OS的工作。阻塞是指因为轮询等操作一直占用CPU,OS得不到CPU资源执行)。当请求处理完毕,通过中断,OS就知道哪些请求已完成。赞美中断。

总结

线程进行阻塞调用时,OS将其挂起以暂停执行,调用完成后再继续执行。线程进行异步操作时,OS立刻返回,线程继续往下执行,调用完成后通过回调通知线程处理。中断机制保证了OS不会被调用阻塞,可以异步的处理和响应IO请求。

参考资料: https://softwareengineering.stackexchange.com/questions/177235/how-are-blocking-calls-implemented https://stackoverflow.com/questions/50532319/where-do-blocking-i-o-comes-from https://www.zhihu.com/question/391359472 https://blog.stephencleary.com/2013/11/there-is-no-thread.html 部分链接可能需要科学
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值