R1-异步编程之始
从异步编程开始
#
异步编程之始异步编程是什么?为什么现代程序语言非得要支持异步编程。实际上异步编程已经成为同现代软件开发中不可或缺的重要环节,如果你对异步编程感到些许困惑,也许我的文章能够帮到你。
异步是什么
代码执行过程中,分为同步和异步,通常的代码都为同步代码,也就是上一行没执行完,下一行就不会执行。由于计算机执行的速度很快,即便当前行的代码执行几千万次的遍历,效率也十分惊人。但凡事都有特例,如果说单纯的调用 CPU 的算力去执行一个循环,那本身是不难的,实际上,我们的开发情况复杂的多,例如:我们在遍历过程中读取写入数据库 (I/O)、文件的上传下载、AI 的训练、DOM 元素的操作等等。
上述的几种操作,他是多方面协作才能产生的,不仅是 CPU 在参与,内存、磁盘、显卡算力 等都会参与其中,这其中的交互就会出现多种情况,并且时间上有不确定性。
因此,异步编程应运而生。我们可以把异步代码想象成做饭炒菜,如果说同步代码是油锅中炒菜,那么异步可能算得上煲汤,我在炒菜的过程中,不影响我去烧汤。当我把异步代码构建好之后,打开火,我就只需要等待汤烧好的那时,可能会30分钟烧好,也可能3个小时烧好,我们只需要等待煲汤过程中、食材产生的奇妙化学反应。
现代语言的异步做法和异同点
一个异步编程通常含有以下的几种处理方式
- 回调(Callbacks):通过将回调函数传递给异步操作,当操作完成时调用回调函数
- Promise/Future:表示一个可能在未来某个时间点完成的值或错误。它们提供了一种更方便的方式来处理异步操作的结果。
async
/await
:基于Promise
/Future
的语法糖,使得异步代码看起来像同步代码,从而提高代码的可读性和可维护性。
回调的话,都是必须要具备的,菜做完之后,需要出锅,数据拿到、文件写入之后,我们都需要进行后续处理,这个按下不表。
Promise/Future 处理异步操作的结果,可以理解成处理回调的工具。不同的语言会根据自身的特性,选择其中不同的实现方案。
#
JavaScript 的方式 function fetchData(callback) { setTimeout(() => { callback("Data fetched"); }, 1000); }
fetchData((data) => { console.log(data); });
或者
function fetchData() { return new Promise((resolve) => { setTimeout(() => { resolve("Data fetched"); }, 1000); }); }
fetchData().then((data) => { console.log(data); });
上述是 JavaScript 的代码,第一段只是执行一个异步操作,同时使用回调函数调用。后一段的代码,使用 Promise 方案将其处理,能够使得回调通过 .then 的方式在函数执行完毕之后获得处理,当 .then 出现的时候,都可以理解为他是异步代码执行完之后的操作。
function fetchData() { return new Promise((resolve) => { setTimeout(() => { resolve("Data fetched"); }, 1000); }); }
async function main() { const data = await fetchData(); console.log(data); }
main();
当然,如果异步的操作很多 then 起来就没完没了,JavaScript 提供了async/await 语法糖,使他看起来像同步代码一样,实际上它仍然是异步的,该等还是要等。同时,JavaScript的异步又快又简单,本身并不复杂。
#
Java的方式首先回调都是必要的,区别只是执行回调的方式不同
import java.util.concurrent.*;
public class Main { public static void main(String[] args) { CompletableFuture.supplyAsync(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return "Data fetched"; }).thenAccept(data -> { System.out.println(data); }).join(); } }
上述是 CompletableFuture 的方案,Java 是通过线程池模拟的方式实现的异步操作,在代码阅读层面,可能不如语法糖来的直观
#
Rust的方式Rust本身是不存在异步运行时,只能依赖社区生态(Tokio 和 async-std) 来实现异步编程,同时,他需要 async/await 关键字和Future 结合异步运行时来实现。
use tokio::time::{sleep, Duration};
async fn fetch_data() -> &'static str { sleep(Duration::from_secs(1)).await; "Data fetched" }
#[tokio::main] async fn main() { let data = fetch_data().await; println!("{}", data); }
上述是Rust的代码,同样的也非常的简单直观,但是其背后层面是做了很多我们看不见的工作。
分析和异同
回调函数,我们可以看到,在 三种语言中都会存在,承载 回调函数的方案 Promise/Future 有所不同。
- Java是在执行 Future的过程中可以取消,由于他具备多线程和线程池,在任务调度的过程中,具有一定的优势,这个特点使得它的并发能力很强。
- JavaScript 是单线程,它的本质是相当于单线程的事件循环,该异步模型有自身的一套运行机制,由于是单线程且是事件循环做的,它一旦执行,便不能取消。相当于一条道走到黑,不能关火(除非出错,也就是菜糊了)
- Rust 不具备内置运行时,只有编译时,编译是这门语言的核心特点,它需要第三方的库来支持。使得它在执行过程中能展开异步任务。
同样的,async和await都是成对出现,单有其中一个关键字在函数方法中,是不会生效的。await加在哪里,通常是网络请求、I/O、文件读写等相关操作。
如果不加,会怎样?
最直观的缺陷,就是一个字词 要等,文件没上传完,就得等,要等这个大文件全部上传完,才会执行下一步操作,事想下,大量的客户排队等着上传文件,这个画面太美我不敢看;DOM操作,html元素的修改,比如改个颜色、宽高等属性,如果不是异步一次性收集好更改,网页就会变成:元素A先变蓝色,接着元素B又撑高了200像素,网页就会有看着像牛皮鲜一样的视觉效果。
异步的过程还有很多需要深挖的点,它往往也是一个性能优化聚焦的核心区,今天的文章就简单的谈一下我对异步代码的理解,更加深入的探讨我们后续有机会再见。