之前在写繁体字转简体字的时候,由于数据量比较大,所以用了多进程来实现。其实我对多进程/多线程的认识只是了解概念,第一次看到实际的应用是在BDCI-OCR的项目中,作者用多进程进行图像处理。毫无疑问,并行计算能显著地减少运行时间。
那么为什么用多进程实现并行计算(多核任务),不用多线程呢?
在Python中用多进程实现多核任务的原因
因为Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
GIL是Python解释器设计的历史遗留问题,通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
所以,在Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。
不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
多进程示例:
网上有很多实现多进程的示例,我只记录自己用过的。
from multiprocessing import Pool, cpu_count
import os, time, random
def long_time_task(name):
print('执行任务%s (%s)...' % (name, os.getpid()))
start = time.time()
time.sleep(random.random() * 3)
end = time.time()
print('任务 %s 运行了 %0.2f seconds.' % (name, (end - start)))
if __name__ == '__main__':
print('父进程 %s.' % os.getpid())
pool_num = cpu_count() # 获取当前CPU最大核数
pool = Pool(pool_num)
for i in range(10):
pool.apply_async(func=long_time_task, args=(i,)) # 异步非阻塞
print('等待所有子进程结束...')
pool.close()
pool.join()
print('所有子进程均结束')
这里我用的是pool.apply_async(),是异步非阻塞的方法,可以理解为:不用等待当前进程执行完毕,随时根据系统调度来进行进程切换。当然,还有其他方法,网上有很多资料,我就不赘述了。
运行结果:
父进程 17416.
等待所有子进程结束...
执行任务0 (25952)...
执行任务1 (24756)...
执行任务2 (30032)...
执行任务3 (22148)...
执行任务4 (7252)...
执行任务5 (10828)...
执行任务6 (14448)...
执行任务7 (24564)...
任务 2 运行了 0.17 seconds.
执行任务8 (30032)...
任务 5 运行了 0.27 seconds.
执行任务9 (10828)...
任务 9 运行了 0.24 seconds.
任务 7 运行了 1.08 seconds.
任务 1 运行了 1.61 seconds.
任务 4 运行了 1.70 seconds.
任务 8 运行了 2.01 seconds.
任务 3 运行了 2.50 seconds.
任务 0 运行了 3.01 seconds.
任务 6 运行了 2.91 seconds.
所有子进程均结束
从运行结果中可以发现:因为cpu最大核心数是8,所以前8个任务的进程id都不一样,任务9的进程id与任务2的相同,即任务2执行结束后再执行任务9,依此类推。
进度条示例:
模拟的事件:共需处理10个任务,每个任务执行时间为5秒(5 * time.sleep(1))
from multiprocessing import Pool, cpu_count
import os, time, random
from tqdm import tqdm
class MyMultiprocess(object):
def __init__(self, process_num):
self.pool = Pool(processes=process_num)
def work(self, func, args):
for arg in args:
self.pool.apply_async(func, (arg,))
self.pool.close()
self.pool.join()
def func(num):
name = num
for i in tqdm(range(5), ncols=80, desc='执行任务' + str(name) + ' pid:' + str(os.getpid())):
# time.sleep(random.random() * 3)
time.sleep(1)
if __name__ == "__main__":
print('父进程 %s.' % os.getpid())
mymultiprocess = MyMultiprocess(cpu_count())
start = time.time()
mymultiprocess.work(func=func, args=range(10))
end = time.time()
print("\n应用多进程耗时: %0.2f seconds" % (end - start))
start = time.time()
for i in range(10):
func(i)
end = time.time()
print("\n不用多进程耗时: %0.2f seconds" % (end - start))
运行结果:
父进程 20412.
执行任务0 pid:16144: 100%|██████████| 5/5 [00:05<00:00, 1.01s/it]
执行任务1 pid:24464: 100%|██████████| 5/5 [00:05<00:00, 1.01s/it]
执行任务2 pid:17732: 100%|██████████| 5/5 [00:05<00:00, 1.01s/it]
执行任务4 pid:11136: 100%|██████████| 5/5 [00:05<00:00, 1.01s/it]
执行任务5 pid:27844: 100%|██████████| 5/5 [00:05<00:00, 1.01s/it]
执行任务3 pid:17288: 100%|██████████| 5/5 [00:05<00:00, 1.01s/it]
执行任务6 pid:26504: 100%|██████████| 5/5 [00:05<00:00, 1.01s/it]
执行任务7 pid:28256: 100%|██████████| 5/5 [00:05<00:00, 1.01s/it]
执行任务8 pid:16144: 100%|██████████| 5/5 [00:05<00:00, 1.01s/it]
执行任务9 pid:24464: 100%|██████████| 5/5 [00:05<00:00, 1.01s/it]
应用多进程耗时: 11.59 seconds
执行任务0 pid:20412: 100%|██████████| 5/5 [00:05<00:00, 1.01s/it]
执行任务1 pid:20412: 100%|██████████| 5/5 [00:05<00:00, 1.01s/it]
执行任务2 pid:20412: 100%|██████████| 5/5 [00:05<00:00, 1.01s/it]
执行任务3 pid:20412: 100%|██████████| 5/5 [00:05<00:00, 1.01s/it]
执行任务4 pid:20412: 100%|██████████| 5/5 [00:05<00:00, 1.01s/it]
执行任务5 pid:20412: 100%|██████████| 5/5 [00:05<00:00, 1.01s/it]
执行任务6 pid:20412: 100%|██████████| 5/5 [00:05<00:00, 1.01s/it]
执行任务7 pid:20412: 100%|██████████| 5/5 [00:05<00:00, 1.01s/it]
执行任务8 pid:20412: 100%|██████████| 5/5 [00:05<00:00, 1.01s/it]
执行任务9 pid:20412: 100%|██████████| 5/5 [00:05<00:00, 1.01s/it]
不用多进程耗时: 50.58 seconds
发现:因为我的cpu是8核,所以10个任务的多进程耗时约为2×单任务耗时。
思考
在查阅相关资料时发现,多进程在实际使用的时候有单参数和多参数之分,那么多参数和单参数的优缺点分别是什么呢?
网友评论