python笔记 并发编程(线程、进程、协程)
一、概念
(一)、操作系统
操作系统是一个用来协调、管理和控制计算机硬件和软件资源的系统程序,它位于硬件和应用程序之间
操作系统的内核的定义:操作系统的内核是一个管理和控制程序,负责管理计算机的所有物理资源,其中包括:文件系统、内存管理、设备管理和进程管理
操作系统的作用: 1:隐藏丑陋复杂的硬件接口,提供良好的抽象接口 2:管理、调度进程,并且将多个进程对硬件的竞争变得有序
(二)、计算机发展
1、真空管与穿孔卡片(无操作系统)
优点:程序员在申请的时间段内独享整个资源,即时的调试自己的程序,如果有bug可以当场处理, 缺点:资源浪费
2、晶体管和批处理系统
优点:批处理 缺点: 1 人机交互过多 2 仍然是顺序计算
3、集成电路芯片和多道程序设计
多道技术:
空间上的复用:将内存分为几部分,每个部分放入一个程序,这样,同一时间内存中就有了多道程序。
时间上的复用:当一个程序在等待I/O时,另一个程序可以使用cpu,如果内存中可以同时存放足够多的作业,则cpu的利用率可以接近100%
操作系统采用了多道技术后,可以控制进程的切换,或者说进程之间去争抢cpu的执行权限。这种切换不仅会在一个进程遇到io时进行,一个进程占用cpu时间过长 也会切换,或者说被操作系统夺走cpu的执行权限
4、个人计算机 发展至今
(三)、进程线程
进程
假如有两个程序A和B,程序A在执行到一半的过程中,需要读取大量的数据输入(I/O操作),而此时CPU只能静静地等待任务A读取完数据才能继续执行,这样就白白浪费了CPU 资源。是不是在程序A读取数据的过程中,让程序B去执行,当程序A读取完数据之后,让程序B暂停,然后让程序A继续执行?当然没问题,但这里有一个关键词:切换 既然是切换,那么这就涉及到了状态的保存,状态的恢复,加上程序A与程序B所需要的系统资源(内存,硬盘,键盘等等)是不一样的。自然而然的就需要有一个东西去记录程序A和程序B分别需要什么资源,怎样去识别程序A和程序B等等,所以就有了一个叫进程的抽象
进程定义:
进程就是一个程序在一个数据集上的一次动态执行过程。 进程一般由程序、数据集、进程控制块三部分组成。 我们编写的程序用来描述进程要完成哪些功能以及如何完成; 数据集则是程序在执行过程中所需要使用的资源; 进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。
线程
线程的出现是为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程只能干一样事的缺陷, 使到进程内并发成为可能。 假设,一个文本程序,需要接受键盘输入,将内容显示在屏幕上,还需要保存信息到硬盘中。若只有一个进程,势必造成同一时间只能干一样事的尴尬(当保存时,就不能通过键盘输入内容)。若有多个进程,每个进程负责一个任务,进程A负责接收键盘输入的任务,进程B负责将内容显示在屏幕上的任务,进程C负责保存内容到硬盘中的任务。这里进程A,B,C间的协作涉及到了进程通信问题,而且有共同都需要拥有的东西-------文本内容,不停的切换造成性能上的损失。若有一种机制,可以使 任务A,B,C共享资源,这样上下文切换所需要保存和恢复的内容就少了,同时又可以减少通信所带来的性能损耗,那就好了。是的,这种机制就是线程。 线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能。线程没有自己的系统资源。
进程和线程的区别
1 一个程序至少有一个进程,一个进程至少有一个线程.(进程可以理解成线程的容器)
2 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
3 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和 程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
4 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈)但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源. 一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.
全局解释器锁(global interpreter lock) 在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势
GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安 全。GIL保护的是解释器级的数据(如垃圾回收线程),保护用户自己的数据则需要自己加锁处理
二、方法
(一)多线程调用
直接调用:
#方式一 from threading import Thread import time def sayhi(name): time.sleep(2) print('%s say hello' %name) if __name__ == '__main__': t=Thread(target=sayhi,args=('egon',)) t.start() print('主线程')
继承式调用:
#方式二 from threading import Thread import time class Sayhi(Thread): def __init__(self,name): #继承父类的init方法 super().__init__() self.name=name def run(self): #必须有run方法,start()调用的就是run方法 time.sleep(2) print('%s say hello' % self.name) if __name__ == '__main__': t = Sayhi('egon') t.start() print('主线程')
(二) threading.thread线程实例方法
1、join():在子线程完成运行之前,这个子线程的父线程将一直被阻塞。
import threading from time import ctime,sleep import time def ListenMusic(name): print ("Begin listening to %s. %s" %(name,ctime())) sleep(8) print("end listening %s"%ctime()) def RecordBlog(title): print ("Begin recording the %s! %s" %(title,ctime())) sleep(5) print('end recording %s'%ctime()) threads = [] t1 = threading.Thread(target=ListenMusic,args=('水手',)) t2 = threading.Thread(target=RecordBlog,args=('python线程',)) threads.append(t1) threads.append(t2) if __name__ == '__main__': for t in threads: t.start() t.join() #此处加join,主线程需要等所有线程结束才会结束,相当于串行 #for循环属于主线程,所以当t.join时,需要等待结束,才能执行下一次循环 '''运行结果:Begin listening to 水手. Thu Jan 9 18:05:31 2020 end listening Thu Jan 9 18:05:39 2020 Begin recording the python线程! Thu Jan 9 18:05:39 2020 end recording Thu Jan 9 18:05:44 2020 all over Thu Jan 9 18:05:44 2020''' t1.join() #此处join,线程已经全部启动了,所以等待t1结束,主线程就结束了 print ("all over %s" %ctime()) '''运行结果 Begin listening to 水手. Thu Jan 9 18:04:16 2020 Begin recording the python线程! Thu Jan 9 18:04:16 2020 end recording Thu Jan 9 18:04:21 2020 end listening Thu Jan 9 18:04:24 2020 all over Thu Jan 9 18:04:24 2020'''
2、守护线程 setDaemon(True):
将线程声明为守护线程,必须在start() 方法调用之前设置, 如果不设置为守护线程程序会被无限挂起。这个方法基本和join是相反的。
当我们 在程序运行中,执行一个主线程,如果主线程又创建一个子线程,主线程和子线程 就分兵两路,分别运行,那么当主线程完成
想退出时,会检验子线程是否完成。如 果子线程未完成,则主线程会等待子线程完成后再退出。但是有时候我们需要的是 只要主线程
完成了,不管子线程是否完成,都要和主线程一起退出,这时就可以用setDaemon方法啦
遵循:守护线程会等待主线程运行完毕后被销毁
需要强调的是:运行完毕并非终止运行 对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
详细解释:
主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
from threading import Thread import time def foo(): print(123) time.sleep(3) print("end123") def bar(): print(456) time.sleep(1) print("end456") t1=Thread(target=foo) t2=Thread(target=bar) t1.setDaemon(True) #注意:一定在start之前设置 ,实际是在调t1.daemon = True t1.start() t2.start() print("main-------") ''' 123 456 main------- end456'''
3、其它方法
# run(): 线程被cpu调度后自动执行线程对象的run方法 # start():启动线程活动。 # isAlive(): 返回线程是否活动的。 # getName(): 返回线程名。 # setName(): 设置线程名。 threading模块提供的一些方法: # threading.currentThread(): 返回当前的线程变量。 # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。 # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
(三) 锁LOCK
1、同步锁
锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据
GIL 与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock
#共享同一数据,数据安全无法保证 from threading import Thread import os,time def work(): global n temp=n time.sleep(0.1) n=temp-1 if __name__ == '__main__': n=100 l=[] for i in range(100): p=Thread(target=work) l.append(p) p.start() for p in l: p.join() print(n) #结果可能为99
遇到sleep所有线程I/O切换,几乎所有的线程都拿到了全局变量100
解决办法是利用同步锁Lock()
import threading from threading import Thread import time from threading import Thread import os,time def work(): global n lock.acquire() #为下面三行代码加锁,变成串行 temp=n time.sleep(0.1) n=temp-1 lock.release() #为上面三行代码解锁 if __name__ == '__main__': n=100 l=[] lock = threading.Lock() #设置同步锁 for i in range(100): p=Thread(target=work) l.append(p) p.start() for p in l: p.join() print(n) #结果为0
2、线程死锁和递归锁
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁,因为系统判断这部分资源都正在使用,所有这两个线程在无外力作用下将一直等待下去。下面是一个死锁的例子:
import threading import time class MyThread(threading.Thread): def doA(self): lockA.acquire() print(self.name,'got A',time.time()) time.sleep(3) lockB.acquire() print(self.name, 'got B', time.time()) time.sleep(1) lockB.release() lockA.release() def doB(self): lockB.acquire() print(self.name, 'got B', time.time()) time.sleep(4) lockA.acquire() print(self.name, 'got A', time.time()) time.sleep(3) lockA.release() lockB.release() def run(self): self.doA() self.doB() if __name__ == "__main__": l = [] lockA = threading.Lock() lockB = threading.Lock() for i in range(5): t = MyThread() t.start() l.append(t) for t in l: t.join() print('end....') ''' Thread-1 got A 1587795082.6547809 Thread-1 got B 1587795085.6560907 Thread-1 got B 1587795086.6565468 Thread-2 got A 1587795086.6565468 卡住了,线程2需要B锁,线程1需要A锁 '''
解决办法:使用递归锁
为了支持在同一线程中多次请求同一资源,python提供了“可重入锁”:threading.RLock。RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源。
import threading import time class MyThread(threading.Thread): def doA(self): lock.acquire() #counter计数加1 = 1 print(self.name,'got A',time.time()) time.sleep(3) lock.acquire() #counter计数加1 = 2 print(self.name, 'got B', time.time()) lock.release() #counter计数减1 = 1 lock.release() #counter计数减1 = 0 此时其他线程可以获取资源了 def doB(self): lock.acquire() #counter计数加1 = 1 print(self.name, 'got B', time.time()) time.sleep(4) lock.acquire() #counter计数加1 = 2 print(self.name, 'got A', time.time()) lock.release() #counter计数减1 = 1 lock.release() #counter计数减1 = 0 此时其他线程可以获取资源了 def run(self): self.doA() self.doB() if __name__ == "__main__": l = [] lock = threading.RLock() for i in range(5): t = MyThread() t.start() l.append(t) for t in l: t.join() print('end....')
(四)、同步对象(Event)和信号量Semaphore(了解)
An event is a simple synchronization object;the event represents an internal flag,
and threads can wait for the flag to be set, or set or clear the flag themselves.
event = threading.Event() 创建event对象
# a client thread can wait for the flag to be set
event.wait() 设置阻塞状态,等待event.set条件然后放行
# a server thread can set or reset it
event.set() 设置event状态
event.clear() 重置event状态
If the flag is set, the wait method doesn’t do anything.如果设置了该标志,则wait方法不会执行任何操作。
If the flag is cleared, wait will block until it becomes set again.如果该标志被清除,则等待将阻塞,直到再次设置。
Any number of threads may wait for the same event.任何数量的线程都可以等待同一事件。
import threading,time class Boss(threading.Thread): def run(self): print("BOSS:今晚大家都要加班到22:00。") print(event.isSet()) event.set() time.sleep(5) print("BOSS:<22:00>可以下班了。") print(event.isSet()) event.set() class Worker(threading.Thread): def run(self): event.wait() print("Worker:哎……命苦啊!") time.sleep(1) event.clear() event.wait() print("Worker:OhYeah!") if __name__=="__main__": event=threading.Event() threads=[] for i in range(5): threads.append(Worker()) threads.append(Boss()) for t in threads: t.start() for t in threads: t.join()
信号量用来控制线程并发数的,BoundedSemaphore或Semaphore管理一个内置的计数 器,每当调用acquire()时-1,调用release()时+1。
计数器不能小于0,当计数器为 0时,acquire()将阻塞线程至同步锁定状态,直到其他线程调用release()。(类似于停车位的概念)
BoundedSemaphore与Semaphore的唯一区别在于前者将在调用release()时检查计数 器的值是否超过了计数器的初始值,如果超过了将抛出一个异常。
import threading,time class myThread(threading.Thread): def run(self): if semaphore.acquire(): print(self.name) time.sleep(5) semaphore.release() if __name__=="__main__": semaphore=threading.Semaphore(5) thrs=[] for i in range(100): thrs.append(myThread()) for t in thrs: t.start()
(五)、多线程利器---队列(queue)
列表是不安全的数据结构,多线程可以同时操作列表
当必须在多个线程之间安全地交换信息时,队列在线程编程中特别有用。
queue列队类的方法
创建一个“队列”对象 import Queue q = Queue.Queue(maxsize = 10) Queue.Queue类即是一个队列的同步实现。队列长度可为无限或者有限。可通过Queue的构造函数的可选参数maxsize来设定队列长度。如果maxsize小于1就表示队列长度无限。 将一个值放入队列中 q.put(10) 调用队列对象的put()方法在队尾插入一个项目。put()有两个参数,第一个item为必需的,为插入项目的值;第二个block为可选参数,默认为 1。如果队列当前为空且block为1,put()方法就使调用线程暂停,直到空出一个数据单元。如果block为0,put方法将引发Full异常。 将一个值从队列中取出 q.get() 调用队列对象的get()方法从队头删除并返回一个项目。可选参数为block,默认为True。如果队列为空且block为True,
get()就使调用线程暂停,直至有项目可用。如果队列为空且block为False,队列将引发Empty异常。 Python Queue模块有三种队列及构造函数: 1、Python Queue模块的FIFO队列先进先出。 class queue.Queue(maxsize) 2、LIFO类似于堆,即先进后出。 class queue.LifoQueue(maxsize) 3、还有一种是优先级队列级别越低越先出来。 class queue.PriorityQueue(maxsize) 此包中的常用方法(q = Queue.Queue()): q.qsize() 返回队列的大小 q.empty() 如果队列为空,返回True,反之False q.full() 如果队列满了,返回True,反之False q.full 与 maxsize 大小对应 q.get([block[, timeout]]) 获取队列,timeout等待时间 q.get_nowait() 相当q.get(False) 非阻塞 q.put(item) 写入队列,timeout等待时间 q.put_nowait(item) 相当q.put(item, False) q.task_done() 在完成一项工作之后,q.task_done() 函数向任务已经完成的队列发送一个信号 q.join() 实际上意味着等到队列为空,再执行别的操作
生产者消费者模型
为什么要使用生产者和消费者模式
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消 费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
什么是生产者消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用 等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
#利用队列阻塞信号,利用task_done、join完成线程间的通信, import time,random import queue,threading q = queue.Queue() #创建线程队列q def Producer(name): #创建一个生产者 count = 0 while count <10: print("%s making......"%name) time.sleep(random.randrange(3)) q.put(count) print('Producer %s has produced %s baozi..' %(name, count)) count +=1 q.join() #等待消费者的task_done信号 print("收到task_done马上开始制作......") def Consumer(name): #创建一个消费者 count = 0 while count <10: time.sleep(random.randrange(4)) print('%s waiting....'%name) data = q.get() print('%s eating...'%name) time.sleep(3) print('\033[32;1mConsumer %s has eat %s baozi...\033[0m' % (name, data)) q.task_done() #用task_done告诉队列一个信号,已经吃了一个了 print(data) count +=1 p1 = threading.Thread(target=Producer, args=('A厨师',)) c1 = threading.Thread(target=Consumer, args=('B食客',)) c2 = threading.Thread(target=Consumer, args=('C食客',)) c3 = threading.Thread(target=Consumer, args=('D食客',)) p1.start() c1.start() c2.start() c3.start()
#运行结果 A厨师 making...... B食客 waiting.... Producer A厨师 has produced 0 baozi.. B食客 eating... D食客 waiting.... C食客 waiting.... Consumer B食客 has eat 0 baozi... 0 收到task_done马上开始制作...... A厨师 making...... Producer A厨师 has produced 1 baozi.. D食客 eating... B食客 waiting.... Consumer D食客 has eat 1 baozi... 1 D食客 waiting.... 收到task_done马上开始制作...... A厨师 making...... Producer A厨师 has produced 2 baozi.. C食客 eating... Consumer C食客 has eat 2 baozi...
(六)、多进程模块 multiprocessing
M
is a package that supports spawning processes using an API similar to the threading module. The ultiprocessing
multiprocessing
package offers both local and remote concurrency,effectively side-stepping the Global Interpreter Lock by using subprocesses
instead of threads. Due to this, the
module allows the programmer to fully leverage multiple processors on a multiprocessing
given machine. It runs on both Unix and Windows.
由于GIL的存在,python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程。
multiprocessing包是Python中的多进程管理包。与threading.Thread类似,它可以利用multiprocessing.Process对象来创建一个进程。该进程可以运行在
Python程序内部编写的函数。该Process对象与Thread对象的用法相同,也有start(), run(), join()的方法。此外multiprocessing包中也有Lock/Event/
Semaphore/Condition类 (这些对象可以像多线程那样,通过参数传递给各个进程),用以同步进程,其用法与threading包中的同名类一致。所以,multiprocessing
的很大一部份与threading使用同一套API,只不过换到了多进程的情境。
一、直接导入模块调用
from multiprocessing import Process #导入进程模块下的process类 import time def f(name): time.sleep(1) print('hello', name,time.ctime()) if __name__ == '__main__': p_list=[] for i in range(3): p = Process(target=f, args=('alvin',)) #创建进程的方式和线程一样 p_list.append(p) p.start() for i in p_list: i.join() print('end')
二、继承式调用
from multiprocessing import Process #导入进程模块下的process类 import time class MyProcess(Process): #自定义多进程类,继承Process def __init__(self,name): super().__init__() #继承父类的init方法 self.name = name def run(self): time.sleep(1) print('hello', self.name,time.ctime()) if __name__ == '__main__': p_list=[] for i in range(3): p = MyProcess('alvin') p_list.append(p) p.start() for i in p_list: i.join() print('end')
为了显示所涉及的各个进程ID,这是一个扩展的示例:
from multiprocessing import Process import os import time def info(title): print("title:", title) print('parent process:', os.getppid()) #父进程ID print('process id:', os.getpid()) #当前进程ID if __name__ == '__main__': info('main process line') #查看主进程的ID号,当前进程是此.PY文件的ID,父进程是pycharm的进程ID。 time.sleep(1) print("------------------") p = Process(target=info, args=('yuan',)) p.start() p.join()
#运行结果: title: main process line parent process: 12160 Pycharm的进程ID process id: 15560 当前运行的.py文件的进程ID ------------------ title: yuan parent process: 15560 当前运行的.py文件的进程ID
process id: 10584 子进程的进程ID
(七)、进程间通信
1.进程对列Queue
进程队列的调用方法from multiprocessing import Queue
from multiprocessing import Process, Queue import queue #线程对象不适用于进程对象 def f(q, n): #接收父进程的队列对象q,然后才能通信 q.put(n * n + 1) print('son process',id(q)) if __name__ == '__main__': # q = queue.Queue() #此时创建线程队列就没法使用 q = Queue() #需要创建进程队列对象 print('main process', id(q)) for i in range(4): p = Process(target=f, args=(q,i)) #要将进程队列对象q传递给子进程,然后通信 p.start() print(q.get()) #父进程可以调用子进程put的数据 print(q.get()) print(q.get())
2.管道pipe
管道pipe的调用方法from multiprocessing import Pipe
from multiprocessing import Process, Pipe def f(conn): #接收父进程传过来的Pipe实例化对象才能通信 conn.send('f:父进程你好啊') #向父进程send消息 print(conn.recv()) #recv父进程的消息 conn.close() if __name__ == "__main__": parent_conn, child_conn = Pipe() #实例化两个双向管道对象,双向管道即可以收消息也可以发消息 p = Process(target=f, args=(child_conn,)) #将管道实例化对象child_conn传给子进程,才能通信 p.start() print(parent_conn.recv()) #recv子进程发过来的消息 parent_conn.send('p:子进程你好啊') '''运行结果: f:父进程你好啊 p:子进程你好啊 '''
3.Managers
Queue和pipe只是实现了数据交互,只能相互传递数据,并没实现数据共享,不能做到一个进程去更改另一个进程的数据。
Managers调用方法 from multiprocessing import Manager
from multiprocessing import Process, Manager def f(d,l,i): d[i] = 1 d['2'] = '我是唯一' l.append(i) if __name__ == '__main__': with Manager() as manager: #,等同于实例化manager=Manager(),maager.close(),但是上下文管理会自动执行 d = manager.dict() l = manager.list() p_list = [] #存放进程的列表 for i in range(10): p = Process(target=f, args=(d,l,i)) #将实例化的对象传给子进程 p.start() p_list.append(p) for p in p_list: p.join() #等待子进程都执行完,主进程再结束 print(d) #{2: 1, '2': '我是唯一', 1: 1, 4: 1, 0: 1, 3: 1, 5: 1, 6: 1, 7: 1, 8: 1, 9: 1} print(l) #[2, 1, 4, 0, 3, 5, 6, 7, 8, 9] #子进程修改了主进程的数据
(八)、进程同步
进程之间虽然是独立的资源空间(也有例外情况)相互不影响,但一些特殊情况不适用同步锁容易发生混乱。比如在屏幕输出的时候,进程就共用了同一个屏幕资源。容易造成抢占资源,在python2比较明显的是会将两个进程的print输出到同一行
from multiprocessing import Process, Lock #导入方法Lock #方式1,l.acquire()加锁,l.release解锁,中间的代码需要串行 # def f(l,i): # l.acquire() # print('hello world %s'%i) # l.release() #方式2,利用wiht上下文管理,此处只需要with l就可以了 def f(l,i): #l是进程锁,是父进程传过来的 with l: print('hello world %s'%i) if __name__ == '__main__': l = Lock() for i in range(10): p = Process(target=f, args=(l,i)) p.start()
(九)、进程池
进程池为了降低多进程的开销,限制进程的最大开启数量,节约系统资源。100块砖100个进程执行开销太大。1个进程执行效率又太低,进程池5个进程一组执行,折中解决问题。
进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进程,那么程序就会等待,直到进程池中有可用进程为止。
进程池中有两个方法:
- apply 同步:遇到IO等待数据
- apply_async 异步:我们多进程需要用的是异步
回调函数callback:某个动作或函数执行成功后再去执行的函数,好处在于利用主进程去处理,不需要每个子进程都去执行。比如日志,就不需要子进程都去执行,而浪费资源。
from multiprocessing import Process, Pool #导入方法Pool import time def foo(i): time.sleep(1) print(i) return i+100 #传递给回调函数 def Bar(args): print('logger:',args) #将子进程return的数据进行处理 if __name__ == '__main__': p = Pool(5) #设置进程池序列大小为5,如果不设定,默认为CPU的核心数 for i in range(10): p.apply_async(func=foo, args=(i,), callback=Bar) p.close() #close必不可缺,且必须在join前 p.join() #join如果不写,那么主进程直接执行完成打印end print('end')
(十)、协程
协程,又称微线程,本质上就是一个线程。英文名Coroutine。
优点1: 协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
优点2: 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
缺点:单线程没法使用多核,那怎么利用多核CPU呢?
因为协程是一个线程执行,最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。多进程+协程是并发的最佳解决方案
协程的底层是通过生成器yield实现,完全由开发者来控制切换的时间,不像线程需要抢占。
1、yield简单实现协程
补充:一个函数中有了yield,那么他就是一个生成器,不能直接调用,需要通过next方法调用,如果需要发送数据要用send方法
import time #有yield,consumer就是一个生成器,不能直接调用 def consumer(name): print('--->ready to eat baozi...') while True: new_baozi = yield #运行到此处就会yield会进行状态保存,等待send数据继续执行 print("[%s] is eating baozi %s"%(name, new_baozi)) def producer(): con.__next__() #利用next方法调用 con2.__next__() #利用next方法调用 n = 0 while True: time.sleep(1) print("\033[32;1m[producer \033[0m is making baozi %s and %s"%(n, n+1)) con.send(n) #向consumer发送数据,传给yield con2.send(n+1) #向consumer发送数据,传给yield n += 2 if __name__ == '__main__': con = consumer('c1') #实例化得到生成器对象CON con2 = consumer('c2') #实例化得到生成器对象CON2 producer() #执行函数
2、Greenlet
greenlet是一个用C实现的协程模块,相比与python自带的yield,它可以使你在任意函数之间随意切换,而不需把这个函数先声明为生成器(generator)
缺点:如果有过多的IO操作,都需要程序员手动切换
from greenlet import greenlet def test1(): print(12) gr2.switch() print(34) gr2.switch() def test2(): print(56) gr1.switch() print(78) gr1 = greenlet(test1) #先将函数实例化成一个greenlet对象 gr2 = greenlet(test2) gr1.switch() #通过switch方法进行切换
3、Gevent
相比Greenlet更加完美的方案
# spawn(函数,参数...) 启动一个协成
# joinall 类似于join 只不过
g1.join()
g2.join()
gevent.joinall([g1,g2]) 一次性把所有需要阻塞的协程对象写到一起
# value 获取协成的返回值
#g1.value
import requests,time start = time.time() def f(url): print('GET:%s' %url) resp = requests.get(url) data = resp.text print('%d bytes received from %s.'%(len(data), url)) #耗时11秒 gevent.joinall([ gevent.spawn(f, 'https://www.python.org/'), gevent.spawn(f, 'https://www.yahoo.com/'), gevent.spawn(f, 'https://www.baidu.com/'), gevent.spawn(f, 'https://www.sina.com.cn/'), ]) #串行耗时24秒 # f('https://www.python.org/') # f('https://www.yahoo.com/') # f('https://www.baidu.com') # f('https://www.sina.com.cn/') print("cost time:",time.time()-start)