惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
博客园 - 三生石上(FineUI控件)
博客园 - 司徒正美
博客园_首页
J
Java Code Geeks
V2EX - 技术
V2EX - 技术
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
TaoSecurity Blog
TaoSecurity Blog
T
Troy Hunt's Blog
Forbes - Security
Forbes - Security
Schneier on Security
Schneier on Security
Hugging Face - Blog
Hugging Face - Blog
PCI Perspectives
PCI Perspectives
O
OpenAI News
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Hacker News: Ask HN
Hacker News: Ask HN
Application and Cybersecurity Blog
Application and Cybersecurity Blog
H
Heimdal Security Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
博客园 - 聂微东
量子位
酷 壳 – CoolShell
酷 壳 – CoolShell
大猫的无限游戏
大猫的无限游戏
WordPress大学
WordPress大学
美团技术团队
V
V2EX
Cisco Talos Blog
Cisco Talos Blog
小众软件
小众软件
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
C
Cybersecurity and Infrastructure Security Agency CISA
有赞技术团队
有赞技术团队
腾讯CDC
Cloudbric
Cloudbric
Google DeepMind News
Google DeepMind News
博客园 - 【当耐特】
SecWiki News
SecWiki News
IT之家
IT之家
C
Cisco Blogs
雷峰网
雷峰网
aimingoo的专栏
aimingoo的专栏
B
Blog RSS Feed
S
Schneier on Security
Security Latest
Security Latest
Scott Helme
Scott Helme
H
Help Net Security
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Palo Alto Networks Blog
L
LINUX DO - 热门话题
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC

czp's blog

Ubuntu 下使用 zsh · HonKit 使用 VNC 来远程管理服务器 · HonKit Linux 下无限试用 JetBrains · HonKit 使用 KMS 激活 Windows · HonKit 为 KVM 中的 Windows 虚拟机启用 VirtIO · HonKit 在 Spring Boot 中处理 MissingKotlinParameterException · HonKit 用 Spring Native 拯救微服务 · HonKit 正确调试 PHP · HonKit Ubuntu 增加最大文件打开数量限制 · HonKit 将 Minecraft 服务端编译到 native · HonKit 用凝聚力购买 P 社游戏 DLC · HonKit 模拟超星网课 Android 客户端 · HonKit 模拟 Bilibili Android 客户端 · HonKit 模拟哔咔 Android 客户端 · HonKit GCP Pub/Sub push 至 IAP 保护的 Endpoint · HonKit 部署 Spring Native 到 GCP App Engine · HonKit 多个 Gradle SubProject 同时编译导致 CI/CD 内存耗尽 · HonKit Google Cloud Build 运行 DIND · HonKit 在 Google Storage 中部署 SPA · HonKit Terraform 不自动更新 CloudRun service · HonKit Telegram Bot 收不到普通群聊消息的问题 · HonKit 加密货币交易 · HonKit 挖掘以太坊 · HonKit 挖掘门罗币 · HonKit 小明学英语 · HonKit 卖程序的小女孩 · HonKit 一个测试工程师走进一家酒吧 · HonKit 我给你们讲个 TCP 笑话吧! · HonKit 我给你们讲个 UDP 笑话吧! · HonKit 格局打开 · HonKit c 语言常见未定义行为 · HonKit 在 Kotlin 使用 SpaceEngineers Remote API · HonKit Kotlin 协程 · HonKit 在 Spring Boot 中正确注册 Jackson Module · HonKit Spring Boot Jpa 使用 Google Cloud SQL · HonKit Spring Boot 中配置单页应用 · HonKit Spring Boot 无法加载 ClasspathResource 问题 · HonKit 正确打包 Spring Boot 到 war · HonKit czp's blog · HonKit 鸣谢列表 · HonKit 友链 · HonKit
异步是什么 · HonKit
2026-04-11 · via czp's blog

异步是什么

在了解异步之前, 先来理解一下什么是同步. 假设有以下伪代码

const result = get("https://api.github.com/users/czp3009/repos")
console.log(result.length)

