从java角度对比nodejs、fastapi,同步和异步区别
我之前一直用java语言编程,最近一年用python fastapi和nodejs nestjs开发了一些项目,站在java程序员的角度谈谈异步编程和同步编程的区别,主要在两方面
- 处理请求,java常用的tomcat是多线程处理请求并执行代码,同步阻塞型。而nodejs是单线程处理请求并执行代码,异步非阻塞。
- 编程思想,同步利用多线程完成非阻塞目的,写代码按顺序写。而异步天生能完成非阻塞,比同步多了回调、async await等,写代码也不是顺序、没那么容易理解。
参考这两章节重要
Node.js 中文网 — Node.js 事件循环
Node.js 中文网 — 不要阻塞事件循环(或工作池)
一。同步编程
习惯了java开发,会觉得同步编程易于理解,符合常见顺序思维。
同步tomcat如何处理请求
我们知道tomcat默认用150个线程,每个线程处理一个客户端请求,堆积的客户端请求会进入队列。某个客户端处理请求的线程被io或大量计算堵塞,不影响tomcat的剩余149处理请求,所以tomcat依然能处理其他客户端的请求。
同步编程该如何完成非阻塞需求
在主线程执行期间,如果想同时执行另一个任务,就新建线程去做,并通过callable get方法、线程阀、中断等办法得知线程执行结果。
以下面代码为例,我们想让第二件IO事和第三件IO事同时进行、不想主线程被第二件IO事堵塞住,所以用新线程执行第二件IO事,但想获得第二件事结果要在主线程堵塞等待get。注意在编程中,第二件事应该是阻塞型代码比如IO、长时间计算型任务利用空闲的cpu执行,否则新建线程无意义。
System.out.println("第一件事");
FutureTask futureTask = new FutureTask<>((Callable) () -> {
System.out.println("第二件事IO");
return 1;
});
Thread thread = new Thread(futureTask);
thread.start();
System.out.println("第三件事IO");
Object o = futureTask.get();
System.out.println("第二三件事IO都完成了,第二件事结果"+o);
二。异步编程
nodejs
nodejs的思想是用较少的线程处理更多的客户端请求。
nodejs如何处理请求和io
nodejs有两种线程,事件循环线程和工作池线程,事件循环线程是单线程,工作池线程有多个。
事件循环线程用来干嘛?看看官网文档,如下Node.js 中文网 — 不要阻塞事件循环(或工作池)
我认为nodejs中文网翻译的不对,估计是工具翻译的,还是自己看英文。
第一段话我反复看了好几次 1.nodejs初始化模块和注册事件回调,然后执行事件循环,nodejs通过回调的方式响应处理客户端请求,这个处理客户端请求的回调是以同步方式执行的,并且可能注册异步请求继续处理。(这段话意思是事件循环线程用来处理客户端请求,处理方式是注册事件和回调,在处理客户端请求回调的过程中,它可能注册新的异步事件,我这里翻译为事件,但原文用的是may register asynchronous requests ,requests不是指客户端http请求、而是异步操作请求。比如平常@get等接口接受请求、并在接口中写新的异步io操作就会注册到事件里)
2.第二段又重复说时事件循环会处理在回调中发起的异步非阻塞请求,比如网络io,我理解是进一步解释了
3.总结,事件循环线程干了两件事 (1)处理事件回调 ,这里包含了处理客户端请求,因为它也是通过回调处理;同时回调中如果注册了新事件,新事件同样由事件循环线程处理 (2)处理非阻塞异步事件如网络IO,为什么要强调网络io呢?看了下面工作线程的作用就明白了,因为文件io是工作线程处理的。
举个例子再理解下,平常写代码@get接口,里面打一行日志、然后查数据库、有查询结果后处理。事件循环线程处理请求,然后用回调执行代码,回调里打一行日志,查数据库是网络io,于是事件循环线程发起了这次网络io,但不等待结果,有结果后回调继续在事件循环中完成。
工作池线程用来干嘛的?当碰到一下api时,会自动使用工作池线程执行
-
I/O 密集型
-
DNS:
dns.lookup()
,dns.lookupService()
. -
文件系统:除
fs.FSWatcher()
之外的所有文件系统 API 以及明确同步的 API 都使用 libuv 的线程池。
-
-
CPU 密集型
-
加密:
crypto.pbkdf2()
,crypto.scrypt()
,crypto.randomBytes()
,crypto.randomFill()
,crypto.generateKeyPair()
. -
Zlib:除了明确同步的 zlib API 之外,所有 zlib API 都使用 libuv 的线程池。
-
到这里就明白了,nodejs用事件循环处理请求,处理请求的回调中有异步操作会注册为新事件、网络io则继续由事件循环线程处理,一旦有文件io或加密等规定api会自动由工作线程处理。
所以nodejs官网一再强调,不要阻塞事件循环,这是开发者干的事。详细阅读文档,它说了哪些操作容易堵塞、如何避免堵塞客户端请求,分区卸载等Node.js 中文网 — 不要阻塞事件循环(或工作池)
nodejs处理事件循环的过程
Node.js 中文网 — Node.js 事件循环
在文档这章里讲各个事件循环的各阶段
nodejs完成非阻塞需求代码
以下代码完成了上面java代码相同的事,第二件事用异步执行,想获取第二件事的结果用await。 这里与tomcat不同的是,执行线程只有1个,await并没有阻塞到计算线程,计算线程可以继续执行其他不阻塞的代码,直到等待事件回调。
async function a(){
console.log("第一件事")
const promise = b();
console.log("第三件事")
const result = await promise;
console.log("第二件事结果"+ result);
}
async function b(){
console.log("第二件事");
return 1;
}
a();
关于nodejs执行宏任务微任务
以上的代码里,先打印第二件事还是第三件事,这与宏任务微任务有关,也不难,可以参考
node【一文搞懂:浏览器和node的事件循环机制】【微任务和宏任务】_node事件循环-CSDN博客
当然用异步不是为了打印日志,这是没意义的,异步是为了io
await怎么实现的
await是个promise语法糖,await 一个promise,和promise.then()差不多。虽然await
看起来像是一种特殊的操作,但本质上它也是基于Promise
机制的。
当await
一个Promise
时,实际上是等待这个Promise
的状态转换。如果Promise
的状态还处于pending
,那么与这个Promise
最终状态转换相关的回调(例如resolve
或reject
后的回调)就会在微任务队列中处于等待状态。在事件循环中,微任务队列会在宏任务之间被检查和执行。一旦Promise
的状态发生改变,微任务队列中的相应回调就会被触发,从而使得await
后的代码能够继续执行(如果Promise
被resolve
)或者抛出异常(如果Promise
被reject
)。
python fastapi
fastapi的异步处理请求过程跟node一样,只是fastapi同时支持异步和同步,当接口方法加了async时,走异步处理流程,当不加async时请求由线程池进行处理、跟tomcat类似!
参考这篇文章python - uvicorn 如何调节线程池大小 - SegmentFault 思否
这篇文章也很好的体现了fastapi异步和同步编程的区别FastAPI到底用不用async?_fastapi async-CSDN博客