先来回顾一下多线程和多进程把。多线程像是在一个国家内,由A点往B点搬运东西,一条线程就是一条路,多条线程就是开启多条路,然后每条路上可以运输东西。多进程就像多个国家,每个国家里面在执行自己的事情。
然后轮到今天的主角:协程出场
1.携程
corotine, 是一种用户态的轻量级线程,被称为微线程。是自己控制的,cpu不知道其存在。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。
协程的好处:
⦁ 无需线程上下文切换的开销
⦁ 无需原子操作锁定及同步的开销
⦁ "原子操作(atomic operation)是不需要synchronized",所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序是不可以被打乱,或者切割掉只执行部分。视作整体是原子性的核心。
⦁ 方便切换控制流,简化编程模型
⦁ 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
缺点:
⦁ 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。 (多个进程占用多个CPU,在进程中启用线程,然后在线程中启用协程)
⦁ 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
协程是单线程的,是串行的,但是要让他看起来像是并行,需要CPU进行不断的切换。
但是什么时候进程切换呢?是在遇到IO操作的时候。
那什么时候在切回去呢? 在IO操作结束后,自动切回去。那系统是怎么实现检测的呢?,python中有一个封装好的模块:gevent可以帮助实现切换
greenlet 是封装好了的协程,可以手动执行切换,是手动挡
gevent 是自动挡,自动挡(genent)封装了手动挡(greenlet)。
import gevent
def foo():
print('Running in foo')
gevent.sleep(2)
print('Explicit context switch to foo again')
def bar():
print('Explict context to bar')
gevent.sleep(1)
print('Implicit context switch to bar')
gevent.joinall([
gevent.spawn(foo), #生成,产生,发起
gevent.spawn(bar),
])
注意在foo中切到了bar中,然后在bar中切回到foo中,但这时候foo中还是sleep,所以会卡住1秒。
Alex的博客上有一个图很经典,清晰地描述了单线程,多线程,还有携程之间的关系。http://www.cnblogs.com/alex3714/articles/5248247.html
2.事件驱动
生活中很多微小的事情,其背后可能用到的思路会很复杂;而我们觉得它简单是因为我们不知道复杂的思路,但是我们一旦发现了,就会觉得原来这么微小的事情中居然包含着这么复杂的道理。
那就从微小的:鼠标点击事件开始讲起。当鼠标点下word图标,那么系统就会启动打开word这个命令,但是在这当中,鼠标是不是就不能活动了呢?当然不是,鼠标还可以点击excel图标,打开excel. 那鼠标是怎么做到能时刻待命,等待我们的信息的呢?难道是创造一个线程,然后时刻检查鼠标有没有按下吗?这样这个线程一直都在工作,会占用很大资源。为了解决这个问题,牛人们就设计了“事件驱动模型”。 简单说就是有一个消息队列,然后每次来的消息,都放到消息队列中,然后系统每次都从队列中取出事件,调用不同的函数。因为事件一般都保留有自己的指针,所以每个消息都有独立处理的函数。
事件驱动模型比多线程更加方便(不用加锁),比单线程更加高效(不用占用过多的资源)。
3.IO
IO 是什么?Input 和Output,相当于是门户。其实有两种IO, 磁盘的IO,网络的IO。我们这里讨论一下网络的IO。
经常会听到”IO阻塞“这个词,那这是什么意思呢?是指进程因为期待的一些事情没有发生,或者请求没有得到回复,而自己进入等待模式,相当于IO就阻塞了。
缓存IO: Linux的缓存IO机制中,操作系统会先把IO的数据缓存在文件系统的缓存页中。也就是说数据会先被拷贝到内核的缓冲区,然后再拷贝到应用程序的内存。数据会从内核态——>用户态。
数据在传输过程中,需要在自己的内存空间和内核之间进行多次拷贝。所以数据拷贝过程中对CPU和内存的消耗是很大的。
IO 模式: 1.数据准备,2.将数据从内核拷贝到程序的内存空间。因为这两个步骤,所以Linux生成了3种通信方式。
- 阻塞IO: blocking I/O model. IO执行的两个阶段都被阻塞了。
- 非阻塞IO:non blocking I/O model. 数据准备阶段,如果数据没有准备好,那么就返回一个error给客户端,客户端再次请求。直到内核空间把数据准备好了,那么客户再次来访问的时候,进入内核将数据考入程序的内存中。
- I/O多路复用:select, poll, epoll会轮流循环socket,当某个socket有数据到达的时候,那么就会通知用户。当用户进程调用了select, 那么整个进程就会被卡住。(I/O多路复用,适用于多个socket)
其中第3种模式就是协程中的一种。Python中有select和selctors两个模块,可以实现IO多路复用。
select 是比较好理解。需要3个参数 readable, writeable,exceptional = select.select(inputs,outputs,inputs)。
import select
import socket
import queue
server = socket.socket()
server.bind(('localhost',9999))
server.listen(1000)
#在接受之前,得设置为非阻塞模式
server.setblocking(False) #False是非阻塞
inputs = [server,]#用户存放socket链接的列表
outputs = []
#每一个连接都要单独有一个队列
msg_dic = {}
#注意是用户发一次数据,那么就激活了readabld,readable一激活,就会接收数据;将链接放到outputs.remove(i),就会激活writeable, writeable一激活,就会开始从存放的队列中取出数据,然后发送数据(或者不发送)
while True:
readable, writeable,exceptional = select.select(inputs,outputs,inputs)
# 第一个参数是传100个socket,要是有一个活动,那么select就会返回有活动,可读;
# 第二个参数是outputs...可写;
# 第三个参数inputs是出现异常报错,检测的还是100个socket链接
print('readable is',readable)
print('writeable is',writeable)
print('exceptional is',exceptional)
for i in readable:
if i is server: #代表来了一个新链接
conn,addr = server.accept()
print(conn,addr)
print("来了一个新链接",conn)
inputs.append(conn) #因为这个新建立的链接还没有发数据,所以如果接受,那么就会报错。所有需要实现这个客户端发数据来server时,就需要让select再检测这个链接。
# inputs = [server,conn],但是select会内部做循环,所以inputs = [conn].如果返回时server那么就代表来新连接,如果返回的是conn那么就直接接受数据。
msg_dic[conn] = queue.Queue() #初始化一个队列,后面存 要返回给这个客户端的数据
else:
try:
data = i.recv(1024) #注意,如果有两个client,那么client1连了之后,client2也连了,conn这时候其实是conn2是client2的,所以这时候client1给server传的时候就会出错。所以要用i来查看是哪个conn发的
print("收到数据",data)
#然后把要给这个连接的数据放到它的队列中, 那些链接需要返回数据的,那就先放到output中。
msg_dic[i].put(data)
outputs.append(i) #放入返回的链接队列中。注意output是在下次循环时候才会有数据outputs = ['conn1','conn2']
except Exception as e:
print("{0}链接出错了{1}".format(i,e)) #如果链接断开,那么需要清理链接
if i in outputs:
outputs.remove(i) #清理已经断开的链接
inputs.remove(i)
del msg_dic[i]
for w in writeable: #要返回给客户端的列表
try:
next_msg = msg_dic[w].get_nowait()
except queue.Empty:
print("client {0} queue is empty".format(w))
outputs.remove(w) #确保下次循环的时候,writable不再返回已经处理完毕的链接
else:
print("sending message {0} to {1}".format(next_msg,w))
w.send(next_msg.upper())
for e in exceptional:#如果客户端断开,那么就把这个实例从input和output中删除
print("handling exception for",e.getpeername())
if e in outputs:
outputs.remove(e)
inputs.remove(e)
del msg_dic[e]
selectors 涉及到了回调函数,比较简洁,但是理解起来有点困难(至少我现在还是不会单独使用,每次使用之前还得看一下代码)
import selectors
import socket
sel = selectors.DefaultSelector()
def accept(sock, mask):
conn,addr = sock.accept()
print("accepted",conn,"from",addr)
conn.setblocking(False)
sel.register(conn,selectors.EVENT_READ,read) #把链接再次注册到连接中,但是调用的回调函数是read.如果客户端的数据发过来了,就调用read
def read(conn,mask):
data = conn.recv(1024)
if data:
print("echoing",repr(data),'to',conn)
conn.send(data)
else:
print("closing",conn)
sel.unregister(conn)
conn.close()
sock=socket.socket()
sock.bind(('localhost',9999))
sock.listen(100)
sock.setblocking(False) #设置为非阻塞模式
sel.register(sock,selectors.EVENT_READ,accept) #注册了新链接,那么就调用accept
while True:
events = sel.select()#默认是阻塞,有互动链接就返回活动的链接列表
print('event is',events)
for key,mask in events:
callback = key.data #相当于调用回调函数,
callback(key.fileobj,mask) #socket链接, fileobj = conn
第十周的作业,是基于完全理解select的基础上。由于我理解得不是很好,所以就花了很长的时间,主要是因为没有拎清楚。附上我理解的流程图,在python学习的长河中垫一块小石头。