async在实际场景中的应用

之前的两篇文章异步编程async和[Rust async原理剖析](https://xx/Rust async原理剖析)中分别介绍了异步编程的概念,以及Rust中async的实现原理,相信大家对async已经有了一定的理解,那今天我们主要从实际场景出发,来体验一下async的真实魅力。

由于异步编程async是提升软件性能和用户体验的一个重要手段,而且目前各个主流语言对async都进行了原生支持,大大降低了开发者的使用门槛,使开发者可以更轻松简单的编写非阻塞代码来解决实际生产中的问题,使其绽放出了耀阳的光芒。

下面就让我们来看下aysnc是如何在实际场景中施展拳脚的。实际场景那么多,那么你如何从理论角度判断某个场景是否适合使用异步编程async呢?俗话说知已知彼,百战不殆,那就先来了解下async的优缺点吧。

async优缺点

优点

  • 高并发
    1. 同一线程可以调度大量并发任务
    2. 线程内非阻塞
  • 资源占比低
    1. 不需要为每个任务创建一个线程或者进程
    2. 线程内并发,不需要线程之间上下文切换
  • 支持事件驱动模型
    1. 天然适合处理websocket、即时通讯和推送系统等事件流式场景
  • 语法简单
    1. 只需在需要异步的语法块添加aysnc关键字即可
    2. 在需要非阻塞等待的地方调用await即可

缺点

  • 不适合CPU密集型场景
    1. 由于是线程内并发调度,所以不适合CPU密集型任务,这类任务容易造成其他任务被饿死的想象
  • 底层高度抽象
    1. 屏蔽了底层的具体实现
  • 不易调试和发现bug
    1. 由于是异步执行,其执行过程不易捕获,造成调试困难

应用场景

高并发的Web应用服务

在现在的Web应用中,往往会有大量的访问请求,这就迫使其应用本身需要处理大量的网络请求,在处理这些请求的过程中可能涉及到本地资源的读取、数据库的访问以及第三方服务的调用,这些操作通常会消耗一定的时间,如果不对其进行优化,会大大降低服务的性能。

我们可以使用异步编程async来优化这些操作,因为这些操作有个普遍的特点,就是都是一些IO操作,则可以通过async/await异步等待IO响应,而无需阻塞当前线程,使其可以继续处理其他请求,从而提升整体服务的性能,增大系统的吞吐量。

典型的应用有:微服务之间接口调用,网关服务,实时数据查询服务和API服务。

下面看一个简单的API服务代码:

from fastapi import FastAPI
import httpx

app = FastAPI()

@app.get("/data")
async def get_data():
    async with httpx.AsyncClient() as client:
        resp = await client.get("https://thirdservice.com/data")
        return resp.json()Code language: JavaScript (javascript)

这里由于使用了async def,所以对每个外部http请求都是并发处理的,并不会阻塞,当遇到await时,便会让出当前线程的执行权限,当前线程即可去执行其他请求,等到thirdservice返回结果之后,当前线程会再次执行此任务,并返回结果。

前端UI交互

上面所说的Web应用服务主要是指后端服务,如果只是后端通过异步编程提升了服务性能,而前端UI依然是同步执行的话,那用户在页面上的操作就会有所卡顿,所以通常也都是进行异步操作。

典型的应用有:点击按钮结果异步返回,图片异步加载。

前端语言中的JavaScript也是通过async/await来实现异步的,代码如下:

async function fetchData(data) {
  const res = await fetch(`/api/data/${data}`);
  const data = await res.json();
  console.log(data);
}Code language: JavaScript (javascript)

物联网IoT设备

IoT设备由于其属性问题,其硬件资源有限,但却要处理大量事件,如传感器数据的采集与发送、接受和执行远程命令以及数据下载,如果这些任务都是同步执行,则资源很快就会被消耗掉,导致有些任务无法正常运行,而async较为轻量而且高效,非常适合这种嵌入式场景。

典型的应用有:传感器数据定期收集,固件远程更新。

爬虫服务

另一个比较适合的场景就是网络爬虫,爬虫通常需要读取大量的网页,并且对其中的内容进行解析加工,而且爬虫对内容的实效性也有一定的要求,这种场景下如果每次请求都是同步阻塞的,肯定无法完成读取数据和解析数据的任务。

而在这种场景下,耗时的部分是发出请求,等待网页响应和数据存储,这些都不是CPU运算任务,是典型的IO密集型任务,所以引入异步编程async,可以让爬虫在等待页面响应时去执行更多的请求,从而大幅提升请求数,从而加快整个任务的执行。

典型的应用:价格监控、舆情分析和公共数据收集

下面看一个Rust的爬虫示例,这里使用异步运行库tokio和异步http库reqwest,在Cargo.toml中添加相应的依赖:

tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json", "gzip", "brotli", "deflate", "stream"] }Code language: JavaScript (javascript)

然后main中的代码如下:

use reqwest::Client;
use tokio;

async fn fetch_url(client: &Client, url: &str) {
    match client.get(url).send().await {
        Ok(resp) => {
            let status = resp.status();
            println!("[{}] Status: {}", url, status);
        }
        Err(e) => {
            eprintln!("[{}] Error: {}", url, e);
        }
    }
}

#[tokio::main]
async fn main() {
    let urls = vec![
        "https://www.news1.com",
        "https://www.news2.com",
        "https://www.news3.com",
    ];

    let client = Client::new();

    // execute all requests concurrently
    let mut tasks = vec![];
    for url in &urls {
        let url = url.to_string();
        let client = client.clone();
        let task = tokio::spawn(async move {
            fetch_url(&client, &url).await;
        });
        tasks.push(task);
    }

    // wait for all tasks to complete
    for task in tasks {
        let _ = task.await;
    }
}Code language: PHP (php)

总结

本篇文章主要介绍了异步编程async在实际场景中的一些应用,特别是在网络爬虫应用中,能够轻松提升服务的性能。

如果你只是碰巧读到这边文章,或者对异步编程async还不是很理解,可以参考下之前的两篇文章异步编程async和[Rust async原理剖析](https://xx/Rust async原理剖析)。