美文网首页
python多任务--协程

python多任务--协程

作者: 小啊小狼 | 来源:发表于2020-10-16 10:12 被阅读0次

    一、前言

    协程

    协程 ,又称为微线程,它是实现多任务的另一种方式,只不过是比线程更小的执行单元。因为它自带CPU的上下文,这样只要在合适的时机,我们可以把一个协程切换到另一个协程。

    协程的优势

    • 执行效率高,因为子程序切换函数,而不是线程,没有线程切换的开销,由程序自身控制切换。于多线程相比,线程数量越多,切换开销越大,协程的优势越明显

    • 不需要锁的机制,只有一个线程,也不存在同时写变量冲突,在控制共享资源时也不需要加锁。

    二、实现协程的几种方式

    1、yield(生成器)可以很容易的实现从一个函数切换到另外一个函数

    def work1():
        for i in range(5):
            print(f"work1--befor----{i}")
            yield
            print(f"work1--after----{i}")
            time.sleep(0.5)
    
    def work2():
        for i in range(5):
            print(f"work2---befor---{i}")
            yield
            print(f"work2--after----{i}")
            time.sleep(0.5)
    
    def main():
        g1 = work1()
        g2 = work2()
        while True:
            try:
                next(g1)
                print('主程序')
                next(g2)
            except StopIteration:
                break
    main()
    

    运行结果如下:


    image.png

    2、原生的协程

    import asyncio
    # 定义一个协程函数
    async def work1():
        for i in range(10):
            print(f"work1--浇花----{i}")
    
    # 调用协程函数,返回的是一个协程对象
    cor1 = work1()
    
    # 执行协程
    asyncio.run(cor1)
    
    2.1、使用原生的协程实现多任务(不同任务)

    协程中切换,通过await语法来挂起自身的协程。await后面跟上耗时操作,耗时操作一般指IO操作: 网络请求,文件读取等,使用asyncio.sleep模拟耗时操作。协程的目的也是让这些IO操作异步化。
    sleep()需要用asyncio.sleep()
    await必须要在 async def function(): 中用,否则会报错

    import asyncio
    async def work1():
        for i in range(3):
            print(f"work1--浇花----{i}")
            await asyncio.sleep(1)
    
    async def work2():
        for i in range(5):
            print(f"work2--打墙----{i}")
            await asyncio.sleep(1)
    
    if __name__ == '__main__':
        loop = asyncio.get_event_loop()
        # 创建两个协程任务
        tasks = [
            work1(),
            work2(),
        ]
        # 启动事件循环并将协程放进去执行
        loop.run_until_complete(asyncio.wait(tasks))
    
    #输出
    work1--浇花----0
    work2--打墙----0
    work1--浇花----1
    work2--打墙----1
    work1--浇花----2
    work2--打墙----2
    work2--打墙----3
    work2--打墙----4
    
    2.2、使用原生的协程实现多任务(同一方法处理大量数据)
    import asyncio
    from queue import Queue
    import time
    
    def decorator(func):
        def wrapper():
            # 函数执行之前获取系统时间
            start_time = time.time()
            func()
            # 函数执行之后获取系统时间
            end_time = time.time()
            print('执行时间为:', end_time - start_time)
            return end_time - start_time
        return wrapper
    
    async def work1(q):
        while q.qsize():
            print(f"请求url:{q.get()}")
            await asyncio.sleep(0.1)
    
    @decorator
    def main():
        #创建一个包含有1000条url的队列
        q = Queue()
        for i in range(1000):
            q.put(f"www.baidu.com.{i}")
    
        loop = asyncio.get_event_loop()
        # 创建100个协程任务
        tasks = [work1(q) for i in range(100)]
    
        # 启动事件循环并将协程放进去执行
        loop.run_until_complete(asyncio.wait(tasks))
        loop.close()
    
    if __name__ == '__main__':
        main()
    
    #输出
    ...
    请求url:www.baidu.com.890
    请求url:www.baidu.com.891
    请求url:www.baidu.com.892
    ...
    执行时间为: 1.060093641281128
    

    100个协程执行1000个耗时0.1秒的请求只需要1秒

    2.3、版本区别:

    python 3.7 以前的版本调用异步函数的步骤:(如以上代码)

    • 1、调用asyncio.get_event_loop()函数获取事件循环loop对象
    • 2、通过不同的策略调用loop.run_forever()方法或者loop.run_until_complete()方法执行异步函数

    python3.7 以后的版本

    • 1、asyncio.run() 函数用来运行最高层级的入口点,下例的main()函数。此函数总是会创建一个新的事件循环并在结束时关闭之。它应当被用作 asyncio 程序的主入口点,理想情况下应当只被调用一次。
    • 2、await 等待一个协程,也可以启动一个协程。
    • 3、asyncio.create_task() 函数用来并发运行作为 asyncio 任务 的多个协程。下例并发运行两个work协程

    改动后代码如下

    #以上省略
    async def main():
        q = Queue()
        for i in range(1000):
            q.put(f"www.baidu.com.{i}")
        #创建了任务
        tasks = [asyncio.create_task(work1(q)) for i in range(100)]
        #将任务丢到执行队列里面去
        [await t for t in tasks]
    
    if __name__ == '__main__':
        m=main()
        start_time = time.time()
        asyncio.run(m)
        end_time = time.time()
        print('运行时间{}秒'.format(end_time - start_time))
    

    3、greenlet模块

    import time
    import greenlet
    
    """
    greenlet:在协程之间只能手动进行切换
    """
    def work1():
        for i in range(6):
            time.sleep(1)
            cor2.switch()
            print(f'浇花的第{i + 1}秒')
    
    def work2():
        for i in range(5):
            time.sleep(1)
            cor1.switch()
            print(f'打墙的第{i + 1}秒')
    
    cor1 = greenlet.greenlet(work1)
    cor2 = greenlet.greenlet(work2)
    cor1.switch()
    

    4、gevent模块实现多任务

    • gevent模块对greenlet又做了一层封装,当程序遇到IO耗时等待的时候会进行自动切换
    • gevent中默认是遇到gevent.sleep()会自动进行切换
    • 如果让gevent遇到io耗时自动切换:需要在程序的导包处加一个补丁monkey.patch_all(),该补丁不支持多线程
    from gevent import monkey
    monkey.patch_all()
    import gevent
    
    def work1():
        for i in range(6):
            gevent.sleep(1)
            print(f'浇花的第{i + 1}秒')
    
    
    def work2():
        for i in range(5):
            gevent.sleep(1)
            print(f'打墙的第{i + 1}秒')
    
    
    # 创建两个协程
    g1 = gevent.spawn(work1)
    g2 = gevent.spawn(work2)
    
    # 等待所有协程任务运行完毕
    gevent.joinall([g1, g2])
    

    示例:
    模拟50000个协程执行对100000个地址的请求

    from gevent import monkey
    monkey.patch_all()
    import gevent
    import time
    
    #创建100000个地址
    urls = ['http://www.baidu.com' for i in range(100000)]
    
    #定义需要执行的任务函数
    def work():
        while urls:
            url = urls.pop()
            # res = requests.get(url)
            time.sleep(0.5)
            print(f"正在请求url:{url},请求结果:url")
    
    def main():
        cos = []
        
        #创建50000个协程
        for i in range(50000):
            cor = gevent.spawn(work)
            cos.append(cor)
    
        # 等待所有协程任务运行完毕
        gevent.joinall(cos)
    
    main()
    

    相关文章

      网友评论

          本文标题:python多任务--协程

          本文链接:https://www.haomeiwen.com/subject/ljlwpktx.html