第九章节的日志部分,已经涉及到了多线程和多进程的概念,所以在此章节单独做一个总结。
主要内容参考:
Python 多线程与多进程详解:高效并发编程实践-CSDN博客
链接中的文章已经讲的比较清晰,作者转载到此供自我学习使用。
基础知识
在 Python 开发中,并发编程 是提高程序执行效率的重要手段。对于 I/O 密集型任务,如文件处理、网络爬虫、数据库操作,可以使用多线程(threading) 或 异步编程(asyncio)。对于 CPU 密集型任务,可以使用多进程(multiprocessing),充分利用多核 CPU;
一、线程 vs 进程
特性 | 线程(Thread) | 进程(Process) |
---|---|---|
适用场景 | I/O 密集型任务(如爬虫、文件 I/O) | CPU 密集型任务(如科学计算、图像处理) |
Python GIL 限制 | 受限(无法利用多核 CPU) | 不受限(可以利用多核 CPU) |
内存共享 | 共享内存(开销小) | 独立内存(开销大) |
启动速度 | 快 | 慢 |
数据同步 | 需要锁(Lock) | 无需锁 |
结论:
- I/O 密集型任务(爬虫、网络请求):推荐使用多线程 或 协程(asyncio)
- CPU 密集型任务(科学计算、数据处理):推荐使用多进程(multiprocessing)--需要使用多核CPU场景。
二、Python 多线程(Threading)
2.1 线程的基本使用
import threading
import time
def task(name):
print(f"线程 {name} 开始执行")
time.sleep(2)
print(f"线程 {name} 结束")
# 创建线程
thread1 = threading.Thread(target=task, args=("A",))
thread2 = threading.Thread(target=task, args=("B",))
# 启动线程
thread1.start()
thread2.start()
# 等待所有线程完成
thread1.join()
thread2.join()
print("所有线程执行完毕")
输出:
线程 A 开始执行
线程 B 开始执行
线程 A 结束
线程 B 结束
所有线程执行完毕
解释
-
Thread(target=task, args=(...))
创建新线程 -
start()
启动线程 -
join()
等待线程执行完毕
2.2 线程池 ThreadPoolExecutor
对于大量任务,使用 ThreadPoolExecutor
管理线程池比手动创建 Thread
更高效:
import concurrent.futures
import time
def task(name):
print(f"线程 {name} 开始")
time.sleep(2)
print(f"线程 {name} 结束")
# 使用线程池
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
executor.map(task, ["A", "B", "C", "D"])
优势
- 自动管理线程,避免手动创建
Thread
- 适用于并发 I/O 任务,如爬虫、数据库查询
2.3 线程同步
Lock 互斥锁
多个线程同时修改同一个变量时,可能发生 竞态条件(Race Condition),导致数据不一致:
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100000):
with lock: # 使用锁防止数据竞争
counter += 1
threads = [threading.Thread(target=increment) for _ in range(3)]
for t in threads: t.start()
for t in threads: t.join()
print("最终计数值:", counter)
为什么需要锁(Lock)?
- Python 的 GIL(全局解释器锁) 允许多个线程执行,但 不能同时修改共享变量
with lock:
确保一次只有一个线程修改counter
消息队列
消息队列是一种更安全的线程通信方式。Python 的 queue 模块提供了线程安全的队列,可以在多个线程之间安全地交换数据。
import threading
from queue import Queue
import time
from concurrent.futures import ThreadPoolExecutor
def get_detail_html(detail_url_list, id):
while True:
url = detail_url_list.get()
#设置退出机制
if url is None:
break
time.sleep(2)
print(f"thread {id}: get {url} detail finished")
print(f"html thread {id} end")
def get_detail_url(queue):
for i in range(10):
time.sleep(1)
queue.put(f"url: {i}")
print(f"get detail url {i} end")
#设置退出机制 不然线程内部queue读取循环无法结束
#写入数量>=读取线程数
for i in range(10):
queue.put(None)
print("url thread end")
if __name__ == "__main__":
detail_url_queue = Queue(maxsize=1000)
start_time = time.time()
#独立thread模式
'''
html_thread = []
for i in range(3):
thread2 = threading.Thread(target=get_detail_html, args=(detail_url_queue, i))
html_thread.append(thread2)
#启动所有queue的读取线程
for i in range(3):
html_thread[i].start()
'''
# 线程池模式 修改重点!!!!!!!
future_list = []
executor = ThreadPoolExecutor(max_workers=5)
for i in range(5):
future = executor.submit(get_detail_html, detail_url_queue,i)
future_list.append(future)
#启动queue的写入线程
thread = threading.Thread(target=get_detail_url, args=(detail_url_queue,))
thread.start()
#等待所有线程结束
thread.join()
for f in future_list:
f.result()
executor.shutdown()
#独立thread模式
'''
for i in range(3):
html_thread[i].join()
'''
print(f"last time: {time.time() - start_time} s")
三、Python 多进程(Multiprocessing)
3.1 多进程的基本使用
import multiprocessing
import os
def worker(name):
print(f"进程 {name} (PID={os.getpid()}) 开始执行")
return name
if __name__ == "__main__":
process1 = multiprocessing.Process(target=worker, args=("A",))
process2 = multiprocessing.Process(target=worker, args=("B",))
process1.start()
process2.start()
process1.join()
process2.join()
print("所有进程执行完毕")
优势
- 每个进程独立运行,不受 GIL 限制
- 适用于 CPU 密集型任务,如大规模计算、图像处理
3.2 进程池 ProcessPoolExecutor
import concurrent.futures
import os
def task(name):
print(f"进程 {name} (PID={os.getpid()}) 开始执行")
return name
if __name__ == "__main__":
with concurrent.futures.ProcessPoolExecutor(max_workers=3) as executor:
results = executor.map(task, ["A", "B", "C", "D"])
适用场景
- CPU 密集型任务
- 提高计算任务的并行性
3.3 进程间通信
3.3.1 共享内存
使用 multiprocessing.Value 可以创建进程间共享的变量,下面是一个例子,创建了一个类型为整数(‘i’)的共享内存变量 value
import multiprocessing
def func(value):
print("read value %d" % value.value)
if __name__ == '__main__':
value = multiprocessing.Value('i', 1000)
processes = [multiprocessing.Process(target=func, args=(value,)) for _ in range(10)]
for process in processes:
process.start()
for process in processes:
process.join()
print(value.value) # 输出 10
3.3.2 管道
使用 multiprocessing.Pipe 可以创建一个管道,两个进程可以通过这个管道互相传递数据,下面是一个例子,创建了一个管道,其中 parent_conn 是父进程持有的端口,child_conn 是子进程持有的端口。然后启动两个进程,分别调用 sender 和 receiver 函数。sender 函数发送一条消息到管道中,receiver 函数从管道中接收消息并打印出来:
import multiprocessing
def sender(conn):
conn.send('Hello, receiver')
def receiver(conn):
message = conn.recv()
print(message)
if __name__ == '__main__':
parent_conn, child_conn = multiprocessing.Pipe()
p1 = multiprocessing.Process(target=sender, args=(parent_conn,))
p2 = multiprocessing.Process(target=receiver, args=(child_conn,))
p1.start()
p2.start()
p1.join()
p2.join()
3.3.3 通过 Queue
共享数据
import multiprocessing
def worker(queue):
queue.put("任务完成")
if __name__ == "__main__":
q = multiprocessing.Queue()
process = multiprocessing.Process(target=worker, args=(q,))
process.start()
process.join()
print("消息:", q.get()) # 读取子进程发送的消息
四、协程(AsyncIO)适用于 I/O 密集型任务
对于 网络请求、文件 I/O、数据库操作,asyncio
提供更高效的异步处理。
4.1 asyncio
基本使用
import asyncio
async def async_task(name):
print(f"任务 {name} 开始")
await asyncio.sleep(2) # 模拟异步 I/O
print(f"任务 {name} 结束")
async def main():
tasks = [async_task("A"), async_task("B")]
await asyncio.gather(*tasks)
asyncio.run(main())
优势
- 非阻塞执行
- 适用于网络爬虫、API 请求、数据库操作
五、总结
方案 | 适用场景 | 主要特性 |
---|---|---|
多线程(threading) | I/O 密集型(爬虫、文件 I/O) | 共享内存,受 GIL 限制 |
线程池(ThreadPoolExecutor) | 并发 I/O 任务 | 自动管理线程 |
多进程(multiprocessing) | CPU 密集型任务 | 多核计算,不受 GIL 限制 |
进程池(ProcessPoolExecutor) | 大规模计算任务 | 自动管理进程 |
协程(asyncio) | I/O 高并发(API、爬虫) | 非阻塞执行 |
如何选择?
- CPU 密集型任务(科学计算、数据分析) → 多进程
- I/O 密集型任务(爬虫、数据库查询) → 多线程 或 asyncio
- 高并发任务(网络请求、日志处理) → asyncio