Python和JavaScript的异步

一. 前言

一般而言, 对于并发, 这是很多人编程道路上一道比较难跨过的坎.

并发编程常见的问题:

  • 竞态(Race, 即不同的进程/线程访问同一个内存地址上的变量)
  • 代码复杂度的增加
  • 资源的管理
  • 调试困难

python中提供了三种并发方式:

  • 多进程(thread)
  • 多线程(process)
  • 协程(异步, async)

img

需要注意的是python中的多线程, 由于GIL的存在, 并不算真正意义的多线程. 相关见, python核心开发者对于这个问题的介绍, 【python】听说因为有GIL, 多线程连锁都不需要了? _哔哩哔哩_bilibili

方式 适用场景 优点 缺点
多线程 I/O 密集型任务 轻量级, 共享内存方便 受 GIL 限制, 不适合 CPU 密集型
多进程 CPU 密集型任务 真正并行, 突破 GIL 限制 进程开销大, 通信复杂
异步 I/O 密集型任务 单线程高效, 资源消耗最小 代码需异步风格, 调试较复杂

本文主要讨论的是协程, 对于另外二者暂不深入探讨, 他们之间的异同和优劣可见: 为什么 MySQL 使用多线程, 而 Oracle 和 PostgreSQL 使用多进程? - 知乎

1.1 什么是协程?

img(图片版权见水印, 这张图很直观, 借用)

Coroutine - HandWiki

相关的介绍很多, 这里就不赘述, 简单来说, 就是CPU的调度的效率最大化, 可视为在实际生产中的流水线调度问题: 生产调度问题Job-shop和Flow-shop问题的区别在哪? - 知乎.

对于协程, 相比于其他二者的好处在于:

  • 资源消耗小: 无需系统内核的上下文切换, 减小资源开销;
  • 内存共享/无需加锁: 无需原子操作锁定及同步的开销, 不用担心资源共享的问题;
  • 效率极高: 单线程即可实现高并发, 对于IO型操作, 如最为常见的web/爬虫操作, 异步可以发挥到极致.

二. Python

asyncio --- 异步 I/O - Python 3.13.3 文档

import asyncio

async def test():
    print("start")
    await asyncio.sleep(3)
    print("end")

asyncio.run(test())

async/await是核心关键字, asyncio是核心包

2.1 Futures

Futures - Python 3.13.3 文档

import asyncio

async def set_after(fut, delay, value):
    # 模拟异步操作, 延迟后设置 Future 的结果
    await asyncio.sleep(delay)
    fut.set_result(value)

async def main():
    loop = asyncio.get_running_loop()
    fut = loop.create_future()

    # 创建一个任务来设置 Future 的结果
    asyncio.create_task(set_after(fut, 1, "Hello"))

    # 等待 Future 完成并获取结果
    result = await fut
    print(f"Future result: {result}")

asyncio.run(main())

2.2 Task

协程与任务 - Python 3.13.3 文档

import asyncio
import time

async def coro1():
    await asyncio.sleep(1)
    return "Result from coro1"

async def coro2():
    await asyncio.sleep(1)
    return "Result from coro2"

async def main():
    # 创建两个任务并发执行
    s = time.time()
    # 实际上不需要创建任务
    task1 = asyncio.create_task(coro1())
    task2 = asyncio.create_task(coro2())
    
    '''
    # task1 = asyncio.create_task(coro1())
    # task2 = asyncio.create_task(coro2())
	
	# 需要注意的是添加task, 直接await
	await task1
	await task2 # 消耗时间2秒
	
    await coro1()
    await coro2() # 消耗时间3秒
    '''
	
    # coro1(), <coroutine object coro1 at 0x000002CD8A8A8F90>
    # 返回一个对象
    # 等价 asyncio.gather(coro1(), coro1())
    results = await asyncio.gather(task1, task2)
    print(results)  # 输出: ['Result from coro1', 'Result from coro2']
    print(time.time() - s)

asyncio.run(main())

2.3 小结

简单来说二者的差异:

  • Future 是底层抽象, 用于表示异步操作的结果, 需手动管理状态.
  • Task 是 Future 的高级封装, 专门用于执行协程, 自动管理生命周期.
  • 日常开发中 Task 更常用, 而 Future 多用于库的底层实现.
