目录
[multiprocessing 跨平台多进程(推荐)](#multiprocessing 跨平台多进程(推荐))
[进程间通信 Queue](#进程间通信 Queue)
[线程锁 Lock](#线程锁 Lock)
[进程 VS 线程](#进程 VS 线程)
前言
现代操作系统都支持多任务。进程、线程是操作系统实现多任务的基础单位。
- 进程:操作系统资源分配的最小单位
- 线程:进程内调度执行的最小单位 一个进程至少包含一个主线程;同一个进程内的多个线程共享该进程的内存资源。 Python中提供
multiprocessing实现多进程、threading实现多线程,同时有GIL全局解释器锁这个重要限制。
多进程
Python的os.fork()在Linux/macOS可用,Windows不支持,跨平台推荐使用multiprocessing模块。
fork(仅Linux/macOS)
调用fork()时,操作系统把当前进程复制一份,分出子进程:
-
返回0:代表子进程
-
返回子进程PID:代表父进程
import os
pid = os.fork()
if pid == 0:
print(f"我是子进程,pid={os.getpid()}, 父进程pid={os.getppid()}")
else:
print(f"我是父进程,pid={os.getpid()}, 创建子进程pid={pid}")
multiprocessing 跨平台多进程(推荐)
from multiprocessing import Process
import os
def run_proc(name):
print(f'子进程 {name} pid={os.getpid()}, 父pid={os.getppid()}')
if __name__ == '__main__':
print(f'主进程pid={os.getpid()}')
p = Process(target=run_proc, args=('test',))
print('准备启动子进程')
p.start()
p.join() # 等待子进程执行完毕,主进程再往下走
print('子进程结束')
start():启动进程join():阻塞等待子进程结束,常用于同步 Windows必须写if __name__ == '__main__',防止递归创建子进程
Pool进程池
批量创建大量子进程,复用进程,减少创建销毁开销:
from multiprocessing import Pool
import os, time
def long_task(x):
print(f'执行任务 {x}, pid={os.getpid()}')
time.sleep(1)
return x*x
if __name__ == '__main__':
p = Pool(4) # 最多同时4个进程
res = p.map(long_task, range(5))
print(res)
p.close()
p.join()
进程间通信 Queue
进程之间内存隔离,不能直接共享变量,用Queue传递消息:
from multiprocessing import Process, Queue
def write(q):
for val in ['A','B','C']:
q.put(val)
def read(q):
while True:
v = q.get()
print('读到:', v)
if __name__ == '__main__':
q = Queue()
pw = Process(target=write, args=(q,))
pr = Process(target=read, args=(q,))
pw.start()
pr.start()
pw.join()
pr.terminate()
多线程
Python线程由threading模块封装,底层调用操作系统原生线程。 线程共享当前进程的内存空间,所以全局变量可以在线程之间直接访问,容易出现竞争,需要加锁。
基础线程示例
import threading, time
def loop():
print(f'子线程 {threading.current_thread().name} 启动')
time.sleep(1)
print(f'子线程 {threading.current_thread().name} 结束')
print(f'主线程 {threading.current_thread().name}')
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print('主线程结束')
线程锁 Lock
多个线程修改同一个共享变量,会出现数据错乱(竞态条件),需要互斥锁:
import threading
balance = 0
lock = threading.Lock()
def change(n):
global balance
lock.acquire()
try:
balance += n
finally:
lock.release() # 一定要释放锁,防止死锁
t1 = threading.Thread(target=change, args=(5,))
t2 = threading.Thread(target=change, args=(-5,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)
注意:锁会让并发变成串行,降低速度;不要持有锁太久,避免死锁。
GIL全局解释器锁
CPython存在GIL:同一时刻,一个进程内只有一个线程在执行Python字节码。 所以:
- CPU密集型任务:多线程无法利用多核,效率差,优先用多进程
- IO密集型任务(网络、文件读写、sleep):多线程适合,IO阻塞时释放GIL,并发效果好
ThreadLocal
同一个进程内线程共享变量,加锁麻烦;ThreadLocal用来保存线程独有的局部变量,每个线程读写自己那一份副本,互不干扰,不用加锁。
import threading
local_school = threading.local()
def process_student():
std = local_school.student
print(f'线程 {threading.current_thread().name} 中的学生:{std}')
def process_thread(name):
local_school.student = name
process_student()
t1 = threading.Thread(target=process_thread, args=('小明',), name='Thread-A')
t2 = threading.Thread(target=process_thread, args=('小红',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()
简单理解:ThreadLocal像一个字典,key自动绑定当前线程,每个线程只能读到自己存的数据。
进程 VS 线程
| 对比项 | 进程 | 线程 |
|---|---|---|
| 资源 | 独立内存空间,资源隔离 | 共享所属进程内存,资源共享 |
| 开销 | 创建、切换开销大 | 轻量,切换开销小 |
| 通信 | 隔离,必须Queue、Pipe等通信 | 直接读写共享变量(需要锁) |
| 稳定性 | 一个进程崩溃不影响其他进程 | 一个线程崩溃会导致整个进程崩溃 |
| 多核利用 | 不受GIL限制,可以充分利用多核CPU | CPython受GIL限制,CPU密集无法并行 |
选型口诀:
- CPU密集型计算:优先多进程,利用多核
- IO密集(网络请求、文件读写):优先多线程/协程,开销更小
分布式进程
multiprocessing.managers支持把Queue放到网络上,实现跨机器的分布式进程。 一台机器作为服务端,把队列注册到网络;其他机器作为客户端连接这个队列,收发任务,实现分布式任务调度。
服务端代码(任务分发):
from multiprocessing.managers import BaseManager
task_queue = Queue()
result_queue = Queue()
class QueueManager(BaseManager):
pass
QueueManager.register('get_task_queue', callable=lambda: task_queue)
QueueManager.register('get_result_queue', callable=lambda: result_queue)
manager = QueueManager(address=('', 5000), authkey=b'abc')
manager.start()
task = manager.get_task_queue()
result = manager.get_result_queue()
# 放任务
for i in range(5):
task.put(i)
# 接收结果
for _ in range(5):
print('结果:', result.get())
manager.shutdown()
客户端代码(工作节点):
from multiprocessing.managers import BaseManager
class QueueManager(BaseManager):
pass
QueueManager.register('get_task_queue')
QueueManager.register('get_result_queue')
server_addr = '127.0.0.1'
m = QueueManager(address=(server_addr, 5000), authkey=b'abc')
m.connect()
task = m.get_task_queue()
result = m.get_result_queue()
for _ in range(5):
n = task.get()
print(f'计算 {n}*{n}')
result.put(n*n)
注意:分布式进程底层走网络,适合简单分布式任务;生产一般用Celery这类成熟任务框架。
小结
- 多进程:
multiprocessing跨平台,进程间隔离,Queue通信,适合CPU密集; - 多线程:
threading,共享内存,修改共享变量必须加Lock,适合IO密集; - ThreadLocal:存放线程私有数据,免除锁的麻烦;
- GIL是CPython的特点,是选择进程/线程的核心依据;
- 分布式进程:借助
managers把队列暴露到网络,实现跨主机任务分发。