文章目录
1.任务需求
在参加2022年3月华为软件精英挑战赛过程中,因为第一版算法比较暴力,耗时过久,当时希望借助多线程、多进程来加快算法的执行速度,在真实使用过程中有了很多基础但重要的理解和体会,总结整理如下,希望以后能够进一步合理使用多线程和多进程。本文的总结参考多篇文章,文中也会给出不少来自网络的代码,以便加深实操印象和理解。
2.基本概念
2.1 并发
指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
2.2 并行
指的是任务数小于等于cpu核数,即任务真的是一起执行的。
2.3 线程
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
2.4 进程
进程是系统进行资源分配和调度的一个独立单位。
3.基本实现
目前python3的内置模块中有threading 、multiprocessing可以实现多线程和多进程操作。
经过调研发现,如果涉及线程池和进程池,除这两种工作之外,还有一个模块可供使用(这个模块是在threading 、multiprocessing基础上进一步实现的)
【from concurrent.futures import ProcessPoolExecutor ,from concurrent.futures import ThreadPoolExecutor
3.1.多线程
多线程在python中有内置模块threading
(1)基本实现
# coding=utf-8
import threading
import time
def fun(Tid):
print(Tid)
time.sleep(3)
if __name__ == "__main__":
start_time = time.time()
for i in range(5):
t = threading.Thread(target=fun, args=(i,))
t.start() # 启动线程,即让线程开始执行
print(time.time() - start_time)
输出:
0
1
2
3
4
0.0
点评:基本的实现就是上文,但是会存在一个比较大的问题,大家看结果就可以发现最后打印的时间不是你真正想统计的时间,也就是说线程和print几乎同步执行了,那么如何满足你等待所有线程执行结束的需求呢?
(2)基本实现的一点完善
if __name__ == "__main__":
start_time = time.time()
t_pools = []
for i in range(5):
t = threading.Thread(target=fun, args=(i,))
t_pools.append(t)
t.start() # 启动线程,即让线程开始执行
for i in range(5):
t_pools[i].join()
print(time.time() - start_time)
输出
0
1
2
3
4
3.0104055404663086
点评:那就是需要在for循环让大家都启动后,再用一个for循环,利用t.join()指令通知他们等待线程结束。注意这里一定要先等for循环start所有线程后再通知他们等待,如果只用一个for循环,那么你就会发现多线程并发变成了单线程,因为他每一次都在等待上一次结束,这就变成了串行。
3.2 多进程
多进程在python中有内置模块multiprocessing
(1)基本实现
# -*- coding:utf-8 -*-
from multiprocessing import Process
import os
import time
def run_proc():
"""子进程要执行的代码"""
print('子进程运行中,pid=%d...' % os.getpid()) # os.getpid获取当前进程的进程号
print('子进程将要结束...')
if __name__ == '__main__':
print('父进程pid: %d' % os.getpid()) # os.getpid获取当前进程的进程号
p = Process(target=run_proc)
p.start()
点评:基本实现方式和逻辑与线程类似,这里不做重复
3.3 线程池
在需要大量线程和进程的时候,不太方便像前文一个一个写线程或者进程,那么可以通过线程池或者进程池的方式,来进行线程和进程的管理和使用,个人认为使用线程池和进程池,代码的逻辑性和可读性也会好一些。
网上有大量的直接的线程池的写法,但是进程池和线程池写法一致,本文反其道而行之,使用进程池作为案例记录。
3.4 进程池
实现方法1
利用内置的multiprocessing模块
import os
import time
from multiprocessing import Pool
def work(n):
print('%s run' %os.getpid())
time.sleep(3)
return n**2
if __name__ == '__main__':
start_time = time.time()
p=Pool(3) #进程池中从无到有创建三个进程,以后一直是这三个进程在执行任务
res_l=[]
for i in range(10):
res=p.apply_async(work,args=(i,)) # 异步运行,根据进程池中有的进程数,每次最多3个子进程在异步执行
# 返回结果之后,将结果放入列表,归还进程,之后再执行新的任务
# 需要注意的是,进程池中的三个进程不会同时开启或者同时结束
# 而是执行完一个就释放一个进程,这个进程就去接收新的任务。
res_l.append(res)
# 异步apply_async用法:如果使用异步提交的任务,主进程需要使用jion,等待进程池内任务都处理完,然后可以用get收集结果
# 否则,主进程结束,进程池可能还没来得及执行,也就跟着一起结束了
p.close()
p.join()
for res in res_l:
print(res.get()) #使用get来获取apply_aync的结果,如果是apply,则没有get方法,因为apply是同步执行,立刻获取结果,也根本无需get
print(time.time()-start_time)
实现方法2
利用内置的from concurrent.futures import ProcessPoolExecutor模块
from concurrent.futures import ProcessPoolExecutor
import time,os
def task(n):
(n1,n2) = n
print('%s is running'% os.getpid())
time.sleep(3)
print(n2)
return n1
def handle(res):
res=res.result()
print("handle res %s"%res)
if __name__ == '__main__':
start_time = time.time()
#同步调用
# pool=ProcessPoolExecutor(8)
#
# for i in range(13):
# pool.submit(task, i).result() #变成同步调用,串行了,等待结果
# # pool.shutdown(wait=True) #关门等待所有进程完成
# pool.shutdown(wait=False)#默认wait就等于True
# # pool.submit(task,3333) #shutdown后不能使用submit命令
#
# print('主')
#异步调用
pool=ProcessPoolExecutor(8)
for i in range(13):
obj=pool.submit(task,[i,1])
obj.add_done_callback(handle) #这里用到了回调函数
pool.shutdown(wait=True) #关门等待所有进程完成
print(time.time()-start_time)
##注意,创建进程池必须在if __name__ == '__main__':中,否则会报错
##其他的用法和创建线程池的一样
理解的基础上两者的实现的调用差别不大,但是使用进程池实现优雅高效,值得掌握。
4.进程间通信
进程间通信方式比较多,但是因为相当于进行IO操作,进程间通信比较慢,我实现和熟悉的是下面三种,记录备忘
1.使用队列(不适合传递大数,可以传递一些简单的Flag)
http://c.biancheng.net/view/2635.html
关键:q.put() q.get()
2.使用管道(可以传递大数,但是速度比较慢)
import numpy as np
from multiprocessing import Process, Pipe
def fun1(conn):
a = np.ones((1000,1000,30))*3000
print('子进程发送消息:')
conn.send(a)
print('子进程接受消息:')
print(conn.recv())
# conn.close()
if __name__ == '__main__':
conn1, conn2 = Pipe() #关键点,pipe实例化生成一个双向管
p = Process(target=fun1, args=(conn2,)) #conn2传给子进程
p.start()
print('主进程接受消息:')
print(conn1.recv())
print('主进程发送消息:')
conn1.send("你好子进程")
p.join()
print('结束测试')
3.使用进程自带的.get()
进程函数需要设置return
5.经验和反思
5.1 本文局限
1.上述实现对于线程没有考虑线程对于共享变量的安全性:技术栈有同步、互斥锁、银行家算法等;
2.守护线程参考文章
5.2 使用场合
更重要的一点感悟
以前对于线程和进程,我只知道可以加速,却没有有考虑锅使用场合的问题,在这次比赛中吃了大亏。
(1)对于计算密集型的子程序来说,采用多线程根本没有加速的效果,因为线程的本质就是抽空干活,而计算密集型的子程序使得cpu没有抽空干活的机会,那么它又如何给你加速的机会呢,谨记谨记!
(2)进程间进行数据的交互会产生额外的I/O开销,这个也是个大问题,在比赛中进程间通信严重堵塞程序,甚至不如直接写入写出文件进行交互。
两者分别在何种场景下应用?
- 如果你的程序有大量与数据交互/网络交互,可以使用多线程,因为程序时间瓶颈不在于GIL而是在I/O,这时多线程的小开销就比多进程更实用。
- 如果你的程序有图形界面GUI,使用多线程,GIL锁会帮助你让你的UI线程不会产生死锁等问题。
- 如果你的程序的运行效率与CPU密切相关的,即瓶颈在于计算等情况,且有多核可用时,就可以考虑用多进程提高效率。
5.3 多进程假死问题
python多进程间用Queue通信时,如果子进程操作Queue满了或者内容比较大的情况下,该子进程会阻塞等待取走Queue内容(如果Queue数据量比较少,不会等待),如果调用join,主进程将处于等待,等待子进程结束,造成死锁
解决方式:在调用join前,及时把Queue的数据取出,而且Queue.get需要在join前 参考文章
5.4 传入进程池的目标函数不报错
多进程代码中调试中代码出错也不报错,简直要命,程序乱飞。
def apply_async(self, func, args=(), kwds={}, callback=None,
error_callback=None):
'''
Asynchronous version of `apply()` method.
'''
if self._state != RUN:
raise ValueError("Pool not running")
result = ApplyResult(self._cache, callback, error_callback)
self._taskqueue.put(([(result._job, 0, func, args, kwds)], None))
return result
解决方法:利用 error_callback 查看报错信息 参考文章