一.基本概念

异步函数通过非阻塞的方式来处理并发任务,使程序在等待某些操作完成时继续处理其他任务,从而提高应用程序的性能和响应速度

1.1 阻塞

传统的编程模型或者说语言模式通常是同步的,例如基础的C语言Java在高校期间的变成开发,这意味着每个操作都是顺序执行的,调用一个操作时程序将会等待这个操作完成后才会去继续执行后后续的任务。尤其是:文件读取、网络请求等操作,如果遇到I/O操作,程序将会发生阻塞,直到执行操作完成后才会继续执行。

异步编程的核心思想则是:档程序发起了某个可能需要很长时间等待的操作时,他不会阻塞当前线程的执行,而是让该操作在后台进行,当线程可以继续执行其他任务。当操作完成时,程序会受到通知(通知的通常方式为 回调函数 事件 Promise等)

1.2 回调函数与事件驱动

异步函数最常见的完成通知方式就是通过回调函数来处理任务。 当某个异步任务执行完成后,程序会调用预设的回调函数。

Javascript、python、Node.js都大量使用了回调的方式来实现异步

1.3 事件循环与任务队列

在单线程任务的模型中,事件循环是异步编程的核心机制,事件循环是一种调度机制,它不断检查任务队列中是否有等待执行的任务,程序的异步操作会把任务或回调函数放入这个任务队列中,事件循环则会从任务队列中取出任务并执行,保证了该任务执行的顺序和非阻塞性

1.4 Promise与async/await

在回调函数方式的异步编程中,嵌套回调通常会导致代码难以维护和理解:(比如rubbish中的战斗,使用了多个setTimeOut嵌套,每个延迟执行又使用了变量与常量混合的方式进行控制,很多时候根本不知道是程序哪里给设置了状态或变量)

为了解决这个问题,现代语言引入了Promise(承诺)模式。Promise代表了一个可能在未来某个时刻完成的异步操作,通过then()方法可以设置回调,链式调用减少了回调的层级。

async/await是基于承诺模式Promise的语法糖

语法糖,也译为糖衣语法,是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。“语法糖”这个词绝非贬义词,它可以给我带来方便,是一种便捷的写法,编译器会帮我们做转换,可以提高开发编码的效率。

解释:就像是乘法是加法的高级写法,进行的运算结果是一样的,可是写法不同

不足:语法糖有的时候是降低了程序的可读性来换取效率,不管是写程序的效率还是程序运行的效率

语法糖、语法盐、语法糖精、语法咳罗伊

他使得异步代码看起来像同步代码,极大的提升了代码的可读性和可维护性。通过await等待异步操作的完成,程序不会阻塞,但代码结构却保持了同步的形式

什么是ansync和await

定义

  • async 是异步的意思,await则可以理解为 async wait。所以可以理解async就是用来声明一个异步方法,而 await是用来等待异步方法执行
  • async作为一个关键字放在函数前面,表示该函数是一个异步函数,异步函数意味着该函数的执行不会阻塞后面代码的执行;而 await 用于等待一个异步方法执行完成;
  • await 等待一个 Promise 对象,如果 Promise的状态变成了 resolve 或者 rejcet,那么 async函数会恢复执行。并会阻塞该函数内后面的代码。
  • 使用 async/await 可以实现用同步代码的风格来编写异步代码,这是因为 async/await 的基础技术使用了生成器和 Promise,生成器是协程的实现,利用生成器能实现生成器函数的暂停和恢复。
  • 为了优化 .then 链而开发出来的。

二.单线程异步变成-事件循环与多任务协作

2.1单线程异步模型的优势

许多现代编程语言(尤其是Node.js)都采用了的单线程异步的模型,在这种模型下,只有一个主线程负责所有任务的调度和执行,通过非阻塞I/O单线程可以同时处理大量的并发任务,而不需要为每个任务单独创建一个独立的线程,这避免了县城切换和上下文切换的开销。

例如,在Node.js中,所有的I/O操作(如文件读取、数据库查询、HTTP请求等)都以异步方式执行。当一个操作被发起时,事件循环会将它交给底层操作系统或其他专门的线程池处理,而Node.js主线程则可以继续执行其他任务,避免了因为等待I/O操作完成而导致的空闲时间浪费。

