zoukankan      html  css  js  c++  java
  • python2.0_s12_day9_协程&Gevent协程

    Python之路,Day9 - 异步IO数据库队列缓存
    本节内容

    Gevent协程
    SelectPollEpoll异步IO与事件驱动
    Python连接Mysql数据库操作

    协程
    1.协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是协程:协程是一种用户态的轻量级线程。(操作系统跟不知道它存在),那你指定协程的实现原理是什么吗?
    我们来聊聊协程的实现原理:
    首先我们知道多个线程在一个单核CPU上进行并发,它的操作过程是,操作系统能调动的最小单位是线程,当操作系统触发多个线程到一个单核心的CPU上,接下来线程如何处理,怎样切换就不是操作系统能控制的了,那是由谁控制的,由硬件CPU,或者其他硬件控制的.它利用一个机制,将每一个线程切片,然后轮询将每一个线程的分片交给CPU处理.
    但是你要知道,CPU同一时刻只能处理一个线程的分片.(那它是怎样将每一个分片对应到相应的线程的,就是通过CPU自己的寄存器,上下文,堆栈信息存储的.总之利用这些CPU会对应每一个线程的处理数据包.)也就是说,CPU也还是串行处理线程的任务的,只是它切换的特别快,让人类觉得是并行处理的.
    那么这个协程有什么关系呢?当然有关系,协程正是python在代码中模仿单核CPU处理多线程的原理,利用代码造就了一个代码切换的机制,这个机制就是执行完有IO操作或者sleep这种操作的代码块后就切换到其他代码段,这样执行的时间就会缩短.因为串行中要等待处理后结果的操作时,这里用做执行其他代码了,等结果返回后在利用自有的寄存器上下文堆栈等信息对应到相应的代码段即可.
    需要注意的是,单核CPU处理多线程时,硬件在不同线程间切换时要消耗时间.而协程技术所产生的切换动作是在一个线程中进行的.虽然协程的切换也要消耗时间,但是它不涉及到线程间的切换,只是CPU在处理一个线程代码时在代码段间不停的切换,所以协程的切换效率要比CPU单核处理多线程的效率还要高.消耗的时间还要短.
    以上就是python协程实现的原理!
    2.协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:
    协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。



    协程的好处:

    无需线程上下文切换的开销
    无需原子操作锁定及同步的开销
    方便切换控制流,简化编程模型
    高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
    缺点:

    无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
    进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序

    那么我们还有一个疑问,协程和多线程哪个速度更快执行效率更高.
    我们知道Cpython有一个GIL全局解释性锁的特性.那么有这个锁导致的结果是,我有两种猜想:
    1.python启用多线程后,比如8个线程,也许打到了2CPU的8个核心上,但是GIL会导致这些线程在同一时刻只有一个线程在执行.
    2. python启用多线程后,python解释器层控制这在代码上生成的多线程轮询调用C语言的线程,实际上这些多线程,最终只是轮询调用c语言的一个线程接口.
    上面两种对GIL实际的操作的猜测,我偏向于2.因为如果按照1中打到CPU的8个核心上的8个线程,后面就不在受操作系统原生线程的控制,而是硬件调度CPU处理线程里的数据包.所以全局解释性锁根本就控制不了.
    那么如果是情况2 ,就意味着,GIL其实起到的左右和协程一样.那他们的效率应该也差不多吧.但是GIL这个貌似被看作是CPython的诟病的特性,应该效率比协程要低一些.另外多线程虽然根据cpython全局解释性锁的特性,最终轮询调用c语言的线程接口,但是在python内部还是维护着几个独立运行的线程任务,这几个线程任务独立运行,占用空间,但实现出来的效果确又和协程差不多,更不要说最终的效率还比协程低。这么一说我们当然知道协程的优势要远远大于多线程。
    所以结论是,一般如果能用协程方式的程序用协程.并且用了协程后就不要在用多线程,可以用多进程加协程解决协程不能利用多核优势的缺点.
    具体想想,协程应用场景和线程的应用场景应该有所不同,举个例子,如果像我们之前的例子,对一个全局变量,调用多线程进行递减,会有锁的问题.这个时候虽然也可以使用协程模块进行处理,但是我们知道协程是在单线程下运行的。所以你对一个全局变量进行更改,这即使程序中遇到io操作时,传给系统的io操作队列中,我先猜测操作系统执行io操作时会判断要操作内容的内存块的id,如果有一样的就串行执行,其他的都并行执行。
         结论:协程是在单线程下进行的,所以是串行的。 当需要对一个全局变量或者对内存id一样的内存块进行更改时,不建议使用协程,因为意义不大。(结论是对的,只是对操作系统执行io的操作是猜测的,管他呢,好理解就行。)

    使用yield实现协程操作例子  
     1   
     2             #!/usr/bin/env python3.5
     3             #__author__:ted.zhou
     4             '''
     5             使用yield实现协程的例子
     6             '''
     7             import time
     8             import queue
     9             def consumer(name):
    10                 print("--->starting eating baozi...")
    11                 while True:
    12                     new_baozi = yield
    13                     print("[%s] is eating baozi %s" % (name,new_baozi))
    14                     #time.sleep(1)
    15 
    16             def producer():
    17 
    18                 r = con.__next__()  # python3.0里变成__next__(),python2.0是next()
    19                 r = con2.__next__()
    20                 n = 0
    21                 while n < 5:
    22                     time.sleep(1) # 模拟阻塞1秒
    23                     n +=1
    24                     con.send(n)
    25                     con2.send(n)
    26                     print("33[32;1m[producer]33[0m is making baozi %s" %n )
    27 
    28 
    29             if __name__ == '__main__':
    30                 con = consumer("c1")
    31                 con2 = consumer("c2")
    32                 p = producer()

    我们看上面的例子,其实就是一个简单的协程首先,con,con2分别都执行了一部分代码,直到调用con.send(),con2.send()方法才继续后面的代码.
    那么问题来了,如果生产者每做一个包子要花费1秒钟,那么相当于每次生产包子的时候产生1秒中的阻塞后,才能继续下面的代码,这就是前面提到的"进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序",影响到整个线程.
    我们想实现,一旦碰到这种sleep()的或者其他IO操做,咱就切换到其他协程上(我们直到IO操作是交给操作系统的IO操作接口进行处理的,程序只要到操作系统注册一个IO任务,操作系统就会做后续的操作,最终把结果放到操作系统中等待返回给注册的程序),这样执行其他协程的代码,等其他协程有IO操作时,在切换到这个协程上取结果,这样就把本来要阻塞的1秒中给用上了.
    那么到操作系统中注册一个IO操作怎么实现?不会~,通过这个普通的yield没有办法实现,yield只能实现一个简单的并发,但是一遇到阻塞怎样把阻塞丢给操作系统.yield不行.
    用Greenlet可以实现~
    Greenlet可以实现底层的阻塞的任务丢给操作系统的队列,具体实现细节后面再讲,它是一个第三方模块.我们不用它,我们就简单看一下就可以,我们要讲更好更高级的东西
    Greenlet具体代码如下:
     1             #!/usr/bin/env python
     2             # -*- coding:utf-8 -*-
     3 
     4 
     5             from greenlet import greenlet
     6 
     7 
     8             def test1():
     9                 print 12
    10                 gr2.switch()
    11                 print 34
    12                 gr2.switch()
    13 
    14 
    15             def test2():
    16                 print 56
    17                 gr1.switch()
    18                 print 78
    19 
    20             gr1 = greenlet(test1)
    21             gr2 = greenlet(test2)
    22             gr1.switch()

    高级的来了~~
    Gevent
    Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
    安装gevent
    $ cd /Library/Frameworks/Python.framework/Versions/3.5/bin/
    $ pip3.5 install gevent
    gevent模块实现协程一遇到IO操作和sleep切换的实例,代码如下:
     1             #!/usr/bin/env python3.5
     2             #__author__:'ted.zhou'
     3             '''
     4             使用gevent模块创建协程代码实例
     5             '''
     6             import gevent
     7             def foo():
     8                 print('33[32;1mRunning in foo33[0m')
     9                 gevent.sleep(0)  # 这里sleep(0)就是为了掩饰gevent协程遇到IO或者sleep就切换的特性
    10                 print('33[32;1mExplicit context switch to foo again33[0m')
    11 
    12             def bar():
    13                 print('Explicit context to bar')
    14                 gevent.sleep(0)
    15                 print('Implicit context switch back to bar')
    16 
    17             gevent.joinall([         # gevent.joinall() 是等待所有执行完成的意思
    18                 gevent.spawn(foo),   # gevent.spawn()是启动的意思
    19                 gevent.spawn(bar),
    20             ])

    协程异步非阻塞:
    上面的代码只是通过sleep的形式看切换的效果,那么我们现在通过gevent来看在但线程下并发的下载几个页面.
    首先我们要了解一个简单的模块urllib,就是一个可以简单爬网页的一个模块.
    在python3.0使用 urllib 中的urlopen 导入: from urllib import urlopen
    在python2.0使用urllib2 中的urlopen 导入: from urllib2 import urlopen
    代码如下:
     1             #!/usr/bin/env python3.5
     2             #__author__:'ted.zhou'
     3             '''
     4             使用gevent 和 urllib 在但线程中并发的抓取几个页面
     5             '''
     6 
     7             from gevent import monkey; monkey.patch_all()
     8             import gevent
     9             from  urllib.request import urlopen
    10 
    11             def f(url):
    12                 print('GET: %s' % url)
    13                 resp = urlopen(url)     #使用urlopen直接讲输入进来的URL进行下载
    14                 data = resp.read()      # 把结果读取下来
    15                 print('%d bytes received from %s.' % (len(data), url)) #打印爬到的字节
    16 
    17             # 使用gevent启动3个协程
    18             gevent.joinall([
    19                     gevent.spawn(f, 'https://www.python.org/'), #调用f函数,后面是参数
    20                     gevent.spawn(f, 'https://www.yahoo.com/'),
    21                     gevent.spawn(f, 'https://github.com/'),
    22             ])
    23         执行结果:
    24             GET: https://www.python.org/
    25             GET: https://www.yahoo.com/
    26             GET: https://github.com/
    27             451842 bytes received from https://www.yahoo.com/.
    28             47381 bytes received from https://www.python.org/.
    29             25540 bytes received from https://github.com/.
    30 
    31             Process finished with exit code 0

    从输出结果的顺序我们清楚的看到,一遇到爬页面的动作,就切换了.
    运行过程如下:
    当启动程序中的多个协程,遇到爬页面的请求,请求到操作系统注册一个IO请求后,就切换到第二,第三.
    这三个请求发送到远端,远端通过网络返回数据.
    远端通过网络返回给这台机器网络接口,网络接口数据进来的时候,网卡会通知CPU,对CPU产生一个中断请求,CPU接收到请求,就会通知操作系统说有数据来了,你要去接数据,操作系统就会把这个数据接回来,通知给当前具体的程序.程序内部会根据自己的寄存器上下文堆栈,把这个数据返回给对应的协程.

    下面我们使用gevent实现一个更有用的代码实例
    通过gevent实现单线程下的多socket并发
    我们刚讲socket的,是一对一的,同时一个socket只能跟一个socket客户端,其他socket客户端都得排队等待.为了解决这个问题,实现一个多并发的效果,我们后来把单个socket改成了多线程的socketserver,改成多线程socket之后,是不是每一个客户端连过来的时候,socket server都会给这个连接分配一个新的线程跟这个客户端联系.这是低效的,为什么是低效的?
    当有100个客户端连到多线程socketserver进行连接,但是连接之间的数据传输不多,那么就会产生上百个线程存在,但是实际用的不多.同时CPU还要不断的去检测socket客户端有没有传输数据.总体来说,开销很大,效率很低.
    如果在线程下实现一个多socket,就像抓网页,在单线程下,但是每一个socket客户端过来我给你创建一个实例.不是每一个实例都是活跃,只有一小部分活跃.其他不活跃的就简单扫一遍,就略过,如果在线程下,只需要维护一个线程实现跟上百个socket通信.
    如果想实现单线程下实现跟上百个socket客户端通信的效果.就必须解决如果一个客户端和我通信要10分钟,而其他实例要是也需要通信就得等待(阻塞)问题?
    为了解决这个问题,客户端实例没有资格和socket服务端通信,在服务端前面前放一个纸箱子,服务端不断得轮询纸箱子看看有没有纸条,如果客户端需要和服务端说话,写一个纸条放入到纸箱子.当服务端看到纸条,就返回一个"已阅"得确认信息,这样每一个客户端传过来得服务端就都可以看到了.那这个但线程下得多socket就是这个效果.
    代码中如何实现得呢?代码如下:
     1         server side
     2             #!/usr/bin/env python3.5
     3             #__author__:"ted.zhou"
     4             '''
     5             单线程下实现多socket,解决多线程socketserver开销大,效率低得问题
     6             '''
     7             import sys
     8             import socket
     9             import time
    10             import gevent
    11             from gevent import monkey   # 非常有意思,python中得黑魔法
    12             from gevent import socket   # 导入的是gevent下的socket
    13             monkey.patch_all()          # 非常有意思,python中得黑魔法,我们写socket是不是很多是阻塞得,比如读IO,或网络接口都是阻塞得,而这个monkey.path_all()方法就使得代码一旦遇到阻塞就切换到其他线程.也就是标题所说的(非阻塞).
    14             def server(port):
    15                 s = socket.socket()
    16                 s.bind(('0.0.0.0', port))
    17                 s.listen(500)           # 最多可以监听500个连接
    18                 while True:
    19                     cli, addr = s.accept()
    20                     gevent.spawn(handle_request, cli)  # 启动一个新的协程,把客户端的socket对象,调用
    21             def handle_request(s):
    22                 try:
    23                     while True:                 # 一个循环,
    24                         data = s.recv(1024)     # 前面用了monkey.path_all()了,这里程序就不阻塞了,而是切换到其他线程了,这里就是回到调用它的server函数
    25                         print("recv:", data)
    26                         s.send(data)
    27                         if not data:            # 如果没有数据
    28                             s.shutdown(socket.SHUT_WR)  # 这个shutdown,就是把客户端连接过来产生的socket客户端对象销毁掉.
    29 
    30                 except Exception as  ex:
    31                     print(ex)
    32                 finally:
    33 
    34                     s.close()       # 把服务器跟这个客户端连接的实例关掉
    35             if __name__ == '__main__':
    36                 server(8001)

    client side 代码如下:
     1             #!/usr/bin/env python3.5
     2             #__author__:'ted.zhou'
     3             '''
     4             socket客户端代码,就是普通的客户端
     5             '''
     6 
     7             import socket
     8 
     9             HOST = 'localhost'    # The remote host
    10             PORT = 8001           # The same port as used by the server
    11             s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    12             s.connect((HOST, PORT))
    13             while True:
    14                 msg = bytes(input(">>:"),encoding="utf8")
    15                 s.sendall(msg)
    16                 data = s.recv(1024)
    17                 #print(data)
    18 
    19                 # print('Received', repr(data))
    20                 print('Received', data.decode())
    21             s.close()

    同样你可以在client程序端,进行多线程的并发测试,看看server端能否正常应答,建议测试3000并发.

    论事件驱动与异步IO
    事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。

    让我们用例子来比较和对比一下单线程、多线程以及事件驱动编程模型。下图展示了随着时间的推移,这三种模式下程序所做的工作。这个程序有3个任务需要完成,每个任务都在等待I/O操作时阻塞自身。阻塞在I/O操作上所花费的时间已经用灰色框标示出来了。
  • 相关阅读:
    TP5 try{}catch{}异常捕获不到 解决办法
    layui2.5 开关在confirm确认了之后在关/开
    JQuery 表单textarea控制字数
    Navicat Premium从远程Mysql数据库复制到本地数据库的方法
    dedecmsV5.7 任意文件上传漏洞修复
    PHP 利用PHPExcel到处数据到Excel;还有导出数据乱码的解决方案。
    Mac Pro 2017款自带php与用brew重装PHP后的地址
    用js传递当前页面的url,丢失了&后面的参数 解决办法
    PHP 超全局变量之$_SERVER
    Linux while和for循环简单分析
  • 原文地址:https://www.cnblogs.com/zhming26/p/5575837.html
Copyright © 2011-2022 走看看