• 协程


    一、开始

    本质上是一个线程;能够在多个任务之间切换。

    def consumer():
        while True:
            x = yield
            print("处理了数据:", x)
    
    def producer():
        g = consumer()
        next(g)
        for i in range(1, 11):
            print("生产了数据:", i)
            g.send(i)
    
    producer()
    """
    生产了数据: 1
    处理了数据: 1
    生产了数据: 2
    处理了数据: 2
    生产了数据: 3
    处理了数据: 3
    生产了数据: 4
    处理了数据: 4
    生产了数据: 5
    处理了数据: 5
    生产了数据: 6
    处理了数据: 6
    生产了数据: 7
    处理了数据: 7
    生产了数据: 8
    处理了数据: 8
    生产了数据: 9
    处理了数据: 9
    生产了数据: 10
    处理了数据: 10
    """
    协程在两个任务之间切换示例

    无论是创建多进程还是创建多线程来解决问题,都要消耗一定的时间来创建进程、创建线程、以及管理他们之间的切换。

    随着我们对于效率的追求不断提高,基于单线程来实现并发又成为一个新的问题,即只用一个主线程(可利用的cpu只有一个)情况下实现并发。这样就可以节省创建线进程所消耗的时间。

    对于单线程下,我们不可避免程序中出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到IO阻塞时就切换到另外一个任务去计算,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态,相当于我们在用户程序级别将自己的IO操作最大限度地隐藏起来,从而可以迷惑操作系统,让其以为:该线程好像是一直在计算,IO比较少,从而更多的将cpu的执行权限分配给我们的线程。

    协程的本质:在单线程下,由用户自己控制一个任务,遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。为了实现它,我们需要找寻一种可以同时满足以下条件的解决方案:

    # 1、可以控制多个任务之间的切换,切换之前将任务的状态保存下来,以便重新运行时,可以基于暂停的位置继续执行;
    # 2、可以检测io操作,在遇到io操作的情况下才发生切换。

    二、协程

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

    需要强调的是:

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

     对比操作系统控制线程的切换,用户在单线程内控制协程的切换,优缺点如下:

    优点:
    # 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
    # 单线程内就可以实现并发的效果,最大限度地利用cpu
    缺点:
    # 协程的本质是单线程,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
    # 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程

    协程特点总结:

    1. 必须在只有一个单线程里实现并发
    2. 修改共享数据不需加锁
    3. 用户程序里自己保存多个控制流的上下文栈
    4. 附加:一个协程遇到io操作自动切换到其它协程(如何实现检测io自动切换,yield、greenlet都无法实现,gevent模块(select机制)则可以)

    greenlet模块

    真正的协程就是使用greenlet完成的切换。

    pip3 install greenlet

    greenlet实现保存状态的任务切换:

    from greenlet import greenlet
    
    def eat(name):
        print("%s eating start" % name)
        g2.switch("德玛")
        print("%s eating end" % name)
        g2.switch()
    
    def play(name):
        print("%s playing start" % name)
        g1.switch()
        print("%s playing end" % name)
    
    g1 = greenlet(eat)
    g2 = greenlet(play)
    g1.switch("盲僧")  # 可以在第一次switch时传入参数,以后都不需要
    """
    盲僧 eating start
    德玛 playing start
    盲僧 eating end
    德玛 playing end
    """
    View Code

    单纯的切换(在没有io的情况下或者没有重复开辟内存空间的操作),反而会降低程序的执行速度。

    # 顺序执行
    import time
    
    def f1():
        res = 1
        for i in range(100000000):
            res += i
    
    def f2():
        res = 1
        for i in range(100000000):
            res *= i
    
    s = time.time()
    f1()
    f2()
    e = time.time()
    print("run time is %s" % (e - s))
    
    ########################################
    
    # 切换
    from greenlet import greenlet
    def f1():
        res = 1
        for i in range(100000000):
            res += i
            g2.switch()
    
    def f2():
        res = 1
        for i in range(100000000):
            res *= i
            g1.switch()
    
    s = time.time()
    g1 = greenlet(f1)
    g2 = greenlet(f2)
    g1.switch()
    e = time.time()
    print("run time is %s" % (e - s))
    
    """
    run time is 22.408281803131104
    run time is 119.99586343765259
    """
    效率对比

    greenlet模块只是提供了一种比generator更加便捷的切换方式,当切到一个任务执行时如果遇到io,就会原地阻塞,仍然没有解决遇到io自动切换以此提升效率的问题。

    单线程里:20个任务的代码通常会既有计算操作又有阻塞操作,所以如果在执行任务1时遇到阻塞,就利用阻塞的时间去执行任务2......如此,才能提高效率,这就用到了Gevent模块。

    gevent模块

    pip3 install gevent

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

    要用gevent,需要将from gevent import monkey;monkey.patch_all()放到文件的开头

    # g1 = gevent.spawn(func1,1,2,x=3,y=4)
        创建一个协程对象g1,spawn括号内第一个参数是函数名,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数的。
    
    # g2 = gevent.spawn(func2)
    
    # g1.join()
        等待g1结束
    
    # g2.join()
        等待g2结束
    
    # gevent.joinall([g1,g2])
        上述两步合作一步:
    
    # g1.value
        拿到func1的返回值
    用法介绍
    from gevent import monkey;monkey.patch_all()
    import time
    import gevent
    import threading
    
    def eat(name):
        print(threading.current_thread().getName())
        print("%s eating start" % name)
        time.sleep(1)
        print("%s eating end" % name)
    
    def play(name):
        print(threading.current_thread().getName())
        print("%s playing start" % name)
        time.sleep(1)
        print("%s playing end" % name)
    
    g1 = gevent.spawn(eat, "德玛")
    g2 = gevent.spawn(play, "盲僧")
    # g1.join()
    # g2.join()
    gevent.joinall([g1, g2])
    遇io主动切换

    上述例子中使用了 threading.current_thread().getName() 来查看g1、g2,查看的结果为DummyThread-n,dummy意思为虚设的、假的、摆样子的,即假线程。

    结论:

    • 进程和线程的任务切换由操作系统完成;
    • 协程任务之间的切换由程序代码完成,只要遇到协程模块能识别的IO操作的时候,程序就会进行任务切换,实现并发的效果。

    gevent之同步与异步

    from gevent import monkey;monkey.patch_all()
    import time
    import gevent
    
    def task(n):
        time.sleep(1)
        print(n)
    
    def sync():  # 同步
        for i in range(10):
            task(i)
    
    def async():  # 异步
        g_list = []
        for i in range(10):
            g = gevent.spawn(task, i)
            g_list.append(g)
        gevent.joinall(g_list)  # 相当于for g in g_list:g.join()
        
    sync()
    async()
    gevent同步异步

    gevent之应用举例

    gevent实现爬虫:

    from gevent import monkey;monkey.patch_all()
    import time
    import gevent
    import requests
    
    def get_page(url):
        print("GET: %s" % url)
        response = requests.get(url)
        if response.status_code == 200:
            print("%d bytes received from %s" % (len(response.text), url))
    
    s = time.time()
    gevent.joinall([
        gevent.spawn(get_page, "https://www.python.org/"),
        gevent.spawn(get_page, "https://www.yahoo.com/"),
        gevent.spawn(get_page, "https://github.com/")
    ])
    e = time.time()
    
    print("run time:%s" % (e - s))
    """
    GET: https://www.python.org/
    GET: https://www.yahoo.com/
    GET: https://github.com/
    79331 bytes received from https://github.com/
    49371 bytes received from https://www.python.org/
    527037 bytes received from https://www.yahoo.com/
    run time:14.403823852539062
    """
    View Code

    gevent实现单线程下的socket并发:

    from gevent import monkey;monkey.patch_all()
    import socket
    import gevent
    
    def server(ip, port):
        sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        sk.bind((ip, port))
        sk.listen(5)
        while True:
            conn, addr = sk.accept()
            gevent.spawn(talk, conn, addr)
    
    def talk(conn, addr):
        try:
            while True:
                msg = conn.recv(1024).decode("utf-8")
                print("client: %s:%s  msg: %s" % (addr[0], addr[1], msg))
                conn.send(msg.encode("utf-8"))
        except Exception as e:
            print(e)
        finally:
            conn.close()
    
    if __name__ == "__main__":
        server("127.0.0.1", 8080)
    server
    import socket
    
    sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sk.connect(("127.0.0.1", 8080))
    
    while True:
        msg = input(">>>: ").strip()
        if not msg:
            continue
            sk.send(msg.encode("utf-8"))
        msg = sk.recv(1024).decode("utf-8")
        print(msg)
    client

    多线程并发多个客户端:

    import socket
    import threading
    
    def client(ip, port):
        """
        套接字对象一定要加到函数内,即局部名称空间内,放在函数外则被所
        有线程共享,则大家公用一个套接字对象,那么客户端端口永远一样了
        """
        sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sk.connect((ip, port))
        count = 0
        while True:
            msg = ("%s say hello %s" %
                (threading.current_thread().getName(), count))
            sk.send(msg.encode("utf-8"))
            msg = sk.recv(1024)
            print(msg.decode("utf-8"))
            count += 1
    
    if __name__ == "__main__":
        for i in range(500):
            t = threading.Thread(target=client, args=("127.0.0.1", 8080))
            t.start()
    View Code
  • 相关阅读:
    [LintCode] Merge Two Sorted Lists 混合插入有序链表
    Convert PLY to VTK Using PCL 1.6.0 or PCL 1.8.0 使用PCL库将PLY格式转为VTK格式
    [LintCode] Best Time to Buy and Sell Stock II 买股票的最佳时间之二
    [LintCode] Maximum Subarray 最大子数组
    [LeetCode] Matchsticks to Square 火柴棍组成正方形
    [LeetCode] Sort Characters By Frequency 根据字符出现频率排序
    [LeetCode] 450. Delete Node in a BST 删除二叉搜索树中的节点
    [LeetCode] Serialize and Deserialize BST 二叉搜索树的序列化和去序列化
    [LeetCode] Find All Numbers Disappeared in an Array 找出数组中所有消失的数字
    [LeetCode] Add Two Numbers II 两个数字相加之二
  • 原文地址:https://www.cnblogs.com/believepd/p/9745718.html
Copyright © 2020-2023  润新知