get 函数会访问 github api 并以 json 格式返回 api 的 ResponseBody. 在 get 函数返回前, 后面的代码不会被执行. 而如果网络耗时较长, 我们就会感受到 get 函数会"卡住"程序的运行. 这种"卡住"就是所谓的"阻塞".

简单地说, 如果一个函数被调用后, 在结果返回前, 线程会一直阻塞, 那么这个函数就是同步的.

阻塞并不是类似某些单片机代码那样用 while(true) 来让 cpu 空转从而不进入下一行, 阻塞是操作系统实现的. 现代操作系统都是分时操作系统, 既然操作系统可以为每个线程分配时间片, 自然也就可以停止对某个线程的时间片分配, 让线程"挂起".

操作系统的基础工作之一是管理计算机的资源, 应用程序不能直接访问计算机上的资源, 包括文件, 网络等等. 因此所有资源的访问最底层都是由操作系统提供的接口(system call). 比如说从 socket 中读取字节时所用的 read 函数就是操作系统提供的(c 语言).

#include <unistd.h>

ssize_t read(int fs, void *buf, size_t N);

当应用程序调用了 read 函数, 而目前 socket 中无字节可读时, 操作系统就会挂起此线程, 不再为此线程分配时间片, 直到 socket 中有新的数据到来. 除此之外, 其他的会"卡住"线程的功能, 比如锁最终也是由操作系统实现的, 如果未能获得锁此线程就会被操作系统挂起直到锁可用.

阻塞是很多程序中不愿意出现的, 比如说现在有一个 UI 程序, 当按钮按下时会发送一个 HTTP 请求. 如果按钮的代码是同步的, 那么按钮按下去之后就会保持按下的状态不会立即弹回来, 因为线程被阻塞了. 最简单的解决这个问题的办法就是创建一个新的线程, 让这个新线程代替主线程去"卡住". 这样主线程的按钮代码是立即执行完毕的, 与之前的同步代码相比, 调用函数后可以立即进行其他操作就是异步(对主线程而言).

多线程是一种常用的异步实现, 但是在大多数情况下, 异步并不是因为操作是计算密集型的, 而是因为操作调用了某种返回速度很慢的计算机资源, 比如上面作为例子的网络 IO. 其本质是相对高速的 CPU 与响应时间很长的网络之前的不匹配, 因此 CPU 必须有一段时间的 suspend 来等待网络 IO 继而进行下一步操作.

为了能够利用网络请求的返回内容, 还需要将本来写在 get 函数下方的代码以回调方式传入新的异步函数 asyncGet

function asyncGet(url, callback) {
    new Thread(()=>{
        const result = get(url)
        callback(result)
    }).start()
}

asyncGet("https://api.github.com/users/czp3009/repos", result=>{
    console.log(result.length)
})
//next line

因此异步的一个表观特征是代码的执行顺序和源码顺序不相同. asyncGet 函数会立即返回让程序进入下一行, 而回调则在网络请求结束后才被执行.

采用多线程的方式, 我们可以为主线程上每一处进行网络 IO 的地方都创建一个新线程来发送请求, 从而保持主线程永远不阻塞. 但是很快的, 我们会发现线程是一种比较贵的资源. 线程一经创建, 操作系统会为它分配栈空间, 创建出几千个线程就会少掉一大块内存. 另外, 线程数量很多之后, 操作系统进行线程上下文切换的开销甚至会超过任务本身的开销. 因此通过多线程来实现异步网络 IO 并不是非常好的办法.

阻塞出在操作系统上, 既然如此, 那么能不能与操作系统商量一下, 让应用程序能在一个线程中创建一千个 socket 后被操作系统挂起, 然后要求操作系统只要这一千个 socket 中有任意一个 socket 有字节可用就唤醒此线程, 从而避免创建一千个线程去分别等待它们呢. 这种办法是有的, 在现代操作系统上都有这种 IO 事件通知机制. 在 Linux 中称为 epoll, 在 BSD 系列中称为 kqueue, NT 上的等价品叫做 iocp. 以 epoll 为例, 最终会有以下 c 语言代码

#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;

/* Code to set up listening socket, 'listen_sock',
  (socket(), bind(), listen()) omitted. */