2.2事件的循环机制

Node.js的事件循环机制是实现异步编程的关键。它通过以下几个阶段来处理异步任务:

  • Timers:执行在setTimeout或setInterval中注册的回调。
  • I/O callbacks:处理一些常见的系统操作的回调(如TCP连接、文件I/O等)。
  • Idle, prepare:用于系统内部准备操作。
  • Poll:等待I/O事件,执行到期的回调。
  • Check:执行setImmediate注册的回调。
  • Close callbacks:处理close事件的回调(如socket.on(‘close’))。

Promise 的编程模型依然充斥着大量的 then 方法,虽然解决了回调地狱的问题,但是在语义方面依然存在缺陷,代码中充斥着大量的 then 函数,这就是 async/await 出现的原因。async/await 让代码更少,更简洁。

关于ansync

我们先来看看 async 到底是什么?根据 MDN 定义,async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。对 async 函数的理解,这里需要重点关注两个词:异步执行和隐式返回 Promise。 下面我们会通过例子来看如何 隐式返回 Promise。

async的用法,语法很简单,在函数前面加上async关键字,表示函数是异步的。

 async function timeout() {
     return 'hello world!'
 }

async 函数返回的是一个promise 对象,并且Promise还有state和result,如果async函数中有返回值,当调用该函数时,内部会调用Promise.resolve()方法把它转化成一个promise对象作为返回,但如果timeout函数内部抛出错误呢? 那么就会调用Promise.reject() 返回一个promise 对象


那么要想获取到async 函数的执行结果,就要调用promise的then 或 catch 来给它注册回调函数

  async function timeout() {
      return 'hello world!'
    }
    timeout().then(val => {
      console.log(val)
    })
    console.log('我虽然在后面,但是先执行')

我们获取到了”hello world!’, 同时timeout的执行也没有阻塞后面代码的执行,和我们刚才说的一致。

   async function timeout () {
      for (let index = 0; index < 3; index++) {
        console.log('async', +index)
      }
    }
    console.log(timeout())
    console.log('outer')

如果async 函数执行完,返回的promise 没有注册回调函数,比如函数内部做了一次for 循环,你会发现函数的调用,就是执行了函数体,和普通函数没有区别,唯一的区别就是函数执行完会返回一个 promise 对象。

外,async函数返回一个promise对象,下面两种方法是等效的

// 方法1
function f() {return Promise.resolve('TEST');
}// asyncF is equivalent to f!

// 方法2
async function asyncF() {return 'TEST';
}

async 关键字最重要的就是async函数的执行会返回promise对象,并且把内部的值进行promise的封装。如果promise对象通过then或catch方法又注册了回调函数,async函数执行完以后,注册的回调函数就会放到异步队列中,等待执行。

关于await

await关键字只能放到async函数里面,await是等待的意思,那么它等待什么呢?它后面跟着什么呢?其实await不仅仅用于等Promise对象,还可以等任意表达式,所以await后面实际是可以接普通函数调用或者直接量的,不过我们更多的是放一个返回promise 对象的表达式。他等待的是promise对象执行完毕,并返回结果。

//所以下面这个示例完全可以正确运行
    function getSomething () {
      return 'something'
    }
    async function testAsync () {
      return Promise.resolve('hello async')
    }
    async function test () {
      const v1 = await getSomething()
      const v2 = await testAsync()
      console.log(v1, v2)
    }
    test()
  • 如果它等到的是一个Promise对象,await就忙起来了,它会阻塞函数后面的代码,等着Promise对象resolve/reject,然后得到resolve/reject的值,作为await表达式的运算结果。
  • 如果 await 等待的是一个非 Promise 对象,那么V8 会隐式地将该对象包装成一个已经 resolve 的 Promise 对象.

async/await 帮我们干了啥?

做个简单的比较
现在举例,用 setTimeout 模拟耗时的异步操作,先来看看不用 async/await 会怎么写

function takeLongTime () {
      return new Promise(resolve => {
        setTimeout(() =>
          resolve('long_time_value'), 1000
        )
      })
    }
