很多初学者学Python,一碰到多线程和多进程就头大。我也经历过这个阶段,总觉得这两个东西差不多,用起来又老出问题。今天我用最直白的话,帮你把这事彻底理清。
先说核心区别。多线程是在一个程序里同时干几件事,多进程是同时开几个程序干活。举个例子,你一个人在厨房做饭,这就是一个程序。你想一边烧水一边切菜,这叫多线程。你要是喊来另一个人,他负责切菜你负责烧水,这叫多进程。
Python的多线程有个大坑,叫GIL锁。这个东西让很多新手吃了亏。简单说,GIL是一把锁,它规定同一时刻只能有一个线程在工作。那你可能会问,多线程还有什么用?对于I/O密集型任务,也就是读写文件、网络请求这类需要等待的操作,多线程确实能提高效率。因为一个线程在等数据的时候,GIL会释放,另一个线程就可以干活。但对于CPU密集型任务,比如循环计算、图像处理,多线程反而比单线程慢。
多进程就不一样了。每个进程都有自己的GIL,没有那个锁的限制。多进程可以真正利用多核CPU,适合处理CPU密集型任务。但代价也明显,进程之间的通信比线程复杂,内存开销也大很多。
怎么选?我跟你讲个真实例子。我有个朋友写爬虫,用多线程抓网页。程序跑起来,看起来在并发,实际上大部分时间都在等网络响应。这种情况下多线程很合适,代码简单,效率也高。后来他处理抓到的数据,要做大量计算。改用多进程后,速度直接翻了好几倍。
具体怎么用,看代码最直接。
import threading import time
def working(name):
print(f'{name}开始工作')
time.sleep(2)
print(f'{name}工作结束')
thread1 = threading.Thread(target=working, args=('线程1',))
thread2 = threading.Thread(target=working, args=('线程2',))
thread1.start()
thread2.start()
thread1.join()
thread2.join()
这是最基本的多线程写法。启动两个线程,让他们同时干活。注意那个sleep,模拟的是等待操作。如果是计算任务,你改成循环算一千遍乘法,两个线程加一起的速度反而可能不如一个线程。
多进程的写法也类似,只是换成Process对象。
from multiprocessing import Process import os
def计算任务(n):
result = sum(ii for i in range(n))
print(f'进程{os.getpid()}计算结果:{result}')
if __name__ == '__main__':
p1 = Process(target=计算任务, args=(10000000,))
p2 = Process(target=计算任务, args=(10000000,))
p1.start()
p2.start()
p1.join()
p2.join()
这段代码里,两个进程分别算自己的,互不干扰。你可以试试在任务管理器里看,CPU占用会明显高起来。
还有一个常见误解,以为多线程能随便共享变量。线程之间确实能共享全局变量,但容易出现数据混乱。比如两个线程同时给同一个变量加1,结果可能只加了1次。解决办法是用锁,但用锁又会降低并发性能。多进程没有这个问题,因为进程之间内存是隔离的,想共享数据需要用Queue或者Pipe。
我见过最典型的错误是,新手在循环里创建几千个线程。每个线程就干一点小事,结果创建线程的开销比干活本身还大。正确的做法是用线程池或进程池,控制并发数量。
from concurrent.futures import ThreadPoolExecutor def下载文件(url):
print(f'正在下载{url}')
return '完成'
urls = ['url1', 'url2', 'url3']
with ThreadPoolExecutor(max_workers=3) as executor:
results = list(executor.map(下载文件, urls))
看到没,max_workers设成3,最多就3个线程同时运行。这样既不会浪费资源,也不会出问题。
最后说个实在的建议。如果你不确定该用哪个,先想想你的程序主要在等什么。等I/O就选多线程,等CPU就算就选多进程。实在拿不准,用多进程准没错。虽然代码写起来复杂点,但不容易掉坑里。
我刚开始也把这两个混着用,出过不少bug。后来想明白了,其实就是一句话的事:多线程适合等,多进程适合算。记住这句话,以后看见别人写代码,你一眼就能看出他有没有用对。