多进程
因为线程无法调用多核,其相当于多个线程在单核下来回切换,所以有时候为了实现多核应用,比如线程不适合执行的计算操作密集的任务,就可以通过多进程来提高效率。当然要注意的是,虽然线程间可以互相通信,但是因为进程间不能互相访问,所以不同进程的线程间也是无法互相通信的,能通信的只有在同一个进程下的多个线程
创建进程
进程的语法几乎完全兼容线程语法,就修改了一些小地方,比如创建进程时是通过Process
来创建,其他大部分操作都是相似的,还有就是在windows下创建进程的代码必须在if __name__ == '__main__':
里编写,举例
import multiprocessing
import time
def run():
while True:
print("This is process")
time.sleep(2)
if __name__ == '__main__':
multiprocessing.freeze_support()
# 防止多进程启动报错
t = multiprocessing.Process(target=run) # 就这里有修改
t.start() # 新进程
创建进程机制
windows创建机制
子进程开辟新的内存后,内存里没有之前的数据,为了能够执行内容,会将父进程的文件重新import
一遍,此时就有需要的内容了。但是在import
的时候,如果父进程文件里的开启新进程代码没有在__main__
下,那么就会又执行一遍,这样就会导致无限创建进程,所以windows下需要在if __name__ == "__main__"
下使用
linux创建机制
直接把父进程的内存空间(值一样,但是地址是不一样的)复制一份,底层通过os.fork()
完成,因此不需要将创建进程的代码放在__main__
下
linux下创建多进程示例
import os
import time
pid = os.fork()
# os.fork只能linux运行
print("create...", pid, ", parent:", os.getpid())
time.sleep(2)
# create... 93762 , parent: 93761
# create... 0 , parent: 93762
结果可以看出主进程和新创建的进程都执行了os.fork()
之后的语句,原因是子进程会从创建进程的地方开始,将和主进程一样的数据、代码运行都拷贝一份并执行,然后这些拷贝的内容和主进程隔离开,所以os.fork()
后面的内容会执行两次
区分父/子进程
os.fork()
会返回创建的子进程的pid
,因为子进程没有创建子进程,所以os.fork()
的返回值为0
,因此我们可以通过该方式来对父进程和子进程进行判断,举例:
import os
import time
print("main:", os.getpid())
pid = os.fork()
if pid == 0:
print("child process:", os.getpid())
else:
print("parent process", os.getpid(), "child process:", pid)
time.sleep(2)
# main: 93961
# parent process 93961 child process: 93962
# child process: 93962
进程资源回收
前面的示例中,都在最后加上了sleep
语句,使得主进程尽量在子进程执行完毕后才结束,如果将主进程的sleep
删掉,使得主进程提前结束,将可能会导致子进程的资源回收出现问题,举例:
import os
import time
print("main:", os.getpid())
pid = os.fork()
if pid == 0:
print("child process:", os.getpid())
time.sleep(1)
print("child process end...")
else:
print("parent process", os.getpid(), "child process:", pid)
print("main end...")
# main: 93987
# parent process 93987 child process: 93988
# child process: 93988
# main end...
# root@ubuntu:~$ child process end...
结果会发现子进程没有正常结束退出,这是因为子进程需要父进程来回收资源,但这里父进程在子进程执行完之前就结束了,从而导致子进程资源无法被回收
多线程/多进程对比
CPU密集的操作,用多进程,因为能够真正使用到多个CPU;而IO密集的操作,使用多线程,因为CPU有很大一部分时间处于空闲,无须用到多核,而且进程的切换代价大于线程,因此此时多线程的结果甚至可能优于多进程
- CPU密集操作示例:
import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, wait
def fab(n):
if n < 2:
return 1
return fab(n - 1) + fab(n - 2)
if __name__ == '__main__':
start = time.time()
[fab(i) for i in range(25, 34)]
print("单线程耗时:{}".format(time.time() - start))
thread_pool = ThreadPoolExecutor()
start = time.time()
fabs = [thread_pool.submit(fab, i) for i in range(25, 34)]
wait(fabs)
print("多线程耗时:{}".format(time.time() - start))
process_pool = ProcessPoolExecutor()
start = time.time()
fabs = [process_pool.submit(fab, i) for i in range(25, 34)]
wait(fabs)
print("多进程耗时:{}".format(time.time() - start))
# 单线程耗时:4.424163579940796
# 线程耗时:4.396239280700684
# 进程耗时:2.3158042430877686
可以看出因为多进程使用到了更多的CPU参与运算,因此大大缩短了运行时间
- IO密集操作示例:
import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, wait
def iotask():
time.sleep(1)
if __name__ == '__main__':
start = time.time()
[iotask() for i in range(5)]
print("单线程耗时:{}".format(time.time() - start))
thread_pool = ThreadPoolExecutor()
start = time.time()
iotasks = [thread_pool.submit(iotask) for i in range(5)]
wait(iotasks)
print("多线程耗时:{}".format(time.time() - start))
process_pool = ProcessPoolExecutor()
start = time.time()
iotasks = [process_pool.submit(iotask) for i in range(5)]
wait(iotasks)
print("多进程耗时:{}".format(time.time() - start))
# 单线程耗时:5.00162410736084
# 多线程耗时:1.0023696422576904
# 多进程耗时:2.124408006668091
可以看出由于IO密集型操作下,CPU利用率不高,而进程的切换开销大,因此结果还不如多线程好
多线程/多进程运行区别
- 多线程就是在同一进程下新开一个线程,数据共享
- 多进程是新开一个不同于主进程的进程,数据不共享
相关API
getpid()/getppid()
获取进程ID和父进程ID,举例:
import os
print(os.getpid(), os.getppid())
# 获取进程ID、父进程ID
# 12440 11440
is_alive()
进程是否启动
terminate()
强制关闭进程
pid
进程id,需要在启动后才有,举例:
import time
import multiprocessing
def task():
time.sleep(1)
if __name__ == '__main__':
p1 = multiprocessing.Process(target=task)
print(p1.pid)
p1.start()
print(p1.pid)
# None
# 72236
ident
进程唯一标识
name
进程名
API示例
from multiprocessing import Process
import time
def fun():
print("process start...")
time.sleep(2)
if __name__ == "__main__":
process = Process(target=fun)
print(process.is_alive())
# 进程是否启动
process.start()
print(process.pid, process.ident, process.name)
# 进程id、进程唯一标识、进程名
print(process.is_alive())
time.sleep(0.1)
process.terminate()
# 强制关闭进程,需要等待一点时间
print(process.is_alive())
time.sleep(0.1)
print(process.is_alive())
# False
# 12396 12396 Process-1
# True
# True
# False
因为启动的进程在另一个程序中,因此fun输出的内容当前终端无法看到
API应用示例
启动多个任务并监听,若任务中止,则重启任务,举例:
import time
import multiprocessing
def task1():
print("start1")
time.sleep(3)
raise
print("end1")
def task2():
print("start2")
time.sleep(5)
print("end2")
def task3():
print("start3")
time.sleep(6)
print("end3")
def task4():
print("aaa")
time.sleep(1000)
# 任务列表
tasks = {
"task1": task1, "task2": task2, "task3":task3, "aaa": task4
}
# 监听任务列表
watch_tasks = {task: None for task in tasks}
def task_deco(task_name, task, *args, **kwargs):
"""监听装饰器
"""
print(f"任务{task_name}启动...")
try:
task(*args, **kwargs)
except Exception as e:
print(f"任务{task_name}异常中止,中止原因:{e}")
finally:
print(f"任务{task_name}结束,准备重启...")
def start_process(task_name, task):
"""进程启动"""
start = time.time()
p = multiprocessing.Process(target=task_deco, args=(task_name, task))
p.start()
return p
def is_process(p):
"""进程判断
"""
return hasattr(p, "is_alive") and hasattr(p, "terminate")
def handle_watch(p, task_name, task):
"""监听处理
"""
if not p.is_alive():
p = start_process(task_name, task)
watch_tasks[task_name] = p
def handle_stop():
"""脚本停止处理
"""
for p in watch_tasks.values():
if is_process(p):
p.terminate()
def loop():
"""监听循环
"""
while True:
time.sleep(1)
for task, p in watch_tasks.items():
if p is None:
continue
if is_process(p):
handle_watch(p, task, tasks[task])
def init():
"""初始化启动所有任务进程
"""
for task in tasks:
p = start_process(task, tasks[task])
watch_tasks[task] = p
def main():
try:
init()
loop()
except KeyboardInterrupt:
handle_stop()
if __name__ == '__main__':
main()
守护进程和守护线程
守护线程
- 主线程会等待子线程结束后才会结束,而守护线程随着主线程的结束而结束
- 主线程一旦结束了进程就会结束,从而回收所有的进程资源(线程也是资源)
- 如果主线程结束后还有其他子线程在运行,则守护线程也守护
守护进程
- 守护进程会随着主进程的结束而结束
- 如果主进程结束后还有其他子进程在运行,则守护进程不守护
原因:
- 守护进程需要主进程来回收资源
- 守护线程是随着进程的结束而结束
进程结束步骤
其他子线程结束 -> 主线程结束 -> 主进程结束 -> 回收整个进程中的资源包括守护线程
相关知识点
- 进程是资源分配单位
- 子进程都需要父进程来回收资源
- 线程是进程中的资源
- 所有的线程都会随着进程的结束而被回收资源
进程通信
进程队列
由于子进程在创建以后,其和父进程的内存是独立分开的,因此父进程的数据是无法和子进程进行共享的,举例:
import time
import multiprocessing
def task(li):
li.append(1)
if __name__ == '__main__':
li = []
multiprocessing.Process(target=task, args=(li, )).start()
time.sleep(1)
print(li)
# []
可以看到列表li并没有被共享使用,但multiprocessing
提供了一个Queue
,其支持进程间的通信,举例:
import time
import multiprocessing
def task(queue):
queue.put(1)
if __name__ == '__main__':
queue = multiprocessing.Queue()
multiprocessing.Process(target=task, args=(queue, )).start()
time.sleep(1)
print(queue.get())
# 1
注:
要注意的是继承于multiprocessing
中的Queue
才可以传入子进程,如果是继承于queue
的Queue
是不能传进去的,比如下面这样虽然看起来看起来二和前面的原理差不多,但因为队列的本质不同,举例:
import multiprocessing
import queue
def run(q):
print(q.get())
if __name__ == '__main__':
q = queue.Queue()
q.put("I'm in main process&thread")
t2 = multiprocessing.Process(target=run, args=(q,))
t2.start()
管道
multiprocessing
下提供了Pipe
对象,其返回管道的两端,而得到其中一端的进程允许与管道另一端的进程间互相通信,举例:
import time
import multiprocessing
def task1(pipe):
pipe.send("aaa")
print(pipe.recv())
def task2(pipe):
print(pipe.recv())
pipe.send("bbb")
if __name__ == '__main__':
p1, p2 = multiprocessing.Pipe()
# 新建个管道对象,返回管道两端,实际都一样,传入哪一端,就用另一端与之通信即可
multiprocessing.Process(target=task1, args=(p1, )).start()
multiprocessing.Process(target=task2, args=(p2, )).start()
# aaa
# bbb
Pipe
虽然只能允许两个进程间互相通信,但其有性能高的优势(Queue
虽然能允许多个进程通信,但由于内部加了很多锁来控制,因此效率相对较低)
进程共享
前面的队列和管道还无法做到真正的进程之间共享数据,所以像如果想多进程共同修改同一份数据还是做不到的,这里就有一个Manager()
对象可以实现,其支持list
、dict
、Lock
、RLock
、Event
、Queue
、Array
等内容的共享,即在新建一个Manager
对象后,调用该对象的上面那些数据结构,然后跟前面的队列和管道一样传入子进程就可以了
示例
(实现多进程间共同修改同一个列表和字典)
import multiprocessing
import os
def run(l, d, i):
l.append(os.getpid()) #各个子进程将自己进程id添加入列表
d[i] = os.getpid() #...添加入字典
# print(l)
# print(d)
if __name__ == '__main__':
manager = multiprocessing.Manager() #新建一个Manager对象
share_list = manager.list(["id"]) #生成可进程间共享的列表
share_dict = manager.dict() #...的字典
control_list = [] #存放进程组
for each in range(5):
t = multiprocessing.Process(target=run, args=(share_list, share_dict, each))
#将可共享的数据传入子进程
t.start()
control_list.append(t)
for each in control_list:
each.join() #各进程不能同时修改,所以需等待前个进程完成操作
print(share_list)
print(share_dict)
结果:
['id', 8716, 8468, 5472, 6452, 6032]
{0: 5472, 1: 8716, 2: 8468, 3: 6452, 4: 6032}
进程锁
照理来说,进程互相独立,是不会影响到对方的,但是像有些地方却还是共用的,比如屏幕输出,如果不设置锁的话,那么可能第一个用print还没输出完就到下一个输出,结果几个进程的输出夹杂在一起,所以就需要进程锁来控制。进程锁和线程锁几乎一样,不过是继承于multiprocessing
的,然后也是用Lock()
生成,用acquire()
和release()
来实现,但是导入的时候要from multiprocessing import Lock
,而且锁要在程序最开始就定义好,即在函数之前就要定义好,举例:
import multiprocessing
from multiprocessing import Lock #另外导入锁
lock = Lock() #先生成锁
def abc(i):
lock.acquire()
print(i)
lock.release()
if __name__ == '__main__':
t_s = [multiprocessing.Process(target=abc, args=(i,)) for i in range(100)]
#链表推导式创建包含一堆进程列表
for t in t_s:
t.start() #依次执行进程
进程池
因为进程的资源消耗较大,所以可以通过进程池来控制一次可以开启的进程的次数(和线程里的信号量相似),使用进程池可以使用multiprocessing.Pool
对象或者concurrent.futures
下的ProcessPoolExecutor
对象
ProcessPoolExecutor
和线程池的用法几乎一致
Pool
通过Pool()
来生成一个进程池,此时创建进程则通过Pool提供的apply
或者apply_async
方法创建,前者代表同步执行(即串行运行),后者代表异步执行(并发执行),里面有两个常用参数:func
和args
对应Process
的target
和args
,通过close
关闭,join
等待(记住这里是先close
才join
),而且在异步执行时必须要有close
和join
,否则程序会直接关闭,举例:
import time
import multiprocessing
def task(n):
time.sleep(1)
return n
if __name__ == '__main__':
pool = multiprocessing.Pool(multiprocessing.cpu_count())
# 进程数与当前设备的CPU数相等更能充分利用CPU
res = pool.apply_async(task, args=(1,))
pool.close()
# join之前需要close,从而不再接收新的任务
pool.join()
print(res.get())
同步进程示例
(串行执行进程池)
import multiprocessing
import os
import time
def run(n):
print("the message is %d\t" % n)
time.sleep(1)
print("%d is end" % n)
if __name__ == '__main__': #别忘了进程必须有这个
pool = multiprocessing.Pool(5) #运行同时存在5个进程
for each in range(10):
pool.apply(func=run, args=(each,)) #串行执行进程池
#pool.apply_async(func=run, args=(each,)) #异步执行
pool.close() #异步时必须有close和join,并且close要在join前
pool.join()
apply_async
里还有一个回调参数callback
,意思是当进程运行结束后会将返回值返回给回调参数里的函数,然后执行该函数,但这个函数不是在前面的子进程里执行的,而是在父进程里执行的
异步进程示例
(异步执行进程,并执行回调函数)
def run(n):
print("the message is %d\t" % n)
time.sleep(1)
print("%d is end" % n)
return os.getpid() #将进程号返回给回调函数
def run_callback(arg): #参数是进程里返回的
print("%s I'm the callback, my process is %s" % (arg, os.getpid()))
#会发现回调函数是在父进程执行的
if __name__ == '__main__': #别忘了进程必须有这个
pool = multiprocessing.Pool(5) #运行同时存在5个进程
for each in range(10):
pool.apply_async(func=run, args=(each,), callback=run_callback) #回调函数run_callback
pool.close()
pool.join()
相关API
- map:迭代创建进程,其使用方法十分简单,首先用
Pool()
方法创建一个进程池,然后用map()
方法传入函数和迭代器,从而迭代创建执行函数的进程,举例:
import multiprocessing
import time
import os
def main(i):
time.sleep(1)
print(i, os.getpid())
if __name__ == "__main__":
pool = multiprocessing.Pool(5)
pool.map(main, [i for i in range(10)])
# 1 37908
# 6 37908
# 0 37228
# 5 37228
# 2 40604
# 7 40604
# 3 39996
# 8 39996
# 4 40688
# 9 40688
监听进程完成
可以使用Pool
实例对象下的imap
方法,和线程池的map
方法差不多,举例:
import time
import multiprocessing
def task(n):
time.sleep(n % 2)
return n
if __name__ == '__main__':
pool = multiprocessing.Pool(multiprocessing.cpu_count())
for res in pool.imap(task, [i for i in range(5)]):
print(res)
# 0
# 1
# 2
# 3
# 4
也可以使用imap_unordered
方法,类似于as_completed
,谁先完成谁返回,举例:
import time
import multiprocessing
def task(n):
time.sleep(n % 2)
return n
if __name__ == '__main__':
pool = multiprocessing.Pool(multiprocessing.cpu_count())
for res in pool.imap_unordered(task, [i for i in range(5)]):
print(res)
# 0
# 2
# 4
# 1
# 3
进程池通信
而对于进程池Pool
,即使使用multiprocessing
里的Queue
也无法互相进行通信,此时需要使用multiprocessing.Manager
对象下的Queue
,举例:
import time
import multiprocessing
def task(queue):
queue.put(1)
if __name__ == '__main__':
queue = multiprocessing.Manager().Queue()
# 使用Manager对象下的Queue
pool = multiprocessing.Pool(multiprocessing.cpu_count())
pool.apply(task, (queue, ))
time.sleep(1)
print(queue.get())
# 1