epollfd = epoll_create1(0);
if (epollfd == -1) {
   perror("epoll_create1");
   exit(EXIT_FAILURE);
}

ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
   perror("epoll_ctl: listen_sock");
   exit(EXIT_FAILURE);
}

for (;;) {
   nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
   if (nfds == -1) {
       perror("epoll_wait");
       exit(EXIT_FAILURE);
   }

   for (n = 0; n < nfds; ++n) {
       if (events[n].data.fd == listen_sock) {
           conn_sock = accept(listen_sock, (struct sockaddr *) &addr, &addrlen);
           if (conn_sock == -1) {
               perror("accept");
               exit(EXIT_FAILURE);
           }
           setnonblocking(conn_sock);
           ev.events = EPOLLIN | EPOLLET;
           ev.data.fd = conn_sock;
           if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1) {
               perror("epoll_ctl: conn_sock");
               exit(EXIT_FAILURE);
           }
       } else {
           do_use_fd(events[n].data.fd);
       }
   }
}

这种事件驱动(event-driven)的网络 IO 模式可以让应用程序告诉操作系统想要监听哪一些 socket, 然后应用程序用一个线程去循环取得对应的事件, 包括连接成功, 连接断开, 有字节可用等. 如果当前没有事件可用, 此线程将在 epoll_wait 函数被阻塞. 而一旦有事件, 此线程就会被唤醒, 程序可以从事件中取出事件类型以及对应的 socket, 继而对其进行处理. 这样应用程序就只需要一个线程就可以管理大量 socket. 除非完全没有事情可做(没有事件), 否则此线程就永远在工作(处理事件).

因此对于非计算密集型任务, 使用多线程来实现异步是不必要的. 现代操作系统均已对网络, 文件等阻塞的源头提供了相应的事件驱动 API. 事件驱动是如此的好用以至于很多语言对程序内部的一些过程, 比如延迟函数, 计时器等也使用事件驱动的方式来实现. Node.js 所用的 libuv 就是一个很好地例子, 这使得整个运行时就是一个事件队列.

然而, 虽然通过事件驱动的方式解决了线程过多的问题, 在对底层的 socket 访问代码进行包装后, 高层代码依然使用回调的模式来利用请求的返回内容. 如果第二个请求依赖于第一个请求的返回内容, 那么第二个请求就必须写到第一个请求的回调内部去, 最终回调里套了别的回调(下例伪代码中的 asyncGet 函数包装了 epoll 而不是多线程)

asyncGet("https://api.github.com/users/czp3009/repos", result=>{
    const firstRepoName = result[0].name
    asyncGet(`https://api.github.com/repos/czp3009/${firstRepoName}`, result=>{
        console.log(result["full_name"])
    })
})

可能两层回调还不是那么难以阅读, 但是如果有十几层, 二十几层, 代码就人类不可读了. 这种代码被称为回调地狱(callback hell). 为了解决回调地狱, 一些语言比如 Node.js 通过运行时维护的事件队列, 允许一个异步函数在实际结果返回时不是直接运行回调, 而是把本来应该是回调的函数投入到运行时维护的事件队列中, 由运行时来执行. 同时在高层代码中抽象出 Promise 的概念来改善代码结构(下例中的 asyncGet 函数返回 Promise)

asyncGet("https://api.github.com/users/czp3009/repos")
    .then(result=>{
        const firstRepoName = result[0].name
        return asyncGet(`https://api.github.com/repos/czp3009/${firstRepoName}`)
    })
    .then(result=>{
        console.log(result["full_name"])
    })

通过 Promise.then 来指定在前一个 Promise 执行完毕后将被投入到事件队列的函数, 并且返回值也可以是 Promise, 从而实现链式代码. 这样就在一定程度上解决了回调地狱.

但是这种方式也不是特别聪明, 与最初的同步代码相比, 虽然我们解决了主线程会被阻塞的问题以及采用多线程方式解决阻塞会导致线程过多的问题, 我们的代码结构依然被改变了. 新的代码结构依然会在复杂情况下增加程序员心智负担. 那么能不能有一种看起来类似同步代码的方式来实现异步呢.

答案也是有的, 请听下回分解之 协程.