一. 前言
一般而言, 对于并发, 这是很多人编程道路上一道比较难跨过的坎.
并发编程常见的问题:
- 竞态(Race, 即不同的进程/线程访问同一个内存地址上的变量)
- 代码复杂度的增加
- 资源的管理
- 调试困难
在python
中提供了三种并发方式:
- 多进程(thread)
- 多线程(process)
- 协程(异步, async)
需要注意的是python
中的多线程, 由于GIL
的存在, 并不算真正意义的多线程
. 相关见, python核心开发者对于这个问题的介绍, 【python】听说因为有GIL, 多线程连锁都不需要了? _哔哩哔哩_bilibili
方式 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
多线程 | I/O 密集型任务 | 轻量级, 共享内存方便 | 受 GIL 限制, 不适合 CPU 密集型 |
多进程 | CPU 密集型任务 | 真正并行, 突破 GIL 限制 | 进程开销大, 通信复杂 |
异步 | I/O 密集型任务 | 单线程高效, 资源消耗最小 | 代码需异步风格, 调试较复杂 |
本文主要讨论的是协程, 对于另外二者暂不深入探讨, 他们之间的异同和优劣可见: 为什么 MySQL 使用多线程, 而 Oracle 和 PostgreSQL 使用多进程? - 知乎
1.1 什么是协程?
(图片版权见水印, 这张图很直观, 借用)
相关的介绍很多, 这里就不赘述, 简单来说, 就是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
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
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 ) |
- 自动化程度
Task
是Future
的子类, 但额外封装了协程的自动执行逻辑. 当你创建一个Task
时, 事件循环会立即开始执行它, 而Future
仅表示一个待完成的操作. - 使用场景
Future
: 用于底层异步操作的结果传递( 如自定义 C 扩展或第三方库的异步接口) .Task
: 用于并发执行协程( 如await asyncio.gather(task1, task2)
) .
- 生命周期
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

理解 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 的事件循环执行顺序遵循以下规则:
- 执行当前宏任务( 如整体代码块) .
- 清空微任务队列: 执行所有微任务, 直到队列为空.
- 渲染 UI( 浏览器环境, 仅在浏览器认为必要时触发) .
- 进入下一轮事件循环, 处理下一个宏任务.
这种机制确保了微任务的执行优先级高于宏任务, 且微任务不会阻塞下一个宏任务的执行.
四. 问题
在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
.
相信读者也发现了, python
和js
执行类似代码的明显差异.
假如python
为了保证异步的耗时部分操作能够执行, 需要在main
中增加await asyncio.sleep(4)
这个可能耗时很长的等待, 那么程序的结束将变得不可控, 同时运行的总消耗时间也大幅增长. 这, 异步的优点还存在吗?
为什么在js
不需要await asyncio.sleep(4)
类似的操作, 即可实现异步的操作一定可以执行到位呢?
4.1 小结
JavaScript
的运行环境( 一般为: 浏览器或 Node.js) 是长期运行的进程, 其生命周期与用户操作或服务请求强相关. 例如:
- 浏览器需要持续响应用户交互( 点击, 滚动) , 网络请求等事件, 因此事件循环必须始终运行.
- Node.js 服务需要持续监听端口, 处理并发请求, 事件循环同样不能主动终止.
这种设计使得 JavaScript
的事件循环天然支持异步任务的自动执行, 无需开发者手动干预. 而 Python
的 asyncio
通常用于脚本式或短生命周期的程序, 其事件循环的生命周期由开发者完全控制. 例如:
- 一个
Python
脚本可能只需要处理一个 HTTP 请求, 任务完成后程序即可退出. - 如果强行保持事件循环运行( 如通过
await asyncio.sleep(4)
) , 反而可能导致程序无法正常终止.
维度 | JavaScript | Python (asyncio) |
---|---|---|
事件循环本质 | 持续运行的闭环系统, 永不主动终止. | 可控制的执行单元, 生命周期由 asyncio.run() 管理. |
任务队列管理 | 自动处理宏任务和微任务, 直到队列为空. | 需要显式等待未完成的任务, 否则事件循环提前关闭. |
运行环境生命周期 | 长期运行的进程( 浏览器 / Node.js) , 与用户操作或服务请求强绑定. | 短生命周期的脚本或程序, 由开发者控制执行流程. |
典型代码模式 | 无需额外代码, 异步任务自动执行. | 必须通过 await 或 gather() 显式等待异步任务完成. |
五. 总结
综上, 在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
的帖子结尾
{
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
十年资历....!
{
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