Python并发编程与异步入门:线程、进程、线程池与 asyncio 基础

写 Python 程序时,很多任务看起来都是“按顺序一行一行执行”的。

这当然是默认方式,但真实开发中,你常常会遇到这样的需求:

  • 同时请求多个接口;
  • 同时处理多个文件;
  • 后台跑任务,前台继续响应;
  • 提高 I/O 密集任务效率;
  • 利用多核处理 CPU 密集任务。

这时就会接触到一个很重要的话题:并发编程

不过“并发”“并行”“异步”这些词,初学者常常容易混在一起。

所以这一篇不会一开始就堆很多概念,而是尽量围绕实际问题来讲清楚:

  • 什么情况下需要并发;
  • 线程、进程和异步各自适合什么;
  • Python 中最常见的并发工具有哪些;
  • asyncio 到底解决什么问题。

理解这一章之后,你就能对 Python 中常见的并发方案形成比较清晰的整体认识。

一、为什么需要并发?

假设你要请求 100 个网页。

如果你这样写:

1
2
for url in urls:
fetch(url)

程序通常会:

  • 请求第 1 个;
  • 等它返回;
  • 再请求第 2 个;
  • 再等它返回;
  • 如此反复。

这种方式当然能工作,但如果每次都要等网络响应,整体会很慢。

再比如:

  • 你要同时下载多个文件;
  • 你要同时监听多个连接;
  • 你要批量处理很多 I/O 操作;

如果全都串行执行,资源利用率往往不高。

所以并发的核心目标之一是:

当某个任务在等待时,让其他任务先继续做。


二、并发、并行、异步到底有什么区别?

这是最容易混淆的一部分。

2.1 并发(concurrency)

并发更强调:

  • 多个任务在一段时间内“交替推进”;
  • 宏观看起来像是同时进行。

不一定真的同一时刻都在运行,但整体上多个任务都在往前走。

2.2 并行(parallelism)

并行更强调:

  • 多个任务真的在同一时刻同时执行;
  • 通常需要多核 CPU 或多个执行单元。
2.3 异步(asynchronous)

异步是一种组织任务的方式,核心思想是:

  • 当任务需要等待时,不阻塞整个流程;
  • 而是先把控制权交出去,让别的任务运行。

所以可以简单理解为:

  • 并发是现象;
  • 并行是更强的同时执行;
  • 异步是一种实现并发的手段。

入门阶段你不需要把定义抠得特别死,先理解“它们解决的是让多个任务更高效推进的问题”就够了。


三、线程:适合很多 I/O 密集任务

3.1 什么是线程?

你可以先把线程理解为:

一个进程内部的执行单元。

一个程序启动后通常至少有一个主线程,而你也可以创建更多线程来并发执行任务。

3.2 一个最简单的线程示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import threading
import time


def worker(name):
for i in range(3):
print(f'{name} -> {i}')
time.sleep(1)


t1 = threading.Thread(target=worker, args=('线程1',))
t2 = threading.Thread(target=worker, args=('线程2',))

t1.start()
t2.start()

t1.join()
t2.join()

print('主线程结束')

这里:

  • start() 启动线程;
  • join() 等待线程执行完成。
3.3 线程适合什么场景?

线程通常比较适合:

  • 网络请求;
  • 文件读写;
  • 日志收集;
  • 爬虫中的并发抓取;
  • 其他 I/O 密集型任务。

因为这类任务很多时间都在“等待”,线程能帮助你把等待时间利用起来。


四、线程安全与锁:多个线程一起改数据怎么办?

线程虽然方便,但它也带来一个问题:

多个线程如果同时修改同一份数据,可能会发生竞争。

4.1 一个共享变量示意
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import threading

counter = 0


def add():
global counter
for _ in range(100000):
counter += 1


t1 = threading.Thread(target=add)
t2 = threading.Thread(target=add)

t1.start()
t2.start()
t1.join()
t2.join()

print(counter)

你可能以为结果一定是 200000,但在真实并发环境下,共享数据修改往往需要额外保护。

4.2 使用锁 Lock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import threading

counter = 0
lock = threading.Lock()


def add():
global counter
for _ in range(100000):
with lock:
counter += 1


t1 = threading.Thread(target=add)
t2 = threading.Thread(target=add)

t1.start()
t2.start()
t1.join()
t2.join()

print(counter)

锁的意义可以简单理解为:

  • 同一时刻只允许一个线程进入关键区域;
  • 避免共享数据被同时乱改。
