第十六章协程-补充内容第十六章协程-补充内容多进程VS多线程多进程多处理模块多进程通信SharedDataQueuePipelineManagersProcessPoolMultithreading什么是GILGILGIL的例子ReleaseofGIL为什么这个设计可以去掉GILthreading模块multiprocessing.dummymodule两个模块的关系多进程分析详解Multi-processVS多线程ProcessMulti-process就是利用CPU的多核优势,同时并行执行多个任务,可以大大提高执行效率。多处理模块示例:frommultiprocessingimportProcessdeffun1(name):print('Test%smulti-process'%name)if__name__=='__main__':process_list=[]foriinrange(5):#Open5childprocessesExecutefun1functionp=Process(target=fun1,args=('Python',))#实例化进程对象p.start()process_list.append(p)foriinprocess_list:p.join()print('endtest')返回结果:testPython多进程测试Python多进程测试Python多进程测试Python多进程测试Python多进程测试Python多进程结束测试类继承方式:frommultiprocessingimportProcessclassMyProcess(Process):#InheritProcessclassdef__init__(self,name):super(MyProcess,self).__init__()self.name=namedefrun(self):print('Test%smultiprocess'%self.name)if__name__=='__main__':process_list=[]foriinrange(5):#开启5个子进程执行fun1functionp=MyProcess('Python')#实例化进程对象p.start()process_list.append(p)foriinprocess_list:p.join()print('endtest')多进程通信共享数据multi-process可以通过pipeline,queue,ManagersfrommultiprocessingimportProcesses实现通信和共享数据队列s,Queuedeffun1(q,i):print('子进程%s开始放数据'%i)q.put('我%s通过Queue通信'%i)if__name__=='__main__':q=Queue()process_list=[]foriinrange(3):#注意args中的q对象必须传递给我们要执行的方法,这样子进程才能使用Queue与主进程通信p=Process(target=fun1,args=(q,i,))p.start()process_list.append(p)foriinprocess_list:p.join()print('主进程获取队列数据')print(q.get())print(q.get())print(q.get())print('endtest')运行结果:子进程0启动putdatasubprocess1启动putdatasubprocess2启动putdata主进程获取队列数据我是0通过队列通信我是1通过队列通信我是2通过队列通信从多进程导入结束测试管道Process,Pipedeffun1(conn):print('Subprocesssendmessage:')conn.send('Hello主进程')print('子进程接受消息:')print(conn.recv())conn.close()if__name__=='__main__':conn1,conn2=Pipe()#关键点,管道实例化生成双向管道p=Process(target=fun1,args=(conn2,))#conn2传递给子进程p.start()print('主进程接受消息:')print(conn1.recv())print('主进程发送消息:')conn1.send("Hellochildprocess")p.join()print('Endtest')返回结果:主进程收到消息:子进程发送消息:子进程收到消息:hellothemainmain进程发送消息:helloSubprocessendtestManagersQueue和Pipe只实现数据交互,不实现数据共享,即一个进程改变另一个进程的数据然后使用ManagersfrommultiprocessingimportProcess,Managerdeffun1(dic,lis,index):dic[index]='a'dic['2']='b'lis.append(index)#[0,1,2,3,4,0,1,2,3,4,5,6,7,8,9]#print(l)if__name__=='__main__':withManager()asmanager:dic=manager.dict()#注意字典的声明方式,不能直接通过{}来定义l=manager.list(range(5))#[0,1,2,3,4]process_list=[]foriinrange(10):p=Process(target=fun1,args=(dic,l,i))p.start()process_list.append(p)forresinprocess_list:res.join()print(dic)print(l)返回结果:{0:'a','2':'b',2:'a',1:'a',3:'a',5:'a',4:'a',9:'a',6:'a',8:'a',7:'a'}[0,1,2,3,4,0,2,1,3,5,4,9,6,8,7]可以看到主进程定义了一个字典和一个列表。在子进程中,可以添加和修改字典的内容,向列表中插入新的数据,实现进程间的数据共享,即可以共同修改同一个数据进程。Pool进程池在里面维护了一个进程顺序。使用时去进程池中获取一个进程。如果进程池序列中没有可用进程,程序将一直等待,直到进程池中有可用进程。也就是说,有几个固定的进程可以使用。进程池中有两个方法:apply:同步,一般不用apply_async:异步asynchronous:frommultiprocessingimportProcess,Poolimportos,time,randomdeffun1(name):print('Runtask%s(%s)...'%(name,os.getpid()))start=time.time()time.sleep(random.random()*3)end=time.time()print('任务%s运行%0.2f秒.'%(name,(end-start)))if__name__=='__main__':pool=Pool(5)#为iinrange(10)创建一个包含5个进程的进程池:pool.apply_async(func=fun1,args=(i,))pool.close()pool.join()print('结束测试')运行结果为:运行任务0(13892)...运行任务1(15796)...运行任务2(6648)...运行任务3(9260)...运行任务4(1872)...任务3运行0.79秒。运行任务5(9260)...任务5运行0.10秒。运行任务6(9260)...任务2运行1.43秒。运行任务7(6648)...任务4运行2.44秒。任务1运行2.44秒。运行任务8(1872)...运行任务9(15796)...任务0运行2.58秒。任务9运行0.14秒。任务8运行0.88秒。Task6运行2.55秒。任务7运行2.07秒。结束测试对Pool对象调用join()方法并等待所有子进程完成执行。在调用join()之前,必须先调用close()。调用close()后,不能继续添加新的Processes。另一个例子:frommultiprocessingimportManager,Poolimportos,timedefreader(q):print("readerstarted(%s),parentprocessis(%s)"%(os.getpid(),os.getpid()))foriinrange(q.qsize()):print("readergotthemessagefromQueue:%s"%q.get(True))defwriter(q):print("writerstart(%s),The父进程是(%s)"%(os.getpid(),os.getpid()))foriin"itcast":q.put(i)if__name__=="__main__":print("(%s)start"%os.getpid())q=Manager().Queue()#在管理器中使用队列po=Pool()po.apply_async(writer,(q,))time.sleep(1)po.apply_async(reader,(q,))po.close()po.join()print("(%s)End"%os.getpid())运行结果为:(9516)startwriterstarted(10852),父进程is(10852)readerstarts(13392),父进程是(13392)reader从Queue获取消息:ireader从Queue获取消息:treader从Queue获取消息:creader从Queue获取消息:arear从Queue:sreader从Queue:t(9516)End获取消息同步:frommultiprocessingimportProcess,Poolimportos,time,randomdeffun1(name):print('Runtask%s(%s)...'%(name,os.getpid()))start=time.time()time.sleep(random.random()*3)end=time.time()print('任务%s运行%0.2f秒。'%(name,(end-start)))if__name__=='__main__':pool=Pool(5)#创建一个包含5个进程的进程池foriinrange(10):pool.apply(func=fun1,args=(i,))pool.close()pool.join()print('endtest')结果:运行任务0(12996)...任务0运行0.96秒。运行任务1(5704)...任务1运行2.68秒。运行任务2(6808)...任务2运行1.31秒。运行任务3(3020)...任务3运行0.85秒。运行任务4(16980)...任务4运行2.35秒。运行任务5(12996)...任务5运行1.25秒。运行任务6(5704)...任务6运行2.43秒。运行任务7(6808)...任务7运行1.79秒。运行任务8(3020)...任务8运行0.72秒。运行任务9(16980))...任务9运行0.77秒。结束了测试。什么是多线程GILGIL?GIL是python的全局解释器锁。如果同一个进程中有多个线程在运行,运行python程序时,一个线程会占用python解释器。解释器(加锁或GIL),使进程中的其他线程无法运行,该线程运行后其他线程才能运行。如果线程运行过程中遇到耗时操作,则解开解释器锁,让其他线程运行。因此,在多线程中,线程的运行仍然是顺序的,不是同时进行的。在多进程中,由于每个进程都可以由系统分配资源,相当于每个进程都有一个python解释器,所以多进程可以实现多个进程同时运行。缺点是进程系统资源开销大。GIL示例importtimedefdecrement(n):whilen>0:n-=1start=time.time()decrement(100000000)cost=time.time()-startprint(cost)#4.968633651733398使用threading多线程模块:importtimeimportthreadingdef递减(n):当n>0:n-=1start=time.time()t1=threading.Thread(target=decrement,args=[50000000])t2=threading.Thread(target=decrement,args=[50000000])t1。start()#启动线程并执行任务t2.start()#同上t1.join()#主线程阻塞,直到t1执行完成,主线程继续执行t2.join()#同上如上cost=time.time()-startprint(cost)#4.755946159362793按理来说,两个线程同时在两个CPU上并行运行,时间应该减半,现在不减反增。是什么导致多线程不愉快和缓慢?原因在于GIL。在Cpython解释器(Python语言的主流解释器)中,有一个全局解释器锁(GlobalInterpreterLock)。解释器在解释执行Python代码时,首先要获得这把锁,这意味着任何一个线程在同一时刻都可能正在执行代码。其他线程要想获得CPU执行代码指令,必须先获得锁。如果锁被其他线程占用,那么该线程只能等待,直到拥有锁的线程释放锁后,才有可能执行代码指令。所以,这也是为什么两个线程一起执行比较慢的原因,因为同一时间只有一个线程在运行,其他线程只能等待。即使是多核CPU,也没有办法让多个线程同时“并行”。执行代码只能交替执行,因为多线程涉及在线文本切换和锁机制处理(获取锁、释放锁等),所以多线程执行慢而不快。GIL发布GIL什么时候发布?当线程遇到I/O任务时,释放GIL。当一个计算密集型(CPU-bound)的线程执行了100个解释器ticks(ticks可以粗略地认为是Python虚拟机的指令)时,GIL也会被释放。可以通过sys.setcheckinterval()设置步长,通过sys.getcheckinterval()检查步长。与单线程相比,这些可能是多线程带来的额外开销。为什么CPython解释器要这样设计?多线程是适应现代计算机硬件飞速发展,充分利用多核处理器的产物。通过多线程,可以高效地利用CPU资源。但是,多线程有一个问题。如何解决共享数据的同步和一致性问题,因为,当多个线程访问共享数据时,可能有两个线程同时修改一个数据。如果没有合适的机制保证数据的一致性,程序最终会引发异常。因此,Python之父拥有全局线程锁,不管你的数据有没有同步问题,反正一刀切,带全局锁保证数据安全。这就是多线程鸡肋的原因,因为它没有细粒度的控制数据安全,而是用一种简单粗暴的方式来解决。是否可以去掉GIL,那么去掉GIL是可行的吗?去掉GIL的Python在单线程条件下执行效率慢了近2倍。threadingmoduleimportthreadingfromtimeimportsleepclassMythead(threading.Thread):defrun(self):foriinrange(3):sleep(1)#Suspend1s#self.name是线程的名字,一个会是默认分配一个“Thread-N”形式的名称,其中N是一个十进制数print(u'Thread:%s,index%s'%(self.name,i))if__name__=='__main__':foriinrange(3):t=Mythead()t.start()运行结果:Thread:Thread-2,index0thread:Thread-3,index0thread:Thread-4,index0thread:Thread-2,index1thread:Thread-3,index1thread:Thread-4,index1thread:Thread-2,index2thread:Thread-3,index2thread:Thread-4,index2从上面的运行结果来看,multi-线程程序执行的顺序是确定的。importthreadingimportloggingfromtimeimportsleep#配置日志记录,以“模式名称/线程名称(10个字符)/信息”的形式输出。logging.basicConfig(level=logging.DEBUG,format='[%(levelname)s](%(threadName)-10s)%(message)s')deffoo():logging.debug('开始...')sleep(2)logging.debug('Exiting...')threading.Thread(name='mythread',target=foo).start()threading.Thread(target=foo).start()返回结果为:[DEBUG](mythread)正在启动...[DEBUG](Thread-2)正在启动...[DEBUG](mythread)正在退出...[DEBUG](Thread-2)正在退出...更有趣的是,多处理一个虚拟对象multiprocessingAPI中提供了module,它提供了对threading模块的封装,也就是说当使用如下代码时:,而multiprocessing就是多进程,api是通用的。frommultiprocessing.dummyimportPool,ProcessPool和Process的底层其实都是使用线程实现的(即ThreadPool和Thread),#frommultiprocessingimportPoolfrommultiprocessing.dummyimportPoolasThreadPoolimporttimefromurllib.requestimporturlopenurls=['http://www.baidu.com','http://home.baidu.com/','http://tieba.baidu.com/',]start=time.time()results=map(urlopen,urls)print('Normal:',time.time()-start)start2=time.time()#开启8个worker,如果不带参数,默认为cpu核心数pool=ThreadPool(processes=8)results2=pool.map(urlopen,urls)pool.close()pool.join()print('ThreadPool:',time.time()-start2)两者的关系是多进程适合CPU密集型操作(有很多cpu操作指令,比如bitmany浮点数操作)。多线程适用于IO密集型操作(读写数据操作,比如爬虫)多进程分析详解深入Python多进程编程基础深入Python多进程通信原理与实战
