5.python基础之进程、线程、协程
一、线程相关介绍
python解释器:Cpython Jpython等,解释完后出CPU来执行,它是由系统来调动CPU的,内存 CPU disk都由OS来调动,解释器通过OS来调CPU
每个.py运行起来就是一个进程
1.1 什么是多线程?
多线程:线程是系统OS进行调度的最小单位,一个线程就是一个指令集
引入:单进程与多线程对比
单进程: import time begin=time.time() def foo(): print('ok') time.sleep(1) def bar(): print('bar') time.sleep(2) foo()---它只有执行完后才能执行bar(),但foo()里有个sleep不占CPU,所以bar()要等着,有线程后,这foo在sleep时bar就会执行,不仅是sleep还有IO等待也会抢占CPU bar() end=time.time() print(end-begin)-----这个是按顺序执行,是一个主线程 多线程 import time import threading begin=time.time() def foo(n): print('ok') time.sleep(1)-----------IO密集型,要等待IO def bar(n): print('bar') time.sleep(2) #foo() #bar() target=foo,args=(2,3) t1=threading.Thread(target=foo,args=2,)---相当于foo(2),这就是创建了一个子线程对象 t2=threading.Thread(target=bar,args=1,) t1.start()--执行子线程 t2.start()--执行子线程 end=time.time() print(end-begin)---主线程 这三个线程就会抢占CPU,没有先后顺序,在sleep时可以处理其他问题,不是串型的要等待了,一个主进程二个子线程
1.进程与线程总结:
一个进程可以有多个线程,一个.py就是一个进程,只有一个主线程,进程间数据不共享,但线程可以,一般线程运行比主进程慢,因为线程用于切换了,
IO密集型用多线程会快于单线程,CPU密集型用单线程会快于多线程(CPU密集不用等待),CPU密集时IO等待很少,所以CPU密集python多线程没优势,
但多核CPU也不能并行原因如下
GIL:python解释器的全局锁GIL
.py | Cpython---与python解释(是开发环境能补全,也能找到对应解释器解释)的关系:在pycharm里file-Export settings这里是定义解释器的 | os | CPU IO 等硬件
总结:
多核CPU不能同时运行的原因只有Cpython有这个问题,原因是Cpython有一个全局锁GIL,在同一时刻只能有一个线程进入解释器,
所以同一时刻只能有一个线程进CPU但解释器可以接受多进程,所以多进程可以在多核CPU上运行,但进程不共享数据,多开销,还可以用另一个办法:
协程+多进程 多进程里用协程来协调线程
2.线程与进程区别:子进程直接复制主进程的, 开进程的消耗比线程要多,进程的地址空间是独立的而线程共用进程里的内存空间
3.join与Daemon(守护线程/进程)
t1.join()----用于阻塞t1的,t1不结束下面不走,变成串型,只有结束了才向下走
t.setDaemon(True):当主进程里的指令跑完了,子线程t就不会影响主进程,如果子线程还有等待及后的操作不再运行了,
子线程变为守护线程后就守护主进程,只要主进程完后子线程就结束了,子线程不是主进程的指令,不是等子线程执行完进程
才执行完他们是同步的
threading.current_thread(Thread-1)----显示当前线程名及进程名eg:Thread-1等不同线程可以是不同名
1.2 创建线程
1. 方式一
import threading
t1=threading.Thread(target=foo,args=2,)
t1.join()等方法
2. 方式之二:通过类创建线程
class MyThread(threading.Thread)--继承类 def __init__(self,num) threading.Thread.__init__(self)---参数在__init__里去添加 self.num=num-----是一个对象这个值写到对象里它可以调用MyThread里的所有方法比如run等 def run(self):---父类的重写 print('running on number:%s'%self.num) time.sleep(3) if __name__ == '__main__' t1=MyTread(1)---所以MyTread(就自动创建线程了) t1.start()
1.3 线程锁
同步锁(线程)与GIL有什么关系?二者功能不一样 当一个进程里有很多个线程一起执行时,如果线程里有先后顺序 num=100 def addNum() global num temp=num print('ok') num=temp-1 thread_list=[] for i in range(100): t=threading.Thread(target=addNum) t.start() thread_list.append(t) for t in thread_list: t.join()---所有线程执行完毕才打印主进程 print(num) 注意:这个结果不一定是0,每次执行不一样,原因,线程一起用cpu,一个线程不是连续使用CPU的,所以会使用 后面的线程先执行,所以100不一定被拿一次,若结果是1证明100次循环中有一次是重复操作,CPU不断切换线程,所以结果不一定的 对于有顺序的操作,这种情况可以加锁解决? 加锁: num=100 def addNum() global num r.acquire()----加锁 temp=num print('ok') num=temp-1 r.release()----释放锁,在加锁与释放锁中间的这个代码是必须执行完才能进行下一次这样的操作,这段代码就是串行的了,但其它没加锁的代码还是可以多线程 thread_list=[] r=threading.Lock()-----创建个锁对象 for i in range(100): t=threading.Thread(target=addNum) t.start()//每个线程执行addNum函数 thread_list.append(t)//生成100个线程来处理函数里的指令 for t in thread_list: t.join()---所有线程执行完毕才打印主进程 print(num)
1.4 死锁与递归锁
1.死锁
死锁: if __name__ == "__main__" lockA=threading.Lock() lockB=threading.Lock()---开了二把锁 threads=[] for i in range(5): threads.append(myThread())--5个线程对象 for t in threads: t.start() for t in threads: t.join() def doA(): lockA.acquire() lockB.acquire() lockB.release() lockA.release() def doB(): lockB.acquire() lockA.acquire() lockA.release() lockB.release() 当线程的作用是执行doA与doB时,有多个线程时,比如二个第一个线程把doA执行完后,第二个线程就可以进了 当第一个线程拿到doB的B锁时,第二个线程拿到doA的A锁,这时一二线程就走不下去了,对方都拿不到下面的锁了,线程就死锁了
如何解决死锁?使用递归锁
if __name__ == "__main__" lock=threading.RLock()----递归锁,可以重用,里面有个计时器,用一次加一次,释放一次减一次 threads=[] for i in range(5): threads.append(myThread())--5个线程对象 for t in threads: t.start() for t in threads: t.join() def doA(): lock.acquire()---AB都用一个锁,都是是对应解开的,不解开其他线程进不去 lock.acquire() lock.release() lock.release() def doB(): lock.acquire() lock.acquire() lock.release() lock.release()
1.5 信号量
信号量也是锁,锁也可重复用与同步锁的区别:同步锁内嵌锁,而信号量是并行的,锁之间不影响---可用于做连接池,能同时连多少数据库
semaphore=threading.BoundedSemaphort(5)----5个锁对象
semaphore.acquire()
semaphore.release()---释放
1.6 条件变量
也是一种锁同时能实现线程的通信,一个线程通过信号与另个线程通信,影响另个线程是否向下运行 有些线程需要满足条件后才能继续执行,Python提供了threading.Condition对象用于条件变量线程的支持 它除了能提供Rlock()或Lock()的方法外,还提供了wait() notify() notifyAll()方法 threading.Condition(Lock/Rlock)--默认是Rlock wait():条件不满足时调用,线程会释放锁并进入等待阻塞 notify():条件创造后调用,通知等待池激活另一个线程 notifyAll():条件创造后调用,通知等待池激活所有线程 条件变量实例: import threading,time from random import randint class Producer(threading.Thread): def run(self): global L while True: val=randint(0,100)--随机生成一个数 print('生产者',self.name,":Append"+str(val),L) if lock_con.acquire(): L.append(val) lock_con.notify()---告诉下个线程可以动了激活了wait() lock_con.release() time.sleep(3) class Consumer(threading.Thread): def run(self): global L while True: lock_con.acquire()--与生产者同一个锁,wait激活后走这步,所以wait下的print('ok')永远不会执行,因wait激活后走这步,这步下面的len(L)条件不再满足 if len(L)==0: lock_con.wait()---有条件的等着上个线程释放锁这个线程再走,注意:它激活后不是从wait开始走的,而是从上步拿开始 print('ok') print('消费者') del L[0]:---什么意思 lock_con.release() time.sleep(0.12) if __name__=="__main__": L=[] lock_con=threading.Condition()//生成锁 threads=[] for i in range(5): threads.append(Producer())---5个线程对象 threads.append(Consumer())--1个消费线程对象 for t in threads: t.start() for t in threads: t join()
1.7 event(事件):也可用于线程间的通信
event.isSet():返回event的状态值 event.wait():如果event.isSet()==False将阻塞线程 event.set():设置event的状态值为True,所有阻塞池的线程激活进入就绪状态,等待操作系统调度 event.clear():恢复event的状态值为False 实例: import threading,time class Boss(threading.Thread): def run(self): print('加班')------2.向下走 event.set()---3.把False改为True time.sleep(5) print('下班') event.set()---7.改为True后 class Worker(threading.Thread): def run(self): event.wait()---1.等BOSS先走,4.向下走 print('不想加') time.sleep(4) event.clear()--5.Worker运行一段时间后改为False event.wait()---6.又阻塞了等BOSS走 print('下班开心')---8.再走 if __name__ == "__main__" event=threading.Event()//创建事件线程 threads=[] for i in range(5): threads.append(Worker())---5个线程 threads.append(Boss())---1个线程 for t in threads: t.start() for t in threads: t.join() print('ok')
1.8 队列***
定义:是一个数据结构,与字典等一样是存数据的
创建队列: import queue d=queue.Queue(#)--默认无限大(0)一个队列对象,#表数据长度只能插入#个数据,如果多加数据不会报错会阻塞在这,只有前面数据取出去后才能进去不再阻塞,当在要满的那个数据里加0就不会阻塞会报错 d.put('jinxin')---存数据方法 d.put('xiaohu') d.put('hao') print(d.get())---取数据的方法,先进先出的取,FIFO,谁先进谁先出,不是固定先进先出也可以改为先进后出或优先级等 eg: d=queue.Queue(1)--只能放一个数据 d.put('xiaohu') d.put('hao',0)---加0后就报错满了,不阻塞了 print(get()) print(get(0))----当拿多了时,不加0时就会阻塞等别人进,加0就会报错 与列表相比有什么优势:对于单线程来说它与列表一样,但对于多线程来说,列表能被多线程用不安全,可能会操作同一个数据 而对于队列来说不可能拿到同一个值 eg: from random import readint class Producer(threading.Thread): def run(self): while True: r=randint(0,100) q.put(r) print('包子') sleep(1) class Proces(threading.Thread): def run(self): while True: re=q.get() print('拿到包子了') sleep(2) if __name__ == "__main__" q=queue.Queue(10) threads=[Producer(),Producer(),Producer(),Proces()] for t in threads: t.start() queue队列的方法: q.put() q.get() q.qsize()---看对队还有多少数据 q.full():判断是否满了 q.empty():判断是否为空
二、进程相关
2.1 进程相关问题?
1.一个进程一个GIL,所以多进程可以实现并发
2.在unix平台上,当某个进程终结后,该进程需要其父进程调用wait,否则进程成为僵尸进程,有必要时对每个proccess对象调用join()方法,
对多线性只有一个进程不存在此必要性,这里的进程可以在CPU上并行了,有四个核可同时跑四个进程,如果有四个以上,那就再进程切换
2.2 如何实现多进程创建及调用
多进程创建与调用之法一: from multiprocessing import 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='alive',)--创建三个进程并行 p_list.append(p) p.start() for p in p_list: p.join() print('end') 总结:这个程序里执行会有四个进程,一个主进程三个子进程 多进程创建与调用之法二: from multiprocessing import Process import time class MyProcess(Process): def __init__(self) supper(MyProcess,self).__init__()--父类的 def run(self) time.sleep(1) print('hello',self.name,time.ctime())--进程名与时间 if __init__ == '__main__' p_list=[] for i in range(3) p=MyProcess()---直接是一个run函数的对象,创建进程继承了Process p.start() p_list.append(p) for p in p_list: p.join() print('end')
2.3 进程与子进程的关系
os.getppid()---进程号
每一个进程都有父母进程,在pycharm里写一个程序执行,那pycharm就是这个程序的父进程,如果这个程序里有三个子进程,它的父进程就是这个程序的进程
每个子进程都是由父进程创建的
2.4 进程通信及数据共享
用管道与队列可以实现进程通信,共享方式如下:
1.进程队列 Queue from multiprocessing import Process,Queue def f(q,n): q.put([42,n,'hello'])---子进程 if __name__ == '__main__' q = Queue()----在主进程里创建一个进程队列,与线程Queue完全不同 p_list=[] for i in range(3): p = Process(target=f,args=(q,i))---创建一个三个子进程,但这个字进程调用主进程的f函数就会报错,因主进程的数据与子进程不共享,所以这三个子进程拿不到q p_list.append(p) 如何能给子进程?就把这个数据当成参数传给子进程,args=(q,i),由于这个q是复制给子进程的,所以这四个q的地址不同 p.start() print(q.get())---这个是子进程调用f才能执行的,因主进程自己没有调用函数f,所以无法get出数据 print(q.get()) print(q.get()) for i in p_list: i.join() 2.管道Pipe,用法同socket form multiprocessing import Process,Pipe def f(conn): conn.send([42,None,'hello'])---子进程发数据 conn.close() if __name__ == '__main__' parent_conn,child_conn = Pipe() ---管道,给父进程一个子进程一个,若有多个子进程传child_conn,多个子进程共用一个管道 p = Process(target=f,args=(child_conn,))--创建一个子进程 p.start()---运行子进程,子进程发send数据 print(parent_conn.recv())---父进程接受子进程的数据 p.join() 3.Manager:数据共享 from multiprocessing import Process,Manager def f(d,l,n):------n:0-9,它拿的是i d[n]='1'---创建键值对 d['2']=2 d[0.25]=None l.append(n) print(1) if __name__=='__main__' with Manager() as manager:----创建manager对象 d = manager.dict()---这个字典就可以在进程里数据共享了,字典是共用的,操作字典时会被覆盖,可以能共同修改数据 l = manager.list(range(5)) p_list = [] for i in range(10): p = Process(target=f,args=(d,l,i))---十个进程, p.start() p_list.append(p) for res in p_list: res.join() print(d) print(l)
三、协程相关内容
3.1 概念
1. 进程与线程都是抢占资源,所以协程作用是实现并发
2.微线程,是用户态的轻量级线程,就是单线程,是串型的
3.优点缺点:并发没有切换的时间,也没有锁,分配到哪个CPU上去执行
优点:无线程上下文的开销
无需原子操作锁定及同步的开销
方便切换控制流,简化编程模型
高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题,所以很适合用于高并发处理
nginx的一个线程是通过协程实现的
缺点:用不了多核,但可能过多线程或多进程来实现,一个进程只开一个协程,多核就可利用了,阻塞就会都阻塞
3.2 协程创建及使用
1. 协程方式一:生成器yield伪并发
import time import queue def consumer(name): print('---starting') while True: new_baozi = yield 生成器 print('[%s] is eating baozi %s'%(name,new_baozi)) def producter(): next(con)--生成器取值的方法,第一次拿到---starting.. next(con2) n = 0 while n < 5: n +=1 con.send(n)---生成器第一次拿的 con2.send(n) print('ok') if __name__ == '__main__' con = consumer('c1')--生成器对象 con2 = consumer('c2') p=producer() 用yeild实现打印数据的切换而非线程切换,创建生成器对象用next与send进行数据切换
2.gevent模块实现协程(pycharm里装上对应模块)
greenlet模块: from greenlet import greenlet def test1(): print(12) gr2.switch() print(34) gr2.switch() def test2() print(56) gr1.swich() print(78) gr1=greenlet(test1)//创建协程对象 print(gr1)----是一个greep对象 gr2=greenlet(test2) gr1.switch() 注:这里没有创建进程与线程,二个函数如何切换? 用gr1.swich()进test1,在test1里进入gr2.swich()再进行切换
3.gevent模块(是一个三方库)
import gevent def foo(): print('explicit context to foo') gevent.sleep(1)--IO阻塞,注这时不能用time.sleep,因为这个一个进程里切换用time.sleep就卡了进程内切换不了了 print('implicit context switch back to foo') def bar(): print('explicit context to bar') gevent.sleep(1) print('implicit context switch back to bar') gevent.joinall([gevent.spawn(foo),gevent.spawn(bar)]) ---走foo,到IO阻塞就切到bar 这个是IO阻塞时进行切换,这个跟线程的区别,这个切换是由我们自己控制的,线程是抢占的不受控制的
3.3 爬虫实例
from gevent import monkey monkey.patch_all()--补丁,监听IO阻塞,只要有阻塞就切换 import gevent form urllib.request import urlopen---网页需要这个模块 def f(url):---网址 print('get:%s'%url) resp=urlopen(url) data=resp.read()---读页面内容 #with open('xiaohua.html','wb') as f: # f.write(data) print('%d bytes received from %s' % (len(data),url)) f(http://www.xiaohuar.com)---结果创建了xiaohua.html这个文件在本目录把,把xiaohua网的源码都是复制下来 gevent.joinall([ gevent.spawn(f,'https://www.python.org') gevent.spawn(f,'https://www.yahoo.com') ) l=['https://www.python.org',https://www.yahoo.com]