takeLongTime().then(val => {
   console.log(val, 'val')
})

如果改用 async/await 呢,会是这样

    function takeLongTime () {
      return new Promise(resolve => {
        setTimeout(() =>
          resolve('long_time_value'), 1000
        )
      })
    }
     async function test () {
      let v = await takeLongTime()
      console.log(v, 'v')
    }
    test()

眼尖的已经发现 takeLongTime () 没有申明为async。实际上takeLongTime () 本身就返回Promise对象,加不加async结果都一样。

await 优势在于处理 then 链,使代码看起来像同步代码一样

写一个函数,让它返回promise 对象,该函数的作用是2s 之后让数值乘以2

// 2s 之后返回双倍的值
function doubleAfter2seconds (num) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(num * 2)
        }, 2000)
      })
    }

再写一个async 函数,从而可以使用await 关键字, await 后面放置的就是返回promise对象的一个表达式,所以它后面可以写上 doubleAfter2seconds 函数的调用

async function testResult() {
    let result = await doubleAfter2seconds(30);
    console.log(result); //2s后打印60
}
testResult();

代码的执行过程
调用testResult 函数,它里面遇到了await, await 表示等待,代码就暂停到这里,不再向下执行了,它等待后面的promise对象执行完毕,然后拿到promise resolve 的值并进行返回,返回值拿到之后,它继续向下执行。具体到 我们的代码, 遇到await 之后,代码就暂停执行了, 等待doubleAfter2seconds(30) 执行完毕,doubleAfter2seconds(30) 返回的promise 开始执行,2秒 之后,promise resolve 了, 并返回了值为60, 这时await 才拿到返回值60, 然后赋值给result, 暂停结束,代码继续执行,执行 console.log语句。

就这一个函数,我们可能看不出async/await 的作用,如果我们要计算3个数的值,然后把得到的值进行输出呢?

async function testResult() {
    let first = await doubleAfter2seconds(30);
    let second = await doubleAfter2seconds(50);
    let third = await doubleAfter2seconds(30);
    console.log(first + second + third);
}
testResult()

6秒后,控制台输出220, 我们可以看到,写异步代码就像写同步代码一样了,再也没有回调地狱了。

这里强调一下,当js引擎在等待promise.resolve的时候,他并没有真正的暂停工作,它可以处理其他的一些事情,如果我们在testResult函数后面继续执行其他代码,比如console.log一下,会发现console.log代码先执行。

async function testResult() {
    let first = await doubleAfter2seconds(30);
    let second = await doubleAfter2seconds(50);
    let third = await doubleAfter2seconds(30);
    console.log(first + second + third);
}
testResult()
console.log('我先执行!!!')

先输出 “我先执行!!!”,6s后输出计算结果

应用举例

1)当遇到 await 时,会阻塞函数内部处于它后面的代码(而非整段代码),去执行该函数外部的同步代码;当外部的同步代码执行完毕,再回到该函数执行剩余的代码。并且当 await 执行完毕之后,会优先处理微任务队列的代码。

上面的例子中,await 会阻塞它下面的代码(即加入微任务队列),先执行 async 外面的同步代码,同步代码执行完,再回到 async 函数中,再执行之前阻塞的代码。

代码的执行过程:

  • 首先进入async1,打印出 async1 start。
  • 然后遇到async2,进入async2,遇到定时器 timer2,加入宏任务队列,之后打印 async2。
  • 由于 async2阻塞了后面的代码执行,将后面的代码加入微任务队列。所以执行后面的定时器 timer3,将其加入宏任务队列,之后打印同步代码 start。
  • 同步代码执行完毕,先执行微任务队列,打印出 async1 end,遇到定时器 timer1,将其加入宏任务队列。
  • 最后再执行宏任务队列,宏任务队列有3个任务,先后顺序是 timer2,timer3,timer1,分别按顺序执行,任务队列按照先进先出原则执行。

代码的执行过程:

  • 首先代码同步执行,打印出0;
  • 然后将 test() 压入执行栈,打印出100, 下面注意了,遇到了关键角色await;
  • await 200,被 JS 引擎转换成一个 Promise :