方法 Future Task
set_result(value) 设置结果值 ❌( 自动由协程返回值设置)
set_exception(exc) 设置异常 ❌( 自动由协程异常传播)
cancel() 取消操作 取消任务( 若未完成)
add_done_callback(fn) 添加回调函数( 结果就绪时触发) 支持( 但更常用 await)
  1. 自动化程度
    TaskFuture 的子类, 但额外封装了协程的自动执行逻辑. 当你创建一个 Task 时, 事件循环会立即开始执行它, 而 Future 仅表示一个待完成的操作.
  2. 使用场景
    • Future: 用于底层异步操作的结果传递( 如自定义 C 扩展或第三方库的异步接口) .
    • Task: 用于并发执行协程( 如 await asyncio.gather(task1, task2)) .
  3. 生命周期
    Task 会随协程执行完毕自动完成, 而 Future 需要手动标记完成状态.
特性 asyncio.Future asyncio.Task
定义 表示异步操作的最终结果( 占位符) 对协程的封装, 由事件循环自动调度执行
创建方式 手动创建( 如 future = asyncio.Future()) 通过 asyncio.create_task()loop.create_task() 创建
执行控制 不自动执行, 需手动设置结果或异常 自动绑定到事件循环, 协程代码按需执行
典型用途 底层异步操作的结果封装( 如 I/O 完成标志) 封装协程以实现并发( 如 await 多个 Task)
状态管理 需手动调用 set_result()set_exception() 协程执行完毕自动标记为完成
与事件循环的关系 被动对象, 需外部驱动 主动注册到事件循环, 由循环调度执行

2.4 常见异步框架

库名称 简要描述
asyncio Python标准库, 提供异步I/O, 事件循环, 协程和任务支持, 适用于I/O密集型任务.
aiohttp 异步HTTP客户端/服务器库, 支持高并发网络请求, 常用于爬虫和API服务.
aiomysql 异步MySQL数据库驱动, 支持非阻塞连接池, 适用于高并发数据库操作.
Tornado 异步网络框架, 擅长处理长连接( 如WebSocket) , 适合实时应用.
Twisted 事件驱动的网络引擎, 支持异步编程, 适用于复杂网络协议开发.
uvloop 高性能异步事件循环, 替代asyncio默认循环, 提升I/O密集型任务性能.
FastAPI 基于Starlette的高性能异步Web框架, 支持自动文档生成, 适合构建RESTful API.
aiofiles 异步文件操作库, 支持非阻塞读写, 适用于大文件处理.
gevent 基于协程的异步网络库, 使用greenlet实现, 适用于高并发网络服务.
curio 轻量级异步I/O库, 提供简洁的协程和任务管理接口, 适合简化异步编程.
trio 用户友好的异步并发库, 强调易用性和错误处理, 适合新手入门.
sanic 类Flask的异步Web框架, 基于asyncio, 适合构建高性能API服务.

三. JavaScript

JavaScript的单线程你真的理解了吗? 众所周知, 我们的JavaScript是一门单线程的语言, 对的, 天王老子来了它 - 掘金

异步 JavaScript 简介 - 学习 Web 开发 | MDN

对于只有单线的js而言, 异步就是灵魂.

console.log('start');
Promise.resolve().then(() => setTimeout(() => console.log('Promise'), 3000));
console.log('end');

js中使用异步几乎是一种无感的存在.

3.1 EventLoop

img

b8216007b6f96d162cee7ae4cf4f483a.gif (906×507)

事件循环 - JavaScript | MDN

理解 JavaScript 中的 macrotask 和 microtask详细介绍了浏览器中的 microtask 和 - 掘金

事件循环: 微任务和宏任务

JavaScript 运行机制详解: 再谈Event Loop - 阮一峰的网络日志

