zoukankan      html  css  js  c++  java
  • 协程

    协程

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

    他是一个能够在线程中实现并发效果的概念(但他是不存在的,虚拟的,是程序员意淫的)

    需要强调的是:

    1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
    2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)

    一、协程的优缺点

    优点:

    • 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
    • 单线程内就可以实现并发的效果,最大限度地利用cpu

    缺点

    • 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
    • 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程

    举例

    情况一、单纯的切换反而会降低运行效率

    import time
    def consumer(res):
        '''任务1:接收数据,处理数据'''
        pass
    
    def producer():
        '''任务2:生成数据'''
        res = []
        for i in range(10000000):
            res.append(i)
        return res
    
    start = time.time()
    # 串行执行
    res = producer()
    consumer(res)  # 协程consumer(producer()) 会降低执行效率
    stop = time.time()
    print(stop-start)
    

    1.3185927867889404

    import time
    def consumer():
        '''任务1:接收数据,处理数据'''
        while True:
            x = yield
    
    def producer():
        '''任务2:生产数据'''
        g = consumer()
        next(g)
        for i in range(10000000):
            g.send(i)
    start = time.time()
    # 基于yield保存状态,实现两个任务直接来回切换,即并发的效果
    # ps:如果每个任务中都加上打印,那么明显的看到两个任务的打印是你一次我一次,即并发执行的
    producer()
    stop = time.time()
    print(stop-start)
    

    1.1409246921539307

    二、Gevent模块

    Gevent是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet,它是以C扩展模块形式接入Python的轻量级协程。Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

    import gevent
    from gevent import monkey;monkey.patch_all()  #打了一个补丁,可以实现捕获非gevent的io.
    import time
    
    def eat(name):
        print(f"{name} 开始吃")
        time.sleep(2)
        # 起初gevent是感知不到time.sleep的,但是会感知到gevent.sleep,为了让它感知到,所以打了补丁
        print(f"{name} 吃完了")
    
    def play(name):
        print(f"{name} 开始玩")
        time.sleep(2)
        print(f"{name} 玩完了")
    
    g1 = gevent.spawn(eat,'鸭屁屁')
    g2 = gevent.spawn(play,'隔壁老王')
    g1.join()  # 让它在这个地方被执行
    g2.join()
    
    --------------------------------------------------------------------------
    鸭屁屁 开始吃
    隔壁老王 开始玩
    鸭屁屁 吃完了
    隔壁老王 玩完了
    

    上面的这个例子充分证明了,只有协程遇见自己能识别的io操作的时候才切换,如果它不认识的io,它是不会切换的,例如在没有打补丁之前的time.sleep()一样,切换不了,它就是一个串行。

    三、通过gevent实现单线程下的socket并发

    注意:from gevent import monkey;monkey.patch_all() 一定要放到导入socket模块之前,否则gevent无法识别socket的阻塞。

    '''服务端'''
    from gevent import monkey;monkey.patch_all()
    from socket import *
    import gevent
    #如果不想用money.patch_all()打补丁,可以用gevent自带的socket
    # from gevent import socket
    # s=socket.socket()
    
    def server(server_ip,port):
        s = socket(AF_INET,SOCK_STREAM)
        s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
        s.bind((server_ip,port))
        s.listen(5)
        while True:
            conn,addr = s.accept()
            gevent.spawn(talk,conn,addr)
    
    def talk(conn,addr):
        try:
            while True:
                res = conn.recv(1024)
                print(f"client{addr[0]:{addr[1]} msg:{res}}")
                conn.send(res.upper())
        except Exception as e:
            print(e)
        finally:
            conn.close()
    if __name__ == '__main__':
        server('127.0.0.1',8080)
    
    '''客户端'''
    from threading import Thread
    from socket import *
    import threading
    
    def client(server_ip,port):
        c = socket(AF_INET,SOCK_STREAM) # 套接字对象一定要加到函数内,即局部名称空间内,
        # 放在函数外则被所有线程共享,则大家公用一个套接字对象,那么客户端端口永远一样了
        c.connect((server_ip,port))
    
        count = 0
        while True:
            c.send(('%s say hello %s' %(threading.current_thread().getName(),count)).encode('utf8'))
            msg = c.recv(1024)
            print(msg.decode('utf8'))
            count += 1
    
    if __name__ == '__main__':
        for i in range(500):
            t = Thread(target=client,args=('127.0.0.1',8080))
            t.start()
    
    

    四、协程的特点

    • 协程本质是一个线程
    • 能够在多个任务之间切换来节省一些IO时间
    • 协程中任务之间切换也是消耗时间的,但是开销要远远小于进程,线程
    • 协程是由greenletswitch()进行切换,进程、线程是靠cpu切换的
    • 协程更适用于网络方面,高IO的情况下,他能规避一些任务中的IO操作
  • 相关阅读:
    JavaScript对原始数据类型的拆装箱操作
    Javascript继承(原始写法,非es6 class)
    动态作用域与词法作用域
    自行车的保养
    探索JS引擎工作原理 (转)
    C语言提高 (7) 第七天 回调函数 预处理函数DEBUG 动态链接库
    C语言提高 (6) 第六天 文件(续) 链表的操作
    C语言提高 (5) 第五天 结构体,结构体对齐 文件
    C语言提高 (4) 第四天 数组与数组作为参数时的数组指针
    C语言提高 (3) 第三天 二级指针的三种模型 栈上指针数组、栈上二维数组、堆上开辟空间
  • 原文地址:https://www.cnblogs.com/zhuangyl23/p/11559938.html
Copyright © 2011-2022 走看看