let promise = new Promise((resolve,reject) => {
resolve(200);
})

这里调用了 resolve,resolve的任务进入微任务队列。

  • 然后,JS 引擎将暂停当前协程的运行,把线程的执行权交给父协程;
  • 回到父协程中,父协程的第一件事情就是对await返回的Promise调用then, 来监听这个 Promise 的状态改变;

promise.then(value => { // 相关逻辑,在resolve 执行之后来调用 })

  • 然后往下执行,打印出300。
  • 根据EventLoop机制,当前主线程的宏任务完成,现在检查微任务队列, 发现还有一个Promise的resolve,执行,现在父协程在then中传入的回调执行。我们来看看这个回调具体做的是什么。

promise.then(value => { // 1. 将线程的执行权交给test协程 // 2. 把 value 值传递给 test 协程 })

  • 现在执行权到了test协程手上,test 接收到父协程传来的200, 赋值给 a ,然后依次执行后面的语句,打印200、200。

总的来说

Promise 的编程模型依然充斥着大量的 then 方法,虽然解决了回调地狱的问题,但是在语义方面依然存在缺陷,代码中充斥着大量的 then 函数,这就是 async/await 出现的原因。
使用 async/await 可以实现用同步代码的风格来编写异步代码,这是因为 async/await 的基础技术使用了生成器和 Promise,生成器是协程的实现,利用生成器能实现生成器函数的暂停和恢复。
async 函数
1)async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。
2)Promise对象的结果由async函数执行的返回值决定
await 表达式
1)正常情况下,await右侧的表达式一般为 promise对象 , 但也可以是其它的值
2)如果表达式是promise对象,await就忙起来了,它会阻塞函数后面的代码,等着Promise对象resolve/reject,然后得到resolve/reject的值,作为await表达式的运算结果。
3)如果 await 等待的是一个非 Promise 对象,那么V8 会隐式地将该对象包装成一个已经resolve 的 Promise 对象。比如 上面案例 await 200,会默认创建一个 Promise 对象。
在使用await的时候我们只是暂停了函数,而非整段代码。这里经常会是容易犯错的地方。
async和await是非阻塞的
仍然可以使用 Promise,例如Promise.all(p1, p2, p3).,接受一个数组作为参数,p1、p2、p3 都是 Promise 实例,如果不是,就会先调用 Promise .resolve方法,将参数转为 Promise 实例,再进一步处理。只要 p1、p2、p3 之中有一个被 rejected,整个状态就变成 rejected。
创建 Promise 时,并不会生成微任务,而是需要等到 Promise 对象调用 resolve 或者 reject 函数时,才会产生微任务。
产生的微任务并不会立即执行,而是等待当前宏任务快要执行结束时再执行。
注意
1)await必须写在async函数中, 但async函数中可以没有await
2)如果await的promise失败了, 就会抛出异常, 需要通过try…catch来捕获处理

async/await or promise使用时到底选择哪个呢?

大部分复杂情况下async/await 的确是最优解,个人觉得也不是所有情况下都是 async/await 写起来更爽,最不爽的就是他的错误处理,try…catch这个代码看起来就很奇怪(当然也有很多人喜欢这种错误处理方式)。所以我个人的习惯,当只有一个异步请求,且需要做错误处理的情况下,更倾向于使用 promise。比如

// promise
getInfo()
.then(res => {
//do somethings
})
.catch(err => {
//do somethings
})

// async/await
try {
const res = await getInfo()
//do somethings
} catch (error) {
//do somethings
}

在有嵌套请求的情况下,肯定是 async/await 更直观的。

// promise
a(() => {
b(() => {
c()
})
})

// async/await
await a()
await b()
await c()

当然代码写的好与不好还是取决于写代码的人的。
比如一个常见的业务场景:有两个并发的异步请求,在都完成后do something。但很多人会错误的用串行的方式实现了。

//错误
await a()
await b()
//这样变成了 a().then(() => b() )
// a 好了才会执行 b
done()

//正确
await Promise.all([a(), b()])
done()

还有一个小细节async/await打包后的代码其实会比 promise 复杂很多, 当然这个是一个忽略不计得问题。

