跳到主要内容位置

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像素,网页就会有看着像牛皮鲜一样的视觉效果。

异步的过程还有很多需要深挖的点,它往往也是一个性能优化聚焦的核心区,今天的文章就简单的谈一下我对异步代码的理解,更加深入的探讨我们后续有机会再见。