难得二逼的协程,事件驱动和异步IO

先来回顾一下多线程和多进程把。多线程像是在一个国家内,由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种通信方式。

  1. 阻塞IO: blocking I/O model. IO执行的两个阶段都被阻塞了。
  2. 非阻塞IO:non blocking I/O model. 数据准备阶段,如果数据没有准备好,那么就返回一个error给客户端,客户端再次请求。直到内核空间把数据准备好了,那么客户再次来访问的时候,进入内核将数据考入程序的内存中。
  3. 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学习的长河中垫一块小石头。

时间: 2024-11-05 16:11:42

难得二逼的协程,事件驱动和异步IO的相关文章

# 进程/线程/协程 # IO:同步/异步/阻塞/非阻塞 # greenlet gevent # 事件驱动与异步IO # Select\Poll\Epoll异步IO 以及selectors模块 # Python队列/RabbitMQ队列

1 # 进程/线程/协程 2 # IO:同步/异步/阻塞/非阻塞 3 # greenlet gevent 4 # 事件驱动与异步IO 5 # Select\Poll\Epoll异步IO 以及selectors模块 6 # Python队列/RabbitMQ队列 7 8 ############################################################################################## 9 1.什么是进程?进程和程序之间有什么

python2.0_s12_day9_事件驱动编程&异步IO

论事件驱动与异步IO 事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定.它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理.另外两种常见的编程范式是(单线程)同步以及多线程编程. 让我们用例子来比较和对比一下单线程.多线程以及事件驱动编程模型.下图展示了随着时间的推移,这三种模式下程序所做的工作.这个程序有3个任务需要完成,每个任务都在等待I/O操作时阻塞自身.阻塞在I/O操作上所花费的时间已经用灰色框标示出来了. 在单线程同步模型中,任务按照顺序执行.如果某个

二十、协程

协程的概念 线程:系统级别的 协程:程序根据自己的需求调度.在同一个线程内一段代码在执行过程中会中断然后跳转执行别的代码,接着在之前中断的地方继续开始执行.携程拥有自己的寄存器上下文和栈.协程的优点:1.无需线程上下文切换的开销,协程避免了无意义的调度,性能提高,程序员自己承担调用的责任.2.无需原子操作及同步开销3.方便切换,简化编程模式4.高并发+高扩展+低成本,一个cpu支持上万协程协程的缺点:1.无法利用多核资源,协程本质是单线程,但可以和进程进行多核CPU,一般没需求2.进行阻塞操作,

谁说Python协程是鸡肋的!站出来我不打死他!这么牛逼的协程!

文章思路:本文将先介绍协程的概念,然后分别介绍Python2.x与3.x下协程的用法,最终将协程与多线程做比较并介绍异步爬虫模块. 协程 概念 协程,又称微线程,纤程,英文名Coroutine.协程的作用,是在执行函数A时,可以随时中断,去执行函数B,然后中断继续执行函数A(可以自由切换).但这一过程并不是函数调用(没有调用语句),这一整个过程看似像多线程,然而协程只有一个线程执行. 进群:548377875   即可获取数十套PDF哦! Python2.x协程 python2.x协程应用: y

从python协程理解tornado异步

博客原文地址:http://www.v2steve.com/py_tornado_async.html 刚接触tornado时候最疑惑的问题就是tornado.gen.coroutine是怎么实现的.如何在代码中用同步格式实现异步效果.看了几次源码发现其实就是python协程的一个具体应用.下面从生成器开始,说说tornado的异步. python协程 python利用yield关键字实现生成器,yield就像生化危机里的T病毒,被yield感染的函数都不仅仅是函数,而是一个函数生成器.函数生成

事件驱动与异步IO使用

事件驱动模型:    每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求,网络服务器采用此方式.    目前大部分的UI编程都是事件驱动模型,如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件.事件驱动模型大体思路如下:    1. 有一个事件(消息)队列:    2. 鼠标按下时,往这个队列中增加一个点击事件(消息):    3. 有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick().onKeyDown()等:    4

论事件驱动与异步IO

通常我们写服务器模型,有以下几种模型: 每收到一个请求,创建一个新的进程,来处理该请求 每收到一个请求,创建一个新的线程,来处理该请求 每收到一个请求,放入到一个事件中,让主程序通过非阻塞I/0方式来处理请求 以上几种方式,各有千秋: 第1种方式,由于创建新的进程开销比较大,所以会导致服务器性能比较低,但实现比较简单 第2种方式,由于要涉及到线程的同步,有可能会面临死锁等问题 第3种方式,在写应用程序代码时,逻辑比前面两种都复杂. 综合考虑因素,一般普遍认为第三种是大多数网络服务器采用的方式.

python的协程和异步io【select|poll|epoll】

协程又叫做微线程,协程是一种用户态的轻量级的线程,操作系统根本就不知道协程的存在,完全由用户来控制,协程拥有自己的的寄存器的上下文和栈,协程调度切换时,将寄存器上下文和栈保存到其他地方,在切换回来后,恢复之前保存的寄存器的上下文关系,因此协程能保留上一次调用的状态,每次过程重入的时候,就相当于进入上一次调用的状态 协程一定在单线程中,协程的切换是在线程中切换,和单个线程在cpu之间不停的切换是一样的但是线程切换是cpu控制的,而协程的切换是用户控制的,操作系统根本无感知:协程的切换比线程的切换速

协程(Coroutine)与多线程,多进程

执行多个任务可以使用多线程或多进程. 多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响 多线程中,所有变量都由所有线程共享.而线程间的切换是系统进行调度,无法控制,所以可能 一个进程中的多个线程可能会同时调用某个变量的值,造成变量值的混乱,这时就引进了线程锁,但是线程锁又容易造成死锁,也阻止了多线程的并发. 另外Python 解释器由于设计时有GIL全局锁,导致了多线程无法利用多核.多线程的并发在Python中就是一个美丽的梦. 在Thread和Process中,应当优选Proce