4.3 锁的使用建议
  • 共享可变数据时要想到锁;
  • 锁范围不要过大;
  • 不要滥用,否则会降低并发收益。

五、线程池:比手动管理线程更实用

真实项目里,很多时候不会手动一个个创建线程,而是用线程池。

5.1 为什么线程池更常用?

因为它能帮你:

  • 复用线程;
  • 控制最大并发数量;
  • 更方便收集执行结果;
  • 写法更清晰。
5.2 使用 ThreadPoolExecutor
1
2
3
4
5
6
7
8
9
10
11
12
from concurrent.futures import ThreadPoolExecutor
import time


def task(n):
time.sleep(1)
return n * n


with ThreadPoolExecutor(max_workers=3) as executor:
results = executor.map(task, [1, 2, 3, 4, 5])
print(list(results))
5.3 提交任务并获取结果
1
2
3
4
5
6
7
8
9
10
11
12
13
from concurrent.futures import ThreadPoolExecutor
import time


def task(n):
time.sleep(1)
return n * n


with ThreadPoolExecutor(max_workers=3) as executor:
futures = [executor.submit(task, i) for i in range(5)]
for future in futures:
print(future.result())

线程池在很多实际场景里都非常适合,例如批量下载、并发请求接口等。


六、进程:更适合 CPU 密集型任务

6.1 什么是进程?

进程可以先理解为:

操作系统分配资源和运行程序的基本单位。

和线程相比,不同进程之间通常内存空间隔离更明显。

6.2 一个简单示例
1
2
3
4
5
6
7
8
9
10
11
12
13
from multiprocessing import Process
import os


def worker():
print('子进程 PID:', os.getpid())


if __name__ == '__main__':
p = Process(target=worker)
p.start()
p.join()
print('主进程结束')
6.3 为什么进程常用于 CPU 密集任务?

例如:

  • 大量数值计算;
  • 图像处理;
  • 数据压缩;
  • 复杂算法运算。

这类任务的瓶颈主要在 CPU,而不是 I/O 等待。

这时使用多进程通常更有意义。


七、进程池:批量 CPU 任务更常见的写法

7.1 使用 ProcessPoolExecutor
1
2
3
4
5
6
7
8
9
10
11
from concurrent.futures import ProcessPoolExecutor


def square(n):
return n * n


if __name__ == '__main__':
with ProcessPoolExecutor(max_workers=4) as executor:
results = executor.map(square, [1, 2, 3, 4, 5])
print(list(results))
7.2 什么时候优先想到进程池?
  • 任务之间相对独立;
  • 主要瓶颈在 CPU;
  • 需要利用多核能力;
  • 不想手动管理多个子进程。

八、线程和进程该怎么选?

这是最常见的问题之一。

你可以先用一个非常实用的判断方法:

8.1 I/O 密集型任务

例如:

  • 网络请求;
  • 文件读写;
  • 数据库访问;
  • 等待外部服务响应。

这类任务通常优先考虑:

  • 线程;
  • 线程池;
  • 异步 I/O。
8.2 CPU 密集型任务

例如:

  • 图像计算;
  • 大规模数学运算;
  • 编码压缩;
  • 复杂数据转换。

这类任务通常优先考虑:

  • 多进程;
  • 进程池。

你不需要死记教条,但这个判断在多数情况下很实用。


九、asyncio:另一种并发思路

线程和进程都很好理解,因为它们比较接近“开更多执行单元”。

asyncio 则更像是:

在一个线程里,通过协作式调度,让大量 I/O 任务高效推进。

9.1 asyncawait 是什么?

你可以先把它理解为:

  • async def 定义协程函数;
  • await 表示在这里等待某个异步结果,同时让出执行权。
9.2 一个最简单的示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import asyncio


async def say_after(delay, message):
await asyncio.sleep(delay)
print(message)


async def main():
await say_after(1, 'Hello')
await say_after(1, 'Python')


asyncio.run(main())

这段代码仍然是顺序等待。

9.3 并发运行多个协程:gather()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import asyncio


async def say_after(delay, message):
await asyncio.sleep(delay)
print(message)


async def main():
await asyncio.gather(
say_after(1, 'Hello'),
say_after(1, 'Python')
)


asyncio.run(main())

这时两个任务会并发推进,总时间通常接近 1 秒,而不是 2 秒。

9.4 asyncio 适合什么?

它特别适合:

  • 大量网络请求;
  • 高并发 I/O;
  • Web 爬虫;
  • 聊天、消息、连接类任务;
  • 异步 Web 框架。

十、为什么说 asyncio 更适合 I/O,而不是 CPU?