特性 微任务( Microtask) 宏任务( Macrotask)
执行时机 在当前宏任务执行结束后立即执行, 直到队列为空. 在下一轮事件循环的开始执行, 每次只处理一个宏任务.
常见触发方式 Promise.then, queueMicrotask, process.nextTick setTimeout, setInterval, I/O 操作, UI 渲染
队列清空规则 连续执行所有微任务, 直到队列为空. 每次事件循环只处理一个宏任务, 然后切换到微任务队列.
对 UI 渲染的影响 微任务执行期间不会触发 UI 渲染. 宏任务执行前可能触发 UI 渲染( 浏览器环境) .

3.1.1 宏任务

宏任务是由浏览器或 Node.js 环境提供的异步任务, 每次事件循环只会处理一个宏任务. 常见的宏任务包括:

  • 整体代码块( Script)
  • setTimeout, setInterval
  • I/O 操作( 如网络请求, 文件读取)
  • UI 渲染( 浏览器环境)
  • setImmediate( Node.js 环境)

3.1.2 微任务

微任务是由 JavaScript 引擎自身管理的异步任务, 在每个宏任务执行结束后, 会立即执行所有微任务队列中的任务, 直到队列为空. 常见的微任务包括:

  • Promise.then, Promise.catch, Promise.finally
  • async/await( 本质是 Promise 的语法糖)
  • process.nextTick( Node.js 环境)
  • queueMicrotask( ES2020 新增 API)

3.1.3 小结

{
    console.log('start');
    setTimeout(() => {
        console.log('setTimeout');
    }, 0);
    Promise.resolve().then(() => {
        console.log('Promise');
    });
    console.log('end');
}

/*
start
end
Promise
setTimeout
*/

JavaScript 的事件循环执行顺序遵循以下规则:

  1. 执行当前宏任务( 如整体代码块) .
  2. 清空微任务队列: 执行所有微任务, 直到队列为空.
  3. 渲染 UI( 浏览器环境, 仅在浏览器认为必要时触发) .
  4. 进入下一轮事件循环, 处理下一个宏任务.

这种机制确保了微任务的执行优先级高于宏任务, 且微任务不会阻塞下一个宏任务的执行.

四. 问题

python中实现类似的功能.

console.log('start'); // 1
Promise.resolve().then(() => console.log('Promise')); // 3
console.log('end'); // 2

应用场景, 如操作数据库.

def test():
    # 伪代码
    db.insert_data() # 需要耗时较久 2
    return result # 需要马上返回结果 1
import asyncio

# 创建一个协程, 模拟 Promise.resolve()
async def promise_task():
    print('Promise') # 3

async def main():
    print('start') # 1
    # 将协程加入事件循环, 但不等待它完成
    asyncio.create_task(promise_task())
    print('end') # 2

# 运行异步主函数
asyncio.run(main())

问题出现, 即, await asyncio.sleep(0), 为什么要加这部分代码呢?

上面大代码不好看出细节, 将需求改动一下, 增加延时 3 秒.

console.log('start');
Promise.resolve().then(() => setTimeout(() => console.log('Promise'), 3000));
console.log('end');

假如还是按照上述的python代码

async def promise_task():
    await asyncio.sleep(3)
    print('Promise')

稍微改动

import asyncio

# 创建一个协程, 模拟 Promise.resolve()
async def promise_task():
    await asyncio.sleep(3)
    print('Promise')

async def main():
    print('start')
    # 将协程加入事件循环, 但不等待它完成
    asyncio.create_task(promise_task())
    print('end')
    # 确保事件循环有机会执行异步任务
    # await asyncio.sleep(4) # 假如注释掉这段

# 运行异步主函数
asyncio.run(main())

会发现print('Promise')这一步永远无法执行到

async def main():
    print('start')
    # 将协程加入事件循环, 但不等待它完成
    asyncio.create_task(promise_task())
    print('end')
    # 确保事件循环有机会执行异步任务
    await asyncio.sleep(4)

需要在main中同样加入await.

相信读者也发现了, pythonjs执行类似代码的明显差异.

假如python为了保证异步的耗时部分操作能够执行, 需要在main中增加await asyncio.sleep(4)这个可能耗时很长的等待, 那么程序的结束将变得不可控, 同时运行的总消耗时间也大幅增长. 这, 异步的优点还存在吗?

为什么在js不需要await asyncio.sleep(4)类似的操作, 即可实现异步的操作一定可以执行到位呢?

4.1 小结

