本篇主要介绍关于多进程的相关知识,同时需对进程与线程之间的关系和区别及应用进行了解,其实包含 multiprocessing模块下的 Process类、进行进程间通讯的 Queue类以及进程池 Pool类的学习,最后实现一个实例 模拟文件夹copy器;python
首先咱们在了解进程以前,咱们须要了解一下什么是程序?多线程
程序:并发
其实就是一堆躺在操做系统下的二进制文件,是静态的;例如 wechar.exe文件,在咱们没点击它时,就是一个静态的可执行文件。
app
进程:async
进程就是跑起来的程序,即代码 + 操做系统根据其需求为其分配的资源称之为进程;例如:当一个 Wechat.exe文件咱们点击运行时,操做系统会为其调配其所需资源,则运行该程序就是一个进程;进程是操做系统分配资源的基本单位。
函数
须要注意的是:同一个程序执行两次,那就是两个进程;例如 打开腾讯视频--》一个能够播放西游记,一个能够播放红楼梦;学习
进程的状态:spa
在现实工做中,任务的数量要远远大于CPU的数量,因此要想实现真正意义上的并行几乎是不可能的,因此大多数状况下都是并发(伪并行),故当运行多个进程时,有的程序处于等待的状态、有的程序处于运行的状态,则进程就能够分为如下几种状态:操作系统
三种状态:线程
就绪:全部准备工做均完成,等待着操做系统分配CPU进行执行;
运行:正在被CPU执行的程序
堵塞:即待某些条件知足,例如一个程序sleep,此时就处于等待态。
咱们建立多进程主要经过模块 multiprocessing 下的Process类来建立进程对象,经过进程对象来实现建立进程的效果,很少说直接看示例,例如:
#!/usr/bin/env python # -*- coding:utf-8 -*- import multiprocessing,time def sing(name): for i in range(10000): print("%s is singing --->%s"%(name,i)) # time.sleep(0.5) def dance(name): for i in range(10000): print("%s is dancing ---> %s"%(name,i)) # time.sleep(0.5) def main(): # 一、建立进程对象,传入工做函数,以及工做函数所需参数 p1 = multiprocessing.Process(target=sing,args=("alex",)) p2 =multiprocessing.Process(target=dance,args=("liudehua",)) # 二、启动进程 p1.start() p2.start() print("This is main process...") # 三、主进程等子进程运行完才关闭 p1.join() p2.join() if __name__=="__main__": main()
从上述例子中能够看出:进程的建立过程与线程的建立过程几乎相同,除了使用模块不相同,其余的几乎相似。那么既然有了线程为何还要有进程呢?接下来咱们就了解一下进程和线程的区别。
首先咱们须要知道不管是线程仍是进程都可以实现多任务的效果,可是一个进程必然有一个线程,同时一个进程也能够有多个线程,例如:一个WeChat能够打开多个聊天窗口,与不一样的人进行聊天。其关系以下图:
从上面的图咱们能够很明确的看出进程与线程之间的关系和区别:
一、一个程序至少有一个进程(固然也能够有多个进程,例如:一个QQ能够同时登入两个用户;),一个进程至少有一个线程(固然也可由多个线程,例如:一个QQ能够同时与两我的进行聊天。)
二、线程没法独立存在,只能依附在进程内执行,即在QQ没有运行的状况下QQ聊天是没法实现的。
三、一个进程的运行须要调配的资源要多得多,可是进程要稳定许多,即一个进程的关闭不会影响另一个进程的运行,即关闭网易云音乐队每天静听的执行没有任何影响;可是一个主线程的关闭必然会致使全部的子线程死亡,稳定性不如进程。
四、线程能够共享全局变量,而进程间没法全局变量的共享;(要想实现进程间的通讯需使用队列等,在后面介绍。)
五、进程是操做系统进行资源管理的基本单位,而线程是对操做系统的分配下来的资源调用的基本单位。
其实咱们也能够将进程和线程比喻成:车间流水线的工做(进程) 和 流水线上工人的工做(线程):
我的理解:
能够将进程好比成流水线的运行,即在该流水线的运行须要各类资源(即进程的运行须要操做系统调配资源),以及许多工人,这些工人在不停的工做(线程),他们对该流水线上的资源是共享的(线程间共享全局变量),同时若想提升流水线的运行速度,则能够为流水线增长一些工人(多线程),而同时若想提升车间的生产能力,则能够经过增长多条流水线(多进程),可是每增长一条流水线,所需的成本也高一些(即进程需调配的资源增长),同时每一个流水线上的资源是不共享的(即进程间不共享全局变量。)。
每一个进程均会有进程号以及父进程号,在Linux中能够经过 ps - aux或者top命令查看当前进程,kill命令+pid杀死进程,而在Windows中能够经过在cmd终端中 tasklist命令查看进程,经过taskkill +pid 杀死进程;那么咱们如何在获取进程号呢?
#!/usr/bin/env python # -*- coding:utf-8 -*- #!/usr/bin/env python # -*- coding:utf-8 -*- import multiprocessing,time,os def sing(name): """唱歌""" for i in range(3): print("%s is singing --->%s"%(name,i)) time.sleep(0.5) print("---in sing process-- pid:%s, ppid:%s ."%(os.getpid(),os.getppid())) # 查看运行sing函数的子进程的pid和ppid def dance(name): for i in range(3): print("%s is dancing ---> %s"%(name,i)) time.sleep(0.5) print("---in dance process-- pid:%s, ppid:%s ." % (os.getpid(), os.getppid())) # 查看运行dance函数的子进程的pid和 ppid def main(): # 一、建立进程对象,传入工做函数,以及工做函数所需参数 p1 = multiprocessing.Process(target=sing,args=("alex",)) p2 =multiprocessing.Process(target=dance,args=("liudehua",)) # 二、启动进程 p1.start() p2.start() print("This is main process...") print("---in main process-- pid:%s, ppid:%s ." % (os.getpid(), os.getppid())) time.sleep(0.5) p1.terminate() #杀死子进程 # 三、主进程等子进程运行完才关闭 p1.join() p2.join() if __name__=="__main__": main()
注:从运行结果得知:子进程的父进程就是主进程,而主进程的父进程其实为为当前集成开发IDE的运行进程PID。
关于进程的运行顺序:
主进程最早运行,而子进程的运行顺序彻底由操做系统所决定。
孤儿进程:
当主进程被杀死的时候,子进程是不会死亡的,这种进程被称为孤儿进程;而除了一个pid为1的进程外,其余的进程均有父进程,这时这些孤儿进程会被收养到孤儿院,根据孤儿进程的ppid咱们能够得知该孤儿院为 进程ID为2041的upstart进程。
僵尸进程:
当子进程被杀死时,父进程不会为该子进程收尸即不会对其所占资源进行回收,则该子进程被称为僵尸进程,这类僵尸进程是对系统的运行时不利的,故咱们一般经过join()方法来避免僵尸进程的出现。
首先,咱们须要知道进程间不共享全局变量,例如:网易云音乐没法播放酷狗中的音乐,接下来咱们从一个例子中验证:
#!/usr/bin/env python # -*- coding:utf-8 -*- import multiprocessing,time # 建立全局变量 name =["alex","little-five","amanda"] def work1(): # work1修改全局变量 name.append("hello") print(name) def work2(): # work2查看全局变量 print("in the work2:--> ",name) def main(): p1 = multiprocessing.Process(target=work1) p2 = multiprocessing.Process(target=work2) p1.start() time.sleep(1) # 休眠1s,保证进程p1先运行 p2.start() print("in main process : ",name) p1.join() p2.join() if __name__=="__main__": main()
从输出结果能够得出 : 进程间不共享全局变量,即即便主进程与子进程间也不共享全局变量。这是因为:
进程每建立子线程时,老版本内核会至关于将主进程的资源和代码copy一份,但每一个子线程运行的代码位置不一样,可是新版本则不会拷贝代码而是代码共享,而当某个进程要修改代码时,则是相应的代码进行拷贝即写时拷贝。
注:在线程中咱们有互斥锁,而进程用于不存在全局变量共享问题,则就不须要互斥锁,而是利用Queue来实现进程间的通讯。
那么咱们怎么实现进程间的通讯呢?
#!/usr/bin/env python # -*- coding:utf-8 -*- import multiprocessing def recv_msg(q): """数据的接收""" names =["alex","wupeiqi","linghaifeng"] for name in names: q.put(name) #将每一个名字传入队列 def analysis_msg(q): """数据的分析""" new_names =list() while not q.empty(): new_name=q.get() # 获取队列中的名字 new_names.append(new_name) print(list(map(lambda x:x.title(),new_names))) #经过map 函数对列表进行处理 q=multiprocessing.Queue() # 建立队列对象 def main(): p1 = multiprocessing.Process(target=recv_msg,args=(q,)) #咱们将队列做为工做函数的参数传入进程 p2 = multiprocessing.Process(target=analysis_msg,args=(q,)) p1.start() p2.start() p1.join() p2.join() if __name__=="__main__": main()
注:其实队列就像一根水管,即先入先出,后入后出,数据从水管的一边传入从另外一边输出,经过队列来实现进程间的数据传递,实现进程间的通讯。
在学习进程池以前,咱们须要了解为何咱们须要学习进程池:
咱们知道经过一个进程咱们能够实现一个复杂的任务,可是当咱们有许多个任务即成千上万的任务或者这些任务要重复执行时,这时候咱们就须要用到进程池。下面咱们看一下进程池的用法:
#!/usr/bin/env python # -*- coding:utf-8 -*- import multiprocessing,time,os def cal(num):
"""计算""" n =0 for i in range(num): n+=1 time.sleep(0.2) print("01-the result is -->%s ,PID: %s ."%(n,os.getpid())) def worker(lis): """计算map函数运行时间""" start_time = time.time() new_list=list(map(lambda x:x.title(),lis )) time.sleep(1) end_time =time.time() print("02--close_time -->%s,PID:%s ,"%(end_time-start_time,os.getpid())) def main(): po = multiprocessing.Pool(3) # 定义进程池,规定进程池中最大的进程数为3 for i in range(100): # 让上面的两个任务重复执行100次 po.apply_async(cal,args=(100,)) # 每次循环结束均会有子进程去执行任务 po.apply_async(worker,args=(["alex","little-five","hello"],)) print("---start----") po.close() # 关闭进程池 po.join() # 等待子进程所有执行完,必须放在close以后 print("---end-----") if __name__ == "__main__": main()
那么进程池的实现机制是怎么样的呢?
始化Pool时,能够指定一个最大进程数,当有新的请求提交到Pool中时,若是池尚未满,那么就会建立一个新的进程用来执行该请求;但若是池中的进程数已经达到指定的最大值,那么该请求就会等待,直到池中有进程结束,才会用以前的进程来执行新的任务。若任务数量十分多的时候,这些任务会被操做系统加载到一个内存中,被进程池中的进程循环执行。
#!/usr/bin/env python # -*- coding:utf-8 -*- import os,multiprocessing,time def copy_file(queue,source_folder,new_folder,file_name): """将源文件内文件数据读取出来 写入新文件夹内的文件中""" source_file_path = source_folder + "/" + file_name f1 = open(source_file_path,"rb") file_data = f1.read() f1.close() new_file_path = new_folder + "/" + file_name f2 = open(new_file_path,"wb") f2.write(file_data) f2.close() # 每copy一个文件,将文件名传入队列,告诉主进程已完成一个文件的拷贝 queue.put(file_name) def main(): # 一、获取源文件夹文件名 while True: source_folder_name = input("请输入源文件夹名称:--》") if os.path.exists(source_folder_name): break # 二、建立目标文件夹 new_folder_name = source_folder_name + "-copy" try: os.mkdir(new_folder_name) except Exception as e: pass # 三、获取源文件的文件 file_list = os.listdir(source_folder_name) # 建立队列来实现主线程和子线程间的通讯 q = multiprocessing.Manager().Queue() # 四、经过进程池来实现原文件数据的读取和写入新文件(文件数量较多,故采用进程池) po = multiprocessing.Pool(5) for file_name in file_list: #没一个文件的成品copy用进程池中的一个进程来实现 po.apply_async(copy_file,args=(q,source_folder_name,new_folder_name,file_name)) po.close() po.join() # 实现进度条的效果 source_len = len(file_list) # 获取源文件列表的长度 while True: time.sleep(0.2) over_copy_filename = q.get() # 获取已完成copy文件的文件名 # new_file_list = os.listdir(new_folder) if over_copy_filename in file_list: # 若是该文件已完成copy,则移除从源文件列表中移除该文件名 file_list.remove(over_copy_filename) len_tax = len(file_list)/source_len print(" \r 进度为:--》%0.2f %%" % (100 * (1 - len_tax)), end="") if len_tax == 0: print("\n copying file is over ..") break if __name__ =="__main__": main()