进程与线程的概念,协程(生产者与消费者模型)
并发编程里包括了 进程 与 线程 、协程、I/O多路复用,如下图:
What?
一、何为并发??
一个CPU 在执行叫“并发” 如图:
并发 : concurrency 1、single Processor
2、logically simultaneous processor
举个粟子:
假设一台电脑只有一核(也就是一个CPU) ,这时开了 world QQ 音乐 ,这从我们的感知上是在并行的,事实不是。
我一边写文档,写着写着又去聊天, 还听着音乐,这时CPU的执行是,你写着文档在运行着,然后发现你点到QQ聊天去了,这时将WORLD文档当前执行状态保存着,然后执行你的QQ聊天,聊着聊着又去听音乐, 这时又把QQ程序的执行状态保存着,又去执行听音乐,因为CPU的切换速度在0.0X毫秒间,你没有感知到,这种状态叫 “并发”,如 我们经常所听到的 高并发 ,是一个CPU在执行着多个(进程)。
###执行状态保存的切换: PCB. 进程控制块 Process control block
####程序: 数据集
####运行: CPU运行
二、何为并行??
多个CPU在执行叫“并行”:如图:
parallelism: 1 、multiprocess ,multicore
2 physically simultaneous processing
每个CPU 在执行着每个进程的时候,叫 “并行”, 这种情况很少,毕竟 需要多个物理CPU 执行
1、还有一种情况 :操作系统会切换 ,出现IO操作的时候, 当某一个程序在读写的时候,这个时候是不是占用CPU的,那这时候就是并行
2、固定时间切换: 多个程序(进程)都没有I/O操作时候,就是都运行着。
进程与线程的概念:
1、进程是一个包含多个执行的 容器
2、线程是进程里的最小执行单位
从整体看,并发编程里有进程、线程、协程、IO多路复用,应用程序要运行在操作系统上,就是开了一个进程,而一个进程里有多个线程在运行,
运行就是CPU在运行程序。
进程定义:
由程序、数据集、进程控制块。
程序:读取内在数据二进制
数据集:内存数据二进制
进程控制块:保存当前读取内存二进制,切换到另一个数据集--又读取程序(读取内存二进制)--进程控制块又保存读取数据集二进制
线程定义:
最小执行单位
How?
开线程的方法:
import threading #导入threading 模块 import time def watch_tv(): #子进程 print("watch tv ",) time.sleep(3) print('watch tv done!!!') def listen_music(): #子进程 print('listen music ') time.sleep(5) print('listen music done') watch_obj=threading.Thread(target=watch_tv) #线程对象 listen_obj=threading.Thread(target=listen_music) #线程对象 print('master process done!') # 主线程 最外层运行 watch_obj.start() listen_obj.start()
join() 等 子线程执行完后,再执行主线程
obj.join()
1 join方法的作用是阻塞主进程无法执行join以后的语句,专注执行多线程,必须等待多线程执行完毕之后才能执行主线程的语句。
2 多线程多join的情况下,依次执行各线程的join方法,前一个结束之后,才能执行后一个。
3 无参数,则等待到该线程结束,才开始执行下一个线程的join。
4 设置参数后,则等待该线程N秒之后不管该线程是否结束,就开始执行后面的主进程。
setDaemon: 守护线程:主线程结束后,不管子线程
obj.setDaemon(True)
obj跟着线程结束
import threading,time def watch_tv(): print("watch tv ",) time.sleep(3) print('watch tv done!!!') def listen_music(): print('listen music ') time.sleep(5) print('listen music done',time.time()) number='cctv5' watch_obj=threading.Thread(target=watch_tv) listen_obj=threading.Thread(target=listen_music) watch_obj.setDaemon(True) watch_obj.start() watch_obj.join() #等于accept , 等待watch_obj执行完 listen_obj.start() print('master process done!')
class 自定义:
class Mythread(threading.Thread): def __init__(self,number): threading.Thread.__init__(self) self.number=number def run(self): print('Mythread %s'%self.number,) time.sleep(3) my_thread=Mythread(12) my_thread1=Mythread(34) my_thread.start() my_thread1.start() my_thread1.join() print('master thread')
GIL (全局解释器锁)
进程锁,每个进程在能出一个线程,在python中 进程 的处理有不好的地方 ,所以要完成多线程的处理只能靠协程解决 。
第二个方法就是开多个进程, 但是进程多了有弊端:开销大、切换复杂
在进程里 GIL规定了每个进程里有多个线程,但只允许每次出去一个线程运算,(至于这个线程如何出去的,是线程们自己竞称。)
GIL 是PYTHON创始人加的,我们在用的时候没法修改
但是可以锁住用户程序的进程。让这个进程执行完以后再执行其它的进程,加个用户线程锁: 线程安全
同步锁:
threading.acquire
同步锁:
import time import threading #同步锁 def subNum(): global num #在每个线程中都获取这个全局变量 print('ok') lock.acquire() time.sleep(1) temp=num num=temp-1 #对此公共变量进行-1操作 lock.release() num=100 #设定一个共享变量 thread_list=[] lock=threading.Lock() for i in range(100): t=threading.Thread(target=subNum) t.start() thread_list.append(t) for t in thread_list: #等待所有线程执行完毕 t.join() print('Result:',num)
结果:
死锁 与 Rlock() 递归锁
Lock_d1=threading.Lock() Lock_d2=threading.Lock() class Mythread(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): self.fun1() self.fun2() def fun1(self): Lock_d1.acquire() print('I am Lock_d1') Lock_d2.acquire() print('I am Lock_d2') Lock_d2.release() Lock_d1.release() def fun2(self): Lock_d2.acquire() print('I am Lock_d22 ') time.sleep(3) Lock_d1.acquire() print('I am Lock_d11') Lock_d1.release() Lock_d2.release() if __name__ == '__main__': print('------------game start %s'%time.time()) for i in range(0,2): my_thread_d=Mythread() my_thread_d.start()
因为两个线程 抢占锁, 都没有抢占到。现在所有的操作系统都拥有 “抢占式调度” 官方称(preemptive multitasking ) 抢先式多任务能力
递归锁:
Lock_d1=threading.RLock() # Lock_d2=threading.RLock() class Mythread(threading.Thread): def __init__(self): threading.Thread.__init__(self) def run(self): self.fun1() self.fun2() def fun1(self): Lock_d1.acquire() print('I am Lock_d1') # time.sleep(0.5) Lock_d1.acquire() print('I am Lock_d2') Lock_d1.release() Lock_d1.release() def fun2(self): Lock_d1.acquire() print('I am Lock_d22 ') # time.sleep(0.2) Lock_d1.acquire() print('I am Lock_d11') Lock_d1.release() Lock_d1.release() if __name__ == '__main__': print('------------game start %s'%time.time()) for i in range(0,2): my_thread_d=Mythread() my_thread_d.start()
Event对象:
event.wait() :默认为False
event.set() : 设置 为True
event.clear :恢复event的状态值为False
Semaphore(信号量)
Semaphore管理一个内置的计数器,
每当调用acquire()时内置计数器-1;
调用release() 时内置计数器+1;
计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
实例:(同时只有5个线程可以获得semaphore,即可以限制最大连接数为5):
Why?
什么情况用:
总结:
对于计算密集型任务:python的多线程并没有用
对于IO密集型任务: python的多线程是有意义的
Python一定要使用多核; 必须得开进程 进程弊端:开销大、切换复杂
所以在开发程序的时候要着重 加强学习 协程 + 多进程
还有一个就是:IO 多路复用
协程实现并发:
一个是 之前学习过的yield 实现,yield是当前状态保存,挂起来,在PYTHON中有相应的模块实现,最常用的地方是在IO操作的时候切换,
1、由于是单线程,不能再切换
2、不再有任何锁的概念
想用协程必须借助其它 的模块实现,greenle 是协和的基础库,它下面有gevent eventlet (对于eventlet 需要时做了解)
switch()就是在有IO的情况下切换线程,
还有一个就是joinall,它里面是一个列表
Sleep 模拟IO,在IO时CPU就会切换处理
Joinall() 它里面是一个列表,
第一个处理的时候遇到 IO 切换到另一个函数,
IO模型:
前面的知识, 我们无法监测到IO操作。 但是IO模型可以实现。IO多路复用是IO模型的一种,
有五种:
1、阻塞 IO
2、非阻塞 IO
3、IO多路复用 #有自己的监听多个连接 #重点
4、异步IO
5、驱动信号
操作系统内核空间,用户空间
#在发数据的时候,应用程序在compute1上,先从自己的操作系统中的用户空间转换成内核空间-->网卡 经过网络 发送到另一台电脑的 网卡--> 到对方的操作系统的内核空间,内核空间转换成用户空间(也就是应用程序(用户态)),你就能看到这条信息了
1、阻塞IO 非阻塞 IO多路复用 就是同步
有阻塞就是 同步
Sock.accept() 发送系统调用 用户态切换内核态。这时内核空间一直在等消息过来,进程一直在等等待完成,直到完成再做其它的事情
代码查看: accept就已经在阻塞着
2、非阻塞IO
有没有数据都回来,从内核空间读取数据,不管有没有都返回一个信息给用户空间,然后你看到这条消息。
优点: 不用等数据过来,wait for data 时无阻塞
缺点:多次发系统调用(由accept recv),消耗资源 ,数据不是即时接收的
两个阶段中:wait for data 非阻塞
Copy data 是阻塞的状态
3、IO多路复用
Select 只是多路复用中一种。
Select 阻塞 将accept拆成两步 第一步在wait for data, 前面都是accept 在wait for data, sock.accept() sock是一个套接字对象
里面可以监听多个socket 对象(也就是多个文件描述符)
多路复用图:
如下:
Sock 永远只是一个sock(套接字对象永远是服务器的),因为客户端
sock.connect((‘ip’,port)) ,服务端每次拿到的还是自己的socket对象, 每次进来的都是 客户端的conn,所以变化的就是conn
特点:1、全程(wait for data,cpoy) 阻塞
2、能监听多个文件描述符
1、套接字对象 是一个非零整数,不会变
2、收发数据的时候,对于接收端而言,数据先到内核空间,然后COPY到用户空间,同时内核空间的数据清掉
3、对于服务端而言由于TCP三次握手四次挥手,客户端不回,服务端不清除内核数据。
4、异步IO
全程无阻塞
总结:
五个IO模型比较:
前面学到select
现在讲selector 模块,它是select 模块实现的IO多路复用,推荐使用selector
Windows下有select 模块:
Linux: 下有select poll epoll #select 是效率最低的一种
select 缺点:
1、每次调用select ,select都要将所有fd(文件描述符),拷贝到内核空间,导致效率下降。
<socket.socket fd=216, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9000)>
2、遍历所有的fd是否有数据访问:(最重要的问题 遍历所有)
3、并且select 还有最大连接数(1024)
Poll : 最大连接数没有限制
epoll: select 和poll 都只有一个函数实现
1、epoll第一个函数:创建epoll句柄,也是将所有fd(文件描述符),拷贝到内核空间,但是只需拷贝一次。
2、第二个函数:回调函数::: 某一个事件(函数、动作)成功后,会触发的函数,为所有的fd绑定一个回调函数,一旦有数据访问触发该 回调函数,回调函数将fd 放到链表中
3、第三个函数::判断链表是否为空
示例:
#!/usr/bin/env python #!-*- coding:utf-8 -*- import selectors #基于select 模块实现的IO多路复用,推荐使用这个 import selectors import socket sock=socket.socket() sock.bind(('127.0.0.1',9000)) sock.listen(5) sock.setblocking(False) sel=selectors.DefaultSelector() #根据具体平台选择最佳IO多路,比如在linux上,会为我们epoll def read(conn,mask): try: data=conn.recv(1024) print(data.decode('utf-8')) except Exception: sel.unregister(conn) send_data=input('>>>: ') conn.send(send_data.encode('utf-8')) def accept(sock,mask): conn,addr=sock.accept() sel.register(conn,selectors.EVENT_READ,read) sel.register(sock,selectors.EVENT_READ,accept) #注册谁,就是监听谁(真正监听的是select) #sock 有变化,accept函数会运行。就不用像写select 那样麻烦了。accept 是触发函数 while True: print('wating..') events=sel.select() #第一个是返回的sock对象,第二个是mask #[(key,mask),(key,mask)] for key,mask in events: print(key.data) #2 conn #1 accept :<function accept at 0x0000000000917F28> print(key.fileobj) #sock: <socket.socket fd=216, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 9000)> func=key.data # obj=key.fileobj # func(obj,mask) #ACCEPT(sock,mask) #2 read(conn,mask)
队列:queue #是一种数据类型,默认是先进先出的数据类型
使用方法:
put()
get()
join 和 task_done配合使用
示例:
import queue # q=queue.Queue(3)#q就是队列对象 ,先进先出 3管道最大值 # q.put(111) # q.put('hello') # q.put(222) # q.put(123)#到第4值已经put 不进去了,就会报错 # # # # ret=q.get() # print(q.get()) # print(q.get()) # print(q.get()) # print(q.get(False)) #join 和task_done q=queue.Queue(5) q.put(111) q.put(222) while not q.empty(): #判断不为空的时候值取完 print(q.get()) # print(q.get()) # q.task_done() # print(q.get()) #这个时候已经取完值,仍会卡住 因为没有用task_done # q.task_done() # q.join() print('ending')
Queue模式:先进后出:
q=queue.LifoQueue() # q.put(123) q.put(432) print(q.get())
优先级:
生产者消费者模型:
生产者 生产太多的消息 消费者 一处理一条,消息过剩
生产者消费者模型 解决的是耦合问题
这个会涉及到消息中间件 ribtmq 与对比着 现在学习的queue 学习
线程进程是操作系统的内容。
回顾:
进程:最小的资源管理单位(盛放线程的容器)
线程:最小的执行单位
串行,并行,并发
串行:执行完一个,才执行第二个
并行:要有多个CPU,每个CPU执行一个进程,称为并行,只能通过进程实现,但是cpython有GIL锁,锁住了每个进程,每个进程只能出一个线程被执行。
(同一时刻同一进程只能有一个线程执行)
并发:一个CPU ,执行多个线程,线程之间的切换,与串行的区别就是:有切换,
Python 线程库:threading
现在有三个线程并发的向下执行。
自定义:
线程对象的方法:
Join 主线程与子线程的关系。 主线程等子线程。
setDaemon 守护线程,守护A,等B结束
程序直到不存在非守护线程时退出
其它方法:
同步锁:
由于多线程处理公共资源
互斥锁:只允许一次一个线程执行完才能执行下一个
Smaphore 允许多个线程并发执行。连接量,一同只允许20个连接。
协程:
协程:又称微线程、纤程。 释义:相互配合工作的一个过程;
∆子程序,或者称为函数 ,例如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后A执行完毕。
∆协程是在执行过程中-----子程序内部中断,转而执行另一个函数(子程序),另一个子程序执行完返回,接着执行前一个中断的程序
∆通常用到的是生产者和消费者模型,
ø协程的优势:优势就是协程的执行效率,因为子程序切换不是线程切换,而是由程序自身控制 ,因此,没有线程切换的开销,和多线程比,线程数量越多,
协程的性能优势就越明显。
ø协程的优势二:没有多线程锁机制,因为只有一个线程,在协程中控制 共享资源不加锁,只需要判断状态就好,所以执行效率比多线程高很多
∆因为协程是一个线程执行,那怎么利用多核CPU? 方法:多进程+协程,即充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
python中对协程的支持是通过generator实现的。
在generator中,不但可以通过for循环来迭代,还可以不断调用next() 函数获取由yield语句返回的下一值。
python 的yield 不但可以返回一个值,它还可以接收调用者发出的参数 。
传统生产者+消费者模型,一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。
如果改用协和,生产者生产消息后(send),直接通过yield 跳转到消费者开始 执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:
import time #生产者就是吃饭拉屎,消费者就是吃生产者的屎后回应 def consumer2(): r = '' while True: n = yield r if not n: return print('[CONSUMER] Consuming %s----------' % n) r = '吃完了' def consumer1(): r = '' while True: time.sleep(1) n = yield r if not n: return print('[CONSUMER] Consuming %s...' % n) r = '200 OK' def produce(c): c.send(None) n = 0 while n < 5: n = n + 1 print('[PRODUCER] Producing %s...' % n) r = c.send(n) print('[PRODUCER] Consumer return: %s' % r) c.close() def produce1(c): c.send(None) n = 0 while n < 5: n = n + 1 print('[PRODUCER] Producing %s...' % ('饭')) r = c.send('屎') print('[PRODUCER] Consumer return: %s' % r) c.close() c1 = consumer1() c2=consumer2() produce1(c2) produce(c1)
结果:
如图:
这次消费者不吃屎:
import time #生产者就是吃饭拉屎,消费者就是吃生产者的屎后回应 def consumer2(): r = '' while True: n = yield r #吃到的饭 if not n: return print('[CONSUMER] Consuming %s----------' % n) #打印吃到的饭 r = '200ok' #回应 def produce1(c): c.send(None) #初始化 。。说:准备好”开始吃饭了 n = 0 while n < 5: #5个菜可以吃 n = n + 1 print('[PRODUCER] Producing %s...' % n) #吃饭 r = c.send(n) #给消费者吃同样的饭 print('[PRODUCER] Consumer return: %s' % r) #消费者吃完饭的回应 c.close() #吃完收工 c2=consumer2() produce1(c2) #给哪个消费生产消息
总结:∆1、生产者 send(None) 启动 生成器。
∆2、send(n) 切换到consumer执行
∆3、consumer 通过yield 接收到生产者的消息--> 处理后,又通过yield 把结果传回
∆4、produce 拿到consumer处理的结果,继续生产下一条消息;
∆5、produce决定不生产了,通过c.close()关闭生产,不生产也就是关闭了消费,整个程序结束。
再总: 整个程序没有锁,在一个线程内由produce和consumer协作完成任务,所以称为‘协程’,而不是线程的抢占式多任务。