JavaScript 的运行环境( 一般为: 浏览器或 Node.js) 是长期运行的进程, 其生命周期与用户操作或服务请求强相关. 例如:

  • 浏览器需要持续响应用户交互( 点击, 滚动) , 网络请求等事件, 因此事件循环必须始终运行.
  • Node.js 服务需要持续监听端口, 处理并发请求, 事件循环同样不能主动终止.

这种设计使得 JavaScript 的事件循环天然支持异步任务的自动执行, 无需开发者手动干预. 而 Pythonasyncio 通常用于脚本式或短生命周期的程序, 其事件循环的生命周期由开发者完全控制. 例如:

  • 一个 Python 脚本可能只需要处理一个 HTTP 请求, 任务完成后程序即可退出.
  • 如果强行保持事件循环运行( 如通过 await asyncio.sleep(4)) , 反而可能导致程序无法正常终止.
维度 JavaScript Python (asyncio)
事件循环本质 持续运行的闭环系统, 永不主动终止. 可控制的执行单元, 生命周期由 asyncio.run() 管理.
任务队列管理 自动处理宏任务和微任务, 直到队列为空. 需要显式等待未完成的任务, 否则事件循环提前关闭.
运行环境生命周期 长期运行的进程( 浏览器 / Node.js) , 与用户操作或服务请求强绑定. 短生命周期的脚本或程序, 由开发者控制执行流程.
典型代码模式 无需额外代码, 异步任务自动执行. 必须通过 awaitgather() 显式等待异步任务完成.

五. 总结

综上, 在web服务器端, 这种理论需要"永远"运行下去的环境, python的异步同样也能很"自然".

import asyncio
from fastapi import FastAPI

app = FastAPI()

async def promise_task():
    await asyncio.sleep(3)
    print('Promise')  # 现在这个代码会被执行

@app.get("/")
async def test_get():
    print('start')
    # 在当前事件循环中创建任务
    asyncio.create_task(promise_task())
    print('end')
    # 立即返回响应, 不等待异步任务完成
    return {'bilili': 'ok'}

if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="localhost", port=8763)

最后以一个v2ex的帖子结尾

img

{
    const ps = new Promise(
        // 传入的函数, 立马执行, 碰到异步操作的, 如fetch, 则等待异步结果返回
        function name(resolve, reject) {
            let i = 0;
            while (i < 3) {
                i = i + 1;
                console.log('i=', i); // 这些全是同步代码, 所以直接执行
            }
        }
    );
    console.log('promise 是异步吗? ');
}
/*
[Running] node "d:\Code\javascript_workspace\tempCodeRunnerFile.javascript"
i= 1
i= 2
i= 3
promise 是异步吗? 
*/
return new Promise((resolve, reject) => fetch(url, configs)
    .then(res => res.json())
    .then(data => {
        if (data.code !== 0) {
            console.log(data);
            reject();
        } else resolve(data);
    }).catch(e => reject(e))
);
{
    const ps = new Promise(
        // 传入的函数, 立马执行, 碰到异步操作的, 如fetch, 则等待异步结果返回
        function name(resolve, reject) {
            let i = 0;
            while (i < 3) {
                i = i + 1;
                console.log('i=', i); // 这些全是同步代码, 所以直接执行
            }
            fetch('https://www.baidu.com')
                .then(res => res.status)
                .then(data => {
                    console.log(data);
                });
        }
    );
    console.log('promise 是异步吗? ');
}
VM182:8 i= 1
VM182:8 i= 2
VM182:8 i= 3
VM182:17 promise 是异步吗? 
VM182:13 200

Promise() 构造函数 - JavaScript | MDN

img

十年资历....!

{
    Promise.resolve().then(
        function name(resolve, reject) {
            let i = 0;
            while (i < 3) {
                i = i + 1;
                console.log('i=', i);
            }
        }
    );
    console.log('promise 是异步吗? ');
}
/*
promise 是异步吗? 
i= 1
i= 2
i= 3
*/

JavaScript 中的 Promise 跟异步有关系吗? 还是我的理解有问题? 谁能把 Promise 解释清楚? Promise 的正确用法应该是什么样的? - V2EX