• day41


    目录

    (见右侧目录栏导航)

    - 1. 前言
    - 2. IO的五种模型
    - 3. 协程
        - 3.1 协程的概念
    - 4. Gevent 模块
        - 4.1 gevent 基本使用
        - 4.2 gevent应用一:爬虫
        - 4.3 gevent应用二:网络编程

    1. 前言

    CPU的速度远远快于磁盘、网络等IO。在一个线程中,CPU执行代码的速度极快,然而,一旦遇到IO操作,如读写文件、发送网络数据时,就需要等待IO操作完成,才能继续进行下一步操作。这种情况称为同步IO。在IO操作的过程中,当前线程被挂起,而其他需要CPU执行的代码就无法被当前线程执行了。因为一个IO操作就阻塞了当前线程,导致其他代码无法执行,所以我们必须使用多线程或者多进程来并发执行代码,为多个用户服务。每个用户都会分配一个线程,如果遇到IO导致线程被挂起,其他用户的线程不受影响。多线程和多进程的模型虽然解决了并发问题,但是系统不能无上限地增加线程。由于系统切换线程的开销也很大,所以,一旦线程数量过多,CPU的时间就花在线程切换上了,真正运行代码的时间就少了,结果导致性能严重下降。由于我们要解决的问题是CPU高速执行能力和IO设备的龟速严重不匹配,多线程和多进程只是解决这一问题的一种方法,另一种解决IO问题的方法是异步IO。当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。

    2. IO 的五种模型

      (1)blocking IO (阻塞IO)

      (2)noblocking IO (非阻塞IO)

      (3)IO multiplexing (IO多路复用)

      (4)signal driven IO(信号驱动IO) -- 不常用

      (5)asynchronous IO (异步IO)

    在理解上面五种IO模式之前需要理解以下4个概念:

      同步、异步、阻塞、非阻塞

    2.1 同步和异步

      同步和异步关注的是消息通信机制

      同步:在发出一个调用时,没得到结果之前,该调用就不返回。但是一旦调用返回就得到返回值(结果)了,调用者需要主动等待这个调用的结果。

      异步:在发送一个调用时,这个调用就直接返回了,不管返回有没有结果。当一个异步过程调用发出后,被调用者通过状态,通知调用者,或者通过回调函数处理这个调用

    2.2 阻塞和非阻塞

      阻塞和非阻塞关注的是程序在等待调用结果时的状态

      阻塞:调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才返回;

      非阻塞:在不能立即得到结果之前,该调用不会挂起当前线程

      有一个很好的例子说明这4者之间的关系:

        老张爱喝茶,废话不说,煮开水。 出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
              1 老张把水壶放到火上,立等水开。(同步阻塞) 老张觉得自己有点傻
              2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞) 老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。
              3 老张把响水壶放到火上,立等水开。(异步阻塞) 老张觉得这样傻等意义不大
              4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞) 老张觉得自己聪明了。
            
              所谓同步异步,只是对于水壶而言。 普通水壶,同步;响水壶,异步。 虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。 同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。
              所谓阻塞非阻塞,仅仅对于老张而言。 立等的老张,阻塞;看电视的老张,非阻塞。 情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。

    3. 协程

    3.1 协程的概念

      进程是资源分配的最小单位,线程是CPU调度的基本单位, 在Cpython中,由于GIL锁的存在,一般来说,同一时间片只有一个线程在cpu中运行,为了提高单线程的效率,这里提出了协程的概念。

      协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是协程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。

      需要强调:

        1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)

        2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)

      对比操作系统控制线程的切换,用户在单线程内控制协程的切换

      

      优点如下:

        1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级

        2. 单线程内就可以实现并发的效果,最大限度地利用cpu

      缺点如下:

        1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程

        2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程

      总结协程的特点:

        1. 必须在只有一个单线程里实现并发

        2. 修改共享数据不需加锁

        3. 用户程序里自己保存多个控制流的上下文栈

        4. 一个协程遇到IO操作自动切换到其他协程

    4. Gevent 模块

    4.1 gevent 基本使用

      Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程。

      g1=gevent.spawn(func,1,,2,3,x=4,y=5)
        创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的
      g2=gevent.spawn(func2)
      g1.join()
        等待g1结束
      g2.join()
        等待g2结束
      或者上述两步合作一步:
      gevent.joinall([g1,g2])
      g1.value
        拿到func1的返回值

      使用gevent 遇到IO就切换实例:

    import gevent
    
    
    def eat():
        print('eat start...')
        gevent.sleep(2)
        print('eat end.')
    
    
    def play():
        print('play start...')
        gevent.sleep(2)
        print('play end.')
    
    
    if __name__ == '__main__':
        g1 = gevent.spawn(eat)
        g2 = gevent.spawn(play)
        g1.join()
        g2.join()
    
        print('----主-----')

      

      上例gevent.sleep(2)模拟的是gevent可以识别的io阻塞,而time.sleep(1)或其他的阻塞,gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了

      from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前或者我们干脆记忆成:要用gevent,需要将from gevent import monkey;monkey.patch_all()放到文件的开头

    from gevent import monkey; monkey.patch_all()
    import gevent
    import time
    
    
    def eat():
        print('eat start...')
        time.sleep(2)
        print('eat end.')
    
    
    def play():
        print('play start...')
        time.sleep(2)
        print('play end.')
    
    
    if __name__ == '__main__':
        g1 = gevent.spawn(eat)
        g2 = gevent.spawn(play)
        g1.join()
        g2.join()
    
        print('----主-----')

      我们可以用threading.current_thread().getName()来查看每个g1和g2,查看的结果为DummyThread-n,即假线程

    from gevent import monkey; monkey.patch_all()
    import threading
    import gevent
    import time
    
    
    def eat():
        print(threading.current_thread().name)
        print('eat start...')
        time.sleep(2)
        print('eat end.')
    
    
    def play():
        print(threading.current_thread().name)
        print('play start...')
        time.sleep(2)
        print('play end.')
    
    
    if __name__ == '__main__':
        g1 = gevent.spawn(eat)
        g2 = gevent.spawn(play)
        g1.join()
        g2.join()
    
        print('----主-----')
    
    执行结果:
    DummyThread-1
    eat start...
    DummyThread-2
    play start...
    (阻塞2秒)
    eat end.
    play end.
    ----主-----

    4.2 gevent 应用一:爬虫

    from gevent import monkey; monkey.patch_all()
    import gevent
    import requests
    
    
    def get(url):
        print('GET:', url)
        response = requests.get(url)
        if response.status_code == 200:
            print('%d bytes recevied from %s' % (len(response.text), url))
    
    
    if __name__ == '__main__':
        gevent.joinall([
            gevent.spawn(get, 'https://www.baidu.com'),
            gevent.spawn(get, 'https://www.taobao.com'),
            gevent.spawn(get, 'https://www.jd.com')])
    gevent-爬虫

    4.3 gevent 应用二:网络编程

      通过gevent实现单线程下的socket并发
      注意:from gevent import monkey;monkey.patch_all()一定要放到导入socket模块之前,否则gevent无法识别socket的阻塞

    from gevent import spawn, monkey;monkey.patch_all()
    import socket
    
    
    def server(ip_port):
        sk_server = socket.socket()
        sk_server.bind(ip_port)
        sk_server.listen(5)
        while True:
            conn, addr = sk_server.accept()
            spawn(walk, conn)
    
    
    def walk(conn):
        conn.send(b'welcome!')
        try:
            while True:
                res = conn.recv(1024)
                print(res)
                conn.send(res.upper())
        except Exception as e:
            print(e)
        finally:
            conn.close()
    
    
    if __name__ == '__main__':
        server(('localhost', 8080))
    server.py
    import socket
    
    sk_client = socket.socket()
    sk_client.connect(('localhost', 8080))
    res = sk_client.recv(1024)
    print(res)
    while True:
        inp = input('>>>').strip()
        if not inp: continue
        sk_client.send(inp.encode())
        print(sk_client.recv(1024))
    client.py
  • 相关阅读:
    【Vue优化】—— Vue项目上线可以做的一些基本优化
    从debian10(buster) 升级到 11 (bullseye)
    jeecgboot集成seata实战
    免费的可视化Web报表工具,JimuReport v1.5.0beta版本发布
    制作报表原来可以这么简单—积木报表使用分享
    如何实现快速高效开发?低代码平台jeecgboot完美解决—jeecgboot3.1新特性
    比excel更好用的免费拖拽报表—JimuReport 1.4.4新特性
    制作打印报表费时费力?积木报表帮你轻松搞定—医院体检项目实战
    Asp.Net Core WebApi入门
    .Net5学习基于.Net5创建WebApi项目
  • 原文地址:https://www.cnblogs.com/hukey/p/10392304.html
Copyright © 2020-2023  润新知