总结:
我认为它们两个人并不是 or 的关系,在特定的业务场景下,选择相对而言代码可读性更好地解决方案。具体该怎么选择还是需要大家更具自己团队的风格或者自己的理解来判断。

三.多线程与分布式系统中的异步编程

虽然单线程异步模型非常高效,但在处理CPU密集型任务时,它的性能可能会受到限制。此时,多线程和分布式系统提供了更多的选择。

3.1 多线程异步编程
在多线程模型中,任务的并发处理是通过创建多个线程来实现的。每个线程独立执行任务,线程之间通过共享内存或消息传递来协调和同步。

异步编程可以在多线程中与事件驱动结合使用。例如,在Java中,可以通过CompletableFuture来进行异步编程,而底层的任务会在多个线程中并行执行。当任务完成时,可以通过回调通知主线程。

多线程编程虽然能够提高CPU密集型任务的处理能力,但也带来了线程同步、资源竞争等问题。因此,通常在多线程的异步编程中需要考虑线程安全和避免死锁等问题。

3.2 分布式系统中的异步编程
在分布式系统中,异步编程不仅仅是一个性能优化的手段,它还是实现系统高可用性和高并发的必要条件。

分布式系统的异步编程模式通常涉及到消息队列(如RabbitMQ、Kafka)、事件驱动架构、回调机制以及分布式事务等。通过异步消息传递,系统的不同模块可以独立执行而不需要直接同步等待,从而大大减少了各个模块之间的耦合度和响应时间。

例如,当用户请求进入Web服务时,服务可能需要查询多个数据库,调用外部API,进行复杂的数据处理等。如果这些操作是同步执行的,用户的请求就会被长时间阻塞。使用异步编程,系统可以在等待外部资源时,继续处理其他用户请求,从而提高了系统的吞吐量和响应能力。

3.3 持久化与容错机制
在分布式系统中,异步编程还需要考虑持久化和容错问题。由于网络不稳定或服务故障,异步任务可能会失败或中断。因此,异步系统需要设计任务的重试机制、失败回调、超时控制等机制,以确保系统能够在出现问题时自动恢复。

3.4 微服务与事件驱动架构
在微服务架构中,服务之间的通信往往采用异步消息传递的方式。通过消息队列和事件驱动架构,微服务可以解耦合,避免直接的同步调用。每个微服务只需关注自己的任务,完成后发布事件或消息,其他服务可以通过异步的方式进行处理,从而提高了系统的扩展性和容错能力。

四.异步编程的挑战与注意事项

尽管异步编程可以带来巨大的性能提升,但它也存在一些挑战:

  • 回调地狱:在复杂的异步逻辑中,回调函数的层级可能变得非常深,导致代码难以维护。
  • 错误处理:异步操作中错误的捕获和处理比较复杂,需要使用Promise的catch(),或者在async/await中使用try/catch来捕获异常。
  • 资源管理:在异步环境下,资源的管理和释放变得更为复杂。比如数据库连接、网络连接等资源需要确保在异步操作完成后被正确释放。
  • 调试困难:异步代码的执行顺序不确定,调试起来可能更加困难。


4.1 解决方案
Promise与async/await:可以通过Promise和async/await来避免回调地狱,提升代码的可读性。
异步错误处理:通过try/catch捕获异常,确保程序在异步操作失败时能够正确处理错误。
使用异步框架:如Node.js中的Express、Python中的 asyncio等,提供了强大的异步编程支持和工具,简化了异步编程的复杂度。


异步编程是现代计算中实现高效并发的核心技术之一。无论是在单线程的事件循环模型中,还是在多线程和分布式系统中的应用,异步编程都能够显著提高系统的性能和扩展性。理解异步编程的原理,掌握事件循环、协程、消息队列等技术,能够帮助开发者设计出高效、稳定的并发程序。然而,异步编程也伴随着一些挑战,如调试复杂性、错误处理等,需要在实际开发中不断总结经验、使用合适的工具来应对。

参考原文链接1:https://blog.csdn.net/m0_38141444/article/details/144972532

参考原本链接2:https://blog.csdn.net/weixin_45811256/article/details/123638582