1.IO(磁盘,网络等)操作不占用CPU
计算占用CPU,例如1+1
多线程使用场景:python多线程不适合CPU密集操作型的任务,适合IO密集型的任务(例如socket server )
2.进程
每一个进程都是由默认父进程启动的(每一个子进程都是由主进程启动的)
比如在pycharm启动程序 ,在windows上是pycharm为父进程:主进程的父进程为pycharm
比如在linux终端启动程序,在linux上是terminal为父进程,主进程的父进程为terminal
两个方法:os.getpid() 获取进程号
os.getppid() 获取父进程号
创建进程与线程类似
from multiprocessing import Process
def func(a):
pass
p=Process(target=func,args=(a,))
p.start()
3.多进程(multiprocess)
多进程间的通信:不同进程之间是不允许访问对方内存的,多进程要实现通信,只能通过以下方式
----进程Queue
----pipe 管道
以下为后两种方式详细解释:
(1)进程通过进程Queue进行通信
与线程的queue不一样 只能用于进程通信的特殊的queue叫进程Queue
from multiprocessing import Process,Queue
def f(q):
q.put([42,None,'1'])
if __name__=='__main__':
q=Queue()
p=Process(target=f,args=(q,))
p.start()
print(q.get())
上述过原理为: 在主进程通过进程QUEUE读到了子进程往q里存的数据
解析过程:
其实是两个queue,主进程开启queue,把queue作为参数传入子进程中,是复制了另一个queue给子进程(pickle序列化)
然后 子进程往queue传数据,再pickle反序列化给主进程的queue
只是实现了这个进程的数据传给另一个进程
(2)PIPE管道
from multiprocessing import Process, Pipe
def f(conn):
conn.send([42, None, 'hello from child'])
conn.send([42, None, 'hello from child3'])
print("",conn.recv())
conn.close()
if __name__ == '__main__':
parent_conn, child_conn = Pipe() # 生成管道有两头,返回两个值
p = Process(target=f, args=(child_conn,))
p.start()
print("parent",parent_conn.recv()) # prints "[42, None, 'hello']"
print("parent",parent_conn.recv()) # prints "[42, None, 'hello']"
parent_conn.send(" from hshs") # prints "[42, None, 'hello']"
p.join()
就是生成一个Pipe管道对象,管道有两头,返回两个值,在这里两头没有准确定义必须是传数据的还是收数据的
(2)进程间的数据共享:
本来进程之间内存不能互相访问,但是可以通过manager实现进程间的数据共享
----通过manager
from multiprocessing import Process, Manager
import os
# 进程间的共享数据
# 其实也是copy了十个数据 最后汇总,而且内部加了锁 ,不用人为加锁
def f(d, l):
d[os.getpid()]=os.getpid()
l.append(1)
print(l,d)
if __name__ == '__main__':
with Manager() as manager: # 和 manger=Manager() 一样
d = manager.dict() # 生成一个可在多个进程之间共享、传递的字典
l = manager.list(range(5)) # 生成一个可在多个进程之间共享、传递的list
p_list = []
for i in range(10):
p = Process(target=f, args=(d, l))
p.start()
p_list.append(p)
for res in p_list: # 等待结果
res.join()
l.append("from parent")
print(d)
print(l)
原理:创建manager对象,通过manager.dict()生成一个可以再多进程间共享、传递的字典对象,通过manager.list() 生成一个可以在多进程间共享、传递的列表对象
其实就是copy了十个数据,最后汇总,而且内部加了锁,所以这里不用加锁
4.进程锁
本身进程之间内存不共享,为什么还需要进程锁?
原因:主要是保证在屏幕上打印的时候不乱
from multiprocessing import Process, Lock
# 进程锁
# 本身进程之间内存不共享,为什么还需要进程锁?
# 原因:主要是保证在屏幕上打印的时候不乱
def f(l, i):
#l.acquire()
print('hello world', i)
#l.release()
if __name__ == '__main__':
lock = Lock()
for num in range(100):
Process(target=f, args=(lock, num)).start()
5.进程池
进程启动开销比线程启动大很多,所以python有进程池 概念防止电脑崩溃。线程池可以通过信号量自己定义
在windows上启动多进程跟linux不一样 ,要import freeze_support或者加if name==‘main’
from multiprocessing import Process,Pool,freeze_support
# 在windows上启动多进程跟linux不一样 ,要import freeze_support或者加if __name__=='__main__'
import time
import os
def Foo(i):
time.sleep(2)
print("in process ",os.getpid())
return i+100
def Bar(arg):
print("--->exec done:",arg)
if __name__=='__main__':
#freeze_support()
pool=Pool(5) # 和 pool=Pool(process=5)一样 意思为 允许进程池同时放入5个进程,同时运行的进程只有五个
for i in range(10):# 启动了 但是被放进进程池的进程才会运行
pool.apply_async(func=Foo,args=(1,),callback=Bar) # callback=回调 执行完Foo 再执行Bar ,注意:回调是主进程调用的而不是子进程,例如备份数据库时候只用父进程创建连接,子进程去回调父进程的连接 而不用多次创建连接
# pool.apply(func=Foo,args=(i,)) # 串行
# pool.apply_async(func=Foo,args=(1,)) # 并行
print('end')
pool.close()
pool.join() # 等所有进程结束 不写join 主进程不会等子进程结束
# 注意python的要求(官方文档都没写),先close再join(死记硬背),如果把join注释了,不等子进程执行完毕程序就关闭了
过程:通过Pool()创建一个pool对象。
pool=Pool(5) 与 pool=Pool(process=5) 表示允许同时放入5个进程,同时运行的进程只有五个,跟多线程的信号量一样。
在for循环里启动进程,但这是还没有启动,只有放入进程池的进程才能运行。
pool.apply(func=Foo,args=(1,)) 这是串行运行进程
pool.apply_async(func=Foo,args=(1,)) 这是并行运行进程(异步)
pool.app;y_async(func=Foo,args=(1,),callback=Bar)
这里callback是回调函数,执行完Foo,再执行Bar,注意:回调是主进程的调用而不是子进程的调用,例如备份数据库时候只用父进程创建连接,子进程去回调父进程的连接
而不用多次创建连接
最后很重要的一点:python的官方要求文档都没写,必须先close再join(死记硬背),如果把join注释了,不等子进程执行完毕程序就关闭了。(而不像多线程时,先join再close)
pool.close()
pool.join()
另外:这里再将一下_ _name _ _的的意思:
如果再程序里加入 if name== 'main’则是手动执行的时候会执行
如果把该程序当做模块被另外程序import后,另外的程序不执行 if name== 'main’里的内容
____ main__代表主进程的__name_ 子进程为__mp_main__的__name__
所以 if name=='main’是判断是否__name__为主进程
(就是当前该程序手动执行自己,被其他模块导入则不执行里面的内容)
6.协程
(微线程)是一种用户态的轻量级线程
协程为什么很快:协程是遇到IO操作就切换,所以剩下都是CPU操作就很快
线程的切换是保存在cpu的寄存器里,而协程拥有自己的寄存器上下文和栈。
可以在单线程下实现并发效果(实际上还是串行 因为切换时间很快,所以在用户视角下是并行)
优点:
1.无需线程上下文切换的开销
2.无需原子操作的锁定及同步的开销(改变量可以叫原子操作)
3.方便切换控制流,简化编程模型
4.高并发、高扩展、低成本:一个cpu支持上万的协程都不是问题缺点:
1.因为是单线程,无法利用多核资源,它不能同时将单个CPU的多个核用上(不能多核),协程
需要和进程配合才能运行在多CPU上,除非是CPU密集型应用
2.进行阻塞(Blocking)操作(如IO)会阻塞掉整个程序
注意:当一个函数含有yield关键字时 ,第一次调用它是变成一个生成器,必须加__next__()才执行
两种切换方式
(1)手动切换 greenlet
from greenlet import greenlet
# 手动切换 gevent 封装了greenlet
def test1():
print(12)
gr2.switch() # 切换gr2
print(34)
gr2.switch()
def test2():
print(56)
gr1.switch() # 切换gr1
print(78)
gr1 = greenlet(test1) #启动一个协程
gr2 = greenlet(test2)
gr1.switch()
(2)自动切换 gevent
gevent自动IO切换 封装了greenlet (手动IO切换),可以通过greenlet实现并发同步或异步编程
import gevent
# 自动IO切换
def foo():
print('Running in foo')
gevent.sleep(2)
print('Explicit context switch to foo again')
def bar():
print('Explicit精确的 context内容 to bar')
gevent.sleep(1)
print('Implicit context switch back to bar')
def func3():
print("running func3 ")
gevent.sleep(0)
print("running func3 again ")
gevent.joinall([
gevent.spawn(foo), # 启动协程
gevent.spawn(bar),
gevent.spawn(func3),
])
代码中 sleep只是gevent模拟io消耗的时间代指类似于io的消耗的时间
gevent.spawn(协程) 启动协程,遇到IO操作自动切换
运行过程为:
foo 先打印第一句 然后遇到io 切换给bar打印第一句 然后遇到io 切换给func3 打印第一句 然后遇到io
又给foo 发现foo还在等io,就给bar bar 也在等io ,又给func3 io结束后 打印第二句,返回给foo 还在等io给bar,bar 等io结束打印第二句 发给foo,foo等待io结束 打印foo第二句
这个单线程的异步执行,只要2s ,但是不使用协程要3s,要2s是指单线程中io等待时间最多的点
通过gevent自动切换协程能实现什么?
(1)很牛逼的实现:gevent实现大并发单线程socket server(通过协程)
from gevent import socket, monkey
monkey.patch_all()
# 通过gevent自动切换协程实现单线程下的socket并发(很牛逼!!!)
# 大并发socket server(单线程)
def server(port):
s = socket.socket()
s.bind(('0.0.0.0', port))
s.listen(500)
while True:
cli, addr = s.accept()
gevent.spawn(handle_request, cli)
def handle_request(conn):
try:
while True:
data = conn.recv(1024)
print("recv:", data)
conn.send(data)
if not data:
conn.shutdown(socket.SHUT_WR)
except Exception as ex:
print(ex)
finally:
conn.close()
if __name__ == '__main__':
server(9999)
(2)gevent实现简单大并发单线程爬虫
import gevent,time
from urllib import request
from gevent import monkey
#简单协程大并发爬网页
monkey.patch_all() # 把当前程序的所有io操作给我单独做上标记
def f(url):
print('GET:%s'%url)
resp=request.urlopen(url)
data=resp.read()
print('%d bytes received from %s .'%(len(data),url))
urls=['https://www.python.org/',
'https://www.yahoo.com/',
'https://github.com/'
]
time_start=time.time()
for url in urls:
f(url)
print("同步cost:",time.time()-time_start)
async_time_start=time.time()
gevent.joinall([
gevent.spawn(f,'https://www.python.org/'),
gevent.spawn(f,'https://www.yahoo.com/'),
gevent.spawn(f,'https://github.com/'),
])
print("异步cost:",time.time()-async_time_start)
(没加monkey.path.all()前提下)时间一样是因为gevent 跟urllib没关系 ,gevent不知道urllib在做io ,所以就没有切换,可以通过加入 monkey补丁
monkey.patch_all() # 把当前程序的所有io操作给我单独做上标记,标记他是IO操作,遇到他就切换
7.论事件驱动与异步IO
通常,我们写服务器处理模型的程序时,有以下几种模型:
(1)每收到一个请求,创建一个新进程,来处理该请求
(2)每收到一个请求,创建一个新线程,来处理该请求
(3)每收到一个请求,放入一个事件列表,让主进程通过非阻塞IO方式来处理请求(协程)
io是操作系统执行的(就是利用事件驱动模型把io操作扔到操作系统中一个队列,io执行完后调用回调函数告知你执行完的标记)
上面的几种方式,各有千秋:
第(1)种方法,由于创建新的进程的开销比较大,所以,会导致服务器性能比较差,但实现比较简单
第(2)种方法,由于要涉及到线程的同步,有可能面临死锁等问题
第(3)种方法,在写应用程序代码时,逻辑比前面两种都复杂
综合考虑各方面因素,一般普遍认为第(3)种方式是大多数网络服务器采用的方式
事件驱动模型:
1.有一个事件(消息)队列
2.例如鼠标按下时 ,往这个队列中增加一个点击事件(消息)
3.有个循环,不断从队列中取出事件,根据不同的时间,调用不同的函数,如onClick()、onKeyDown()等
4.事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数
事件驱动编程是一种编程范式,这里程序的执行流由外部事件决定
8.IO多路复用
进程的阻塞:
只有处于运行态的进程,才有可能转为阻塞状态。当进程进入阻塞状态时,不会占用CPU资源文件描述符 fd:
就是一组非负整数,是操作系统内部文件记录表(有序的,存放的是句柄对象)的索引值,操作系统拿到文件描述 符,从文件记录表中找到文件句柄对象,从对象中操作数据。在unix,linux上才有文件描述符的概念缓存IO:
又被称作标准I/O,大多数文件系统默认IO都是缓存IO。在linux缓存IO机制中,数据会先拷贝到操作系统内核的缓存 区中,然后从操作系统内核再拷贝到应用程序的地址空间(比如socket中,两次send会发送在一起(黏包),是因为系统为了减少操作系统内核拷贝
到应用程序的开销。)(内核态—》用户态的数据转换) 缓存IO缺点:这些数据拷贝对cpu以及内存的开销是非常大的
9.IO五种网络模式(有一种驱动IO很少用)
情景:用户有个read操作
1-3都是同步IO(synchronous IO 必须等内核态到用户态的转变)
(1)阻塞IO(blocking iO)
在linux ,默认情况下所有的socket都是阻塞IO (blocking IO) 用户发送read操作到内核
内核中没有数据,在等待数据被发送过来,此时用户进程在等待,当内核中有数据后,再返回给用户。 用户在等待的时候就是阻塞I/O
(2)非阻塞IO(nonblocjing io)
linux下可以通过设置socket为nonblocking
用户发送read操作到内核,内核中没有数据,用户不用等内核是或否有数据,内核没有数据会发送一个error到用户,用户收到
内核的信息做判断,当为error的时候可以去做其他事,收到数据之后再处理数据 所以 nonblocking
IO的特点是用户进程徐不断的主动询问kernel数据好了没有,可以实现用户视角下的单线程多并发
但是在内核态到用户态 如果数据过大 还是会阻塞
(3)I/O多路复用(IO multiplexing或者 event driven IO 事件驱动IO)
常用的select poll epoll 是建立在非租塞IO的情况下,因为非阻塞IO情况下,在等待
接受数据的时候没有阻塞,但是在拷贝数据的时候,如果从内核拷贝到用户的数据太大,则会阻塞,这是IO多路复用要解决的问题。
三种方式 select poll epoll:
select (windows,linux) 例如多个连接 循环这些连接(例如有一百个链接,就循环这个一百个,有一个返回数据就返回给用户),任意一个返回就返回信号(缺点,文件描述符上限1024,当然可以自行修改,如果要循环连接(数组轮询)过多,容易浪费资源)
poll 没有最大文件描述符限制(基于select优化 但还是有select的缺点)
epoll (最流行的,windows不支持,linux2.6内核.Django就是用的这个,例如nginx)
(1)epoll_create 创建一个epoll对象,一般epollfd = epoll_create()
(2)epoll_ctl (epoll_add/epoll_del的合体),往epoll对象中增加/删除某一个流的某一个事件
比如
epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//注册缓冲区非空事件,即有数据流入
epoll_ctl(epollfd, EPOLL_CTL_DEL, socket,
EPOLLOUT);//注册缓冲区非满事件,即流可以被写入(3)epoll_wait(epollfd,…)等待直到注册的事件发生
(注:当对一个非阻塞流的读写发生缓冲区满或缓冲区空,write/read会返回-1,并设置errno=EAGAIN。而epoll只关心缓冲区非满和缓冲区非空事件)。
4.异步IO(asynchronous IO,不用等内核态到用户态)—用得少(其实很多叫异步IO都用的是IO多路复用 epoll)
发起一个read操作,立刻返回,所以不会对用户进程产生任何block。然后kernel会等待数据准备完成,然后拷贝到用户内存,
当这一切完成之后,kenel会给用户进程发送一个signal,告诉他read操作已完成。
注意 :这里拷贝完成才会给用户进程发送一个signal,所以用户进程是不会阻塞的,用户进程只是把任务丢给内核,可以去做其他事,当他收到内核发送的signal时就知道数据已经从内核态到用户态了。
以上这四种网络模式图解:
一个单线程下通过select方式实现IO多路复用的socket server 例子:
import select
import socket
import queue
# 单线程下的io 多路复用的selcet实现socket_server
server=socket.socket()
server.bind(('localhost',9000))
server.listen(1000)
# 设置为非阻塞模式
server.setblocking(False) # 不阻塞
inputs=[server]
outputs=[]
while True:
readable,writeable,exceptional=select.select(inputs,outputs,inputs)
print(readable,writeable,exceptional)
for r in readable:
if r is server: # 如果是server 代表来了一个新连接
conn,addr=server.accept()
print("来了个新连接",addr)
inputs.append(conn) # 是应为这个新建立的连接还没发数据过来,现在就接受的话程序就要报错
# 所以要想实现这个客户端发数据来时,server端能知道,就需要让select再监测这个conn
else: # 如果是之前的conn 表示发数据了
data=r.recv(1024)
print("收到数据",data)
r.send(data)
print("send done..")
server.setblocking(False) 设为非阻塞模式
两个列表 inputs 和 outputs
select 去循环去inputs列表里的对象
在inputs列表里的对象首先必须要是server本身,其次是conn连接实例。
如果是server 代表来了一个新连接
然后 inputs.append(conn) 是应为这个新建立的连接还没发数据过来,现在就接受的话程序就要报错 ,所以要想实现这个客户端发数据来时,server端能知道,就需要让select再监测这个conn
如果是conn 表示这个连接开始发数据了