因为异步的强项在于:

  • 当任务需要等待时,把时间让给其他任务;
  • 避免线程切换的额外成本;
  • 在大量 I/O 请求时非常高效。

但如果任务本身一直在疯狂计算 CPU,没有等待点,那么 await 也帮不了你太多。

所以可以记住:

  • 高等待 → 异步很有优势;
  • 高计算 → 进程通常更合适。

十一、一个非常实用的选择思路

很多人学完之后最需要的其实不是“定义”,而是一个快速判断框架。

可以先这么用:

11.1 普通任务
  • 先写串行版本;
  • 如果性能足够,就不要为了并发而并发。
11.2 主要在等 I/O
  • 先考虑线程池;
  • 并发量更大时,再考虑异步。
11.3 主要在吃 CPU
  • 优先考虑进程池。
11.4 有大量网络请求,且希望更高吞吐
  • 往往可以进一步考虑 asyncio + 异步 HTTP 客户端。

十二、一个综合示例:线程池和异步的对比思路

假设你要处理很多网页请求。

线程池版思路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from concurrent.futures import ThreadPoolExecutor
import requests


def fetch(url):
response = requests.get(url, timeout=5)
return response.status_code


urls = ['https://httpbin.org/get'] * 10

with ThreadPoolExecutor(max_workers=5) as executor:
results = list(executor.map(fetch, urls))
print(results)

异步版思路(这里只展示结构感):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import asyncio


async def fetch(url):
await asyncio.sleep(1)
return url


async def main():
urls = ['a', 'b', 'c']
results = await asyncio.gather(*(fetch(url) for url in urls))
print(results)


asyncio.run(main())

这两个方向都能实现并发,但适用工具链和复杂度不同。


十三、学习并发时的常见误区

13.1 不要一开始就把所有方案混在一起

很多初学者一上来就同时学:

  • 线程;
  • 进程;
  • 异步;
  • 协程;
  • 锁;
  • 队列;
  • 回调。

结果很容易越学越乱。

更好的顺序通常是:

  1. 先理解“为什么需要并发”;
  2. 再理解线程和进程差别;
  3. 最后再学 asyncio
13.2 不要为了“高级”而强行异步

如果你的任务规模很小,串行就够快,那没必要一上来就异步化。

13.3 不要忽视共享数据问题

只要有多个执行单元一起修改同一份状态,就要想到线程安全或进程间通信问题。

13.4 不要把并发当成性能银弹

并发不是所有问题的答案。

有时真正慢的原因可能是:

  • SQL 查询没优化;
  • 算法复杂度太高;
  • 接口本身很慢;
  • 数据结构选错了。

十四、学习建议:这一章最值得优先掌握什么?

14.1 先建立“任务类型判断”能力

也就是学会区分:

  • 我这是 I/O 密集型任务,还是 CPU 密集型任务?

这比死记 API 更重要。

14.2 先掌握线程池和进程池

因为它们在真实项目里非常常见,而且比手写底层线程/进程管理更实用。

14.3 asyncio 先理解核心思想即可

先理解:

  • async def
  • await
  • asyncio.run()
  • asyncio.gather()

这就已经足够入门。

14.4 以后再往更深处走

后续如果继续深入,通常还会接触:

  • 线程安全队列;
  • 生产者消费者模型;
  • 异步 HTTP 客户端;
  • 异步 Web 框架;
  • 任务调度系统。

十五、总结

这一篇我们系统梳理了 Python 中“并发编程与异步入门”的核心内容:

  1. 为什么需要并发:理解等待型任务和批量任务带来的效率问题;
  2. 线程:适合理解和处理很多 I/O 密集场景;
  3. :用于保护共享数据,避免线程竞争;
  4. 线程池:更常见、更实用的线程管理方式;
  5. 进程:更适合 CPU 密集型任务;
  6. 进程池:用于更方便地管理多进程任务;
  7. asyncio:一种特别适合 I/O 并发的异步模型;
  8. 任务选择思路:学会区分 I/O 密集和 CPU 密集,并选择合适方案。

如果说前面的章节更多是在处理“单个任务怎么做”,那么这一篇更像是在回答:

当任务变多、等待变多、规模变大时,Python 应该如何更高效地组织执行流程?

掌握这一篇之后,你对 Python 的工程能力会再上一个台阶。

继续往后写时,比较自然的衔接方向通常有两个:

  • Python项目结构、打包与发布入门:让代码更规范、更可复用;
  • Python Web 开发或异步网络实战:把并发能力真正用于工程项目。

参考资料