第一部分:并发编程基础理论
1. 并发编程概述
1.1 并发编程的意义与目标
并发编程(Concurrent Programming)的核心目标是提高程序执行效率,充分利用多核 CPU 资源,使多个任务能够在同一时间段内得到处理,从而提升计算机资源的整体利用率。
并发编程并不仅仅意味着“同时执行”,其本质在于任务的合理调度与资源的高效利用。在单核 CPU 上,也可以通过任务切换实现“看似同时执行”的效果。通过并发机制,程序能够在等待 I/O 操作期间执行其他任务,从而提高吞吐量与响应性能。
2. 同步与异步、阻塞与非阻塞
理解“同步 / 异步”与“阻塞 / 非阻塞”是掌握并发编程的基础。这四个概念描述了任务在等待和执行过程中的行为模式。
2.1 同步与异步
| 模式 | 执行逻辑 | 是否等待结果 | 示例 |
|---|---|---|---|
| 同步(Synchronous) | 当前任务完成后才能继续执行下一个任务 | 是 | 普通函数调用 |
| 异步(Asynchronous) | 任务发出后可继续执行其他操作,无需等待完成 | 否 | 异步 I/O、回调机制 |
- 同步:任务顺序执行,一个完成后才能进行下一个。
- 异步:任务之间可交替执行,无需等待上一个任务的结果。
2.2 阻塞与非阻塞
| 模式 | CPU 状态 | 是否等待操作完成 | 示例 |
|---|---|---|---|
| 阻塞(Blocking) | CPU 空闲 | 是 | requests.get() |
| 非阻塞(Non-blocking) | CPU 可执行其他任务 | 否 | 非阻塞 socket、异步请求 |
- 阻塞:当任务等待外部事件(如网络响应)时,CPU 处于空闲状态。
- 非阻塞:等待期间,CPU 可以继续执行其他任务。
2.3 四种组合模式
| 模式 | 执行机制 | CPU 利用率 | 效率 | 常见场景 |
|---|---|---|---|---|
| 同步阻塞 | 顺序执行,等待时 CPU 空闲 | 低 | 最低 | 普通网络请求 |
| 异步阻塞 | 多线程并发,但调用仍阻塞 | 中 | 较低 | 多线程下载 |
| 同步非阻塞 | 主动轮询检查结果 | 中 | 中 | 非阻塞 socket |
| 异步非阻塞 | 异步调度,完全非阻塞执行 | 高 | 最高 | asyncio、aiohttp |
2.4 示例代码
以下示例展示了四种执行模式的差异。
python
import time
import requests
import aiohttp
import asyncio
import socket
import threading
# ===============================
# 1. 同步阻塞 (Synchronous Blocking)
# ===============================
def sync_blocking_example():
"""同步阻塞示例:依次请求多个URL"""
urls = ['http://httpbin.org/delay/1'] * 3
start = time.time()
for url in urls:
resp = requests.get(url)
print(f"[Sync Blocking] Got response: {resp.status_code}")
print(f"Total time: {time.time() - start:.2f}s") # 约3秒
# ===============================
# 2. 异步阻塞 (Asynchronous Blocking)
# ===============================
def async_blocking_example():
"""异步阻塞示例:多线程并发但阻塞I/O"""
urls = ['http://httpbin.org/delay/1'] * 3
start = time.time()
def fetch(url):
resp = requests.get(url)
print(f"[Async Blocking] Got response: {resp.status_code}")
threads = [threading.Thread(target=fetch, args=(u,)) for u in urls]
[t.start() for t in threads]
[t.join() for t in threads]
print(f"Total time: {time.time() - start:.2f}s") # 约1秒
# ===============================
# 3. 同步非阻塞 (Synchronous Non-blocking)
# ===============================
def sync_nonblocking_example():
"""同步非阻塞示例:非阻塞socket轮询"""
start = time.time()
host, port = "httpbin.org", 80
s = socket.socket()
s.setblocking(False)
try:
s.connect((host, port))
except BlockingIOError:
pass # 非阻塞下连接未立即建立
while True:
try:
s.send(b"GET /delay/1 HTTP/1.0\r\nHost: httpbin.org\r\n\r\n")
break
except BlockingIOError:
continue # 轮询等待
resp = b""
while True:
try:
chunk = s.recv(4096)
if not chunk:
break
resp += chunk
except BlockingIOError:
continue
print(f"[Sync Non-blocking] Response length: {len(resp)}")
print(f"Total time: {time.time() - start:.2f}s")
s.close()
# ===============================
# 4. 异步非阻塞 (Asynchronous Non-blocking)
# ===============================
async def async_nonblocking_example():
"""异步非阻塞示例:aiohttp + asyncio"""
urls = ['http://httpbin.org/delay/1'] * 3
start = time.time()
async with aiohttp.ClientSession() as session:
tasks = [session.get(url) for url in urls]
responses = await asyncio.gather(*tasks)
for r in responses:
print(f"[Async Non-blocking] Got response: {r.status}")
await r.text()
print(f"Total time: {time.time() - start:.2f}s") # 约1秒
if __name__ == "__main__":
print("=== 1. 同步阻塞 ===")
sync_blocking_example()
print("\n=== 2. 异步阻塞 ===")
async_blocking_example()
print("\n=== 3. 同步非阻塞 ===")
sync_nonblocking_example()
print("\n=== 4. 异步非阻塞 ===")
asyncio.run(async_nonblocking_example())结果对比表:
| 模式 | 预计耗时 | 特征 |
|---|---|---|
| 同步阻塞 | ≈ 3s | 顺序执行,CPU 空闲 |
| 异步阻塞 | ≈ 1s | 多线程并发但阻塞 I/O |
| 同步非阻塞 | ≈ 1.2s | 轮询方式,CPU 占用较高 |
| 异步非阻塞 | ≈ 1s | 完全异步调度,效率最高 |
3. 串行、并发与并行
| 模式 | 描述 | 特点 |
|---|---|---|
| 串行(Serial) | 任务依次执行,一个任务完成后才开始下一个任务 | 实现简单但效率较低 |
| 并发(Concurrent) | 多个任务在同一核心上交替执行 | 单核可实现,看似同时执行 |
| 并行(Parallel) | 多个任务在多个 CPU 核心上同时执行 | 真实意义上的“同时”运行 |
并发是“任务调度策略”,并行是“任务执行状态”。
4. 实现多任务的三种方式
4.1 进程(Process)
- 定义:进程是操作系统分配资源的基本单位,拥有独立的内存空间、数据段、代码段和堆栈。
- 特点:
- 进程间相互独立;
- 创建和销毁开销较大;
- 通信需通过 IPC(如管道、消息队列、共享内存)。
- 适用场景:独立任务、CPU 密集型任务。
4.2 线程(Thread)
- 定义:线程是进程内最小的执行单元,共享进程资源(内存、文件描述符等)。
- 特点:
- 创建销毁成本较低;
- 共享内存空间,通信高效;
- 需要同步机制(如锁)防止数据竞争;
- 在 Python 中受 GIL 限制。
- 适用场景:相关性强的并发任务、轻度 I/O 操作。
4.3 协程(Coroutine)
- 定义:协程是用户态的轻量级线程,由程序控制切换。
- 特点:
- 切换开销极小;
- 单线程即可实现并发;
- 适合 I/O 密集型任务;
- 不适合 CPU 密集任务。
- 适用场景:大量网络请求、文件读写等 I/O 密集操作。
5. 三种方式对比
| 特性 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 调度单位 | 操作系统 | 操作系统 | 用户态(语言层) |
| 内存空间 | 独立 | 共享 | 共享 |
| 创建/销毁成本 | 高 | 中 | 低 |
| 并发性能 | 较好 | 较好 | 最优(I/O 场景) |
| 通信方式 | IPC | 共享内存 | 共享内存 |
| 适用场景 | 独立任务 / CPU 密集 | 相关任务 / 轻 I/O | 大量 I/O 密集 |
| CPU 利用 | 真正并行 | 受 GIL 限制 | 单线程并发 |
6. 适用场景与选择策略
| 任务类型 | 推荐方式 | 优点 | 缺点 |
|---|---|---|---|
| CPU 密集型 | 多进程 | 真正并行执行 | 内存占用较高 |
| I/O 密集型(少量) | 多线程 | 实现简单 | 受 GIL 限制 |
| I/O 密集型(大量) | 协程 | 高并发、低开销 | 无法多核并行 |
| 混合型任务 | 进程 + 协程 | 综合利用 CPU 与 I/O | 实现复杂 |
7. 总结
- 并发编程旨在通过合理的调度机制提高资源利用率;
- “同步 / 异步”“阻塞 / 非阻塞”定义了任务在等待与执行过程中的行为;
- “进程 / 线程 / 协程”是三种主要的多任务实现方式;
- CPU 密集型任务使用多进程,I/O 密集型任务使用协程;
- 并发是任务的组织方式,并行是任务的执行方式。
总结要点:
并发编程的核心是利用任务切换机制,提高计算机系统在多任务场景下的整体效率。