python之协程概念和gevent使用注意事项及三个案例

协程基本概念

协程本质就是一条线程,它能实现多个任务在一条线程上来回切换执行。

使用协程可以在执行任务遇到IO时切换到别的任务继续执行,避免进入阻塞态,提高了CPU利用率。另外降低了操作系统负担。

进程、线程、协程的区别

进程线程协程
操作系统资源分配的最小单位操作系统调度执行的最小单位操作系统不可见
内存隔离内存共享内存共享
数据不安全数据不安全数据安全
开销大开销小开销极小
发生IO时都能感知,由操作系统负责切换发生IO时都能感知,由操作系统负责切换发生IO时程序切换,文件、input等io不能感知

gevent模块:

安装

这是第三方模块,需要单独安装。

pip install gevent

打补丁

from gevent import monkey
monkey.patch_all()

这里我重点说一下,monkey.patch_all()目的是使用gevent自己的模块替代了原来的模块,只有使用patch_all中支持的模块触发IO时才会被识别并切换任务,若使用了patch_all不支持的模块触发IO时是无法被识别并切换任务的。

  1. gevent支持清单:

    sockt、dns、time、thread、os、ssl、subprocess、aggressive、Event、builtins、signal、queue、contextvars。

  2. gevent不支持清单:

    sys及其它不在支持清单中的模块。

  3. 查看gevent支持清单的方法:

    请查看patch_all函数,它有很多默认值参数,默认值为True的模块名是支持的,默认值为False的模块名是不支持的,未列出的模块名是不支持的。

    def patch_all(socket=True, dns=True, time=True, select=True, thread=True, os=True, ssl=True,
                  subprocess=True, sys=False, aggressive=True, Event=True,
                  builtins=True, signal=True,
                  queue=True, contextvars=True,
                  **kwargs):
    
  4. 使用patch_all()的注意事项:

    一、使用patch_all()时已经导入了所有可支持的模块,不需要再重复导入。

    这里举个导入socket的例子:

    • 首先看看导入原始的socket:

      import socket
      print(socket.socket)
      
      out:
      <class 'socket.socket'>
      
    • 再看看patch_all的socket:

      from gevent import monkey
      monkey.patch_all()
      import socket
      print(socket.socket)
      
      out:
      <class 'gevent._socket3.socket'>
      
    • 最后比较一下gevent.socket:

      from gevent import monkey
      monkey.patch_all()
      import gevent
      print(gevent.socket.socket)
      
      out:
      <class 'gevent._socket3.socket'>
      
    • 总结:从前面的案例中可以看到patch_all后,import socket已经没有必要,因为gevent.socket.socket()和socket.socket()是完全一样的。所以patch_all之后不需要再import原来的包,直接使用“gevent.包名”即可。

    二、不需要导入的模块建议加False参数不导入它。

    patch_all()导入了非常多的模块,假设你使用socket,没用到signal、queue、contextvars、aggressive,那么可以这样做:

    from gevent import monkey
    monkey.patch_all(signal=False, queue=False, contextvars=False, aggressive=False)
    

gevent使用案例

  1. 案例一:

    import random
    import gevent
     
    def func(n):
        n += 1
        print(f'任务{n}开始运行!')
        gevent.sleep(random.random())
        print(f'任务{n}结束运行。')
    
    tasks = []
    for i in range(5):
        tasks.append(gevent.spawn(func,i))
    gevent.joinall(tasks)
    

    输出:

    任务1开始运行!
    任务2开始运行!
    任务3开始运行!
    任务4开始运行!
    任务5开始运行!
    任务3结束运行。
    任务1结束运行。
    任务4结束运行。
    任务2结束运行。
    任务5结束运行。
    

    说明:

    gevent.spawn(func,i),这里是创建协程实例,该实例的第一初始化参数是func,即要执行协程任务的函数;第二个及以后任意多个参数都是要传给func的参数。

    gevent.joinall(tasks),这里是等待所有协程任务,直到所有协程任务完成后才继续向后执行。

  2. 案例二:

    import gevent
     
    def func1():
        print('func1开始运行!')
        gevent.sleep(0.3)
        print('func1继续运行。')
     
    def func2():
        print('func2开始运行!')
        gevent.sleep(0.2)
        print('func2继续运行。')
    
    def func3():
        print('func3开始运行!')
        gevent.sleep(0.1)
        print('func3继续运行。')
     
    gevent.joinall((
        gevent.spawn(func1),
        gevent.spawn(func2),
        gevent.spawn(func3),
    ))
    

    输出:

    func1开始运行!
    func2开始运行!
    func3开始运行!
    func3继续运行。
    func2继续运行。
    func1继续运行。
    

    说明:

    gevent.joinall((gevent.spawn(func1), gevent.spawn(func2), gevent.spawn(func3),)):这里和案例一功能类似,添加协程任务func1、func2、func3,然后等待这些协程任务执行完毕。

  3. 案例三:

    协程socket的server端:

    import gevent
    from gevent import monkey
    monkey.patch_all()
    
    
    def func(con):
        while True:
            msg = con.recv(1024).decode('utf8')
            con.send(msg.upper().encode('utf8'))
    
    
    sk = gevent.socket.socket()
    sk.bind(('127.0.0.1', 9001))
    sk.listen()
    while True:
        conn, _ = sk.accept()
        gevent.spawn(func, conn)
    

    多线程socket的client端:

    import socket
    import time
    from threading import Thread
    
    
    def client(i):
        sk = socket.socket()
        sk.connect(('127.0.0.1', 9001))
    
        for j in range(100):
            sk.send(f'hello -> {i}'.encode('utf8'))
            msg = sk.recv(1024).decode('utf8')
            print(msg)
            time.sleep(0.5)
    
    if __name__ =="__main__":
        for i in range(500):
            Thread(target=client, args=(i,)).start()
    

    说明:

    上述server端是使用多协程方法编写的,while True接收连接,然后给该连接分配服务,即接收client端发来的消息,将其转换成大写再转发回client端。

    client端是使用多线程方法编写的,经测试client开500-1000条线程去连接server端单线程多协程的服务没有任何问题。可见协程在高并发方面极具优势。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值