标签: code

  • Python 进程/线程/协程/异步编程

    前置基础

    什么是GIL?

    进程

     

    线程

     

    协程

    非协程实例

    首先来看非协程的代码实例

    t1 = time.time()
    def func1():
        print("当前执行function 1")
        time.sleep(1)  # 当程序出现了同步操作的时候. 异步就中断了
        print("当前执行function 1")
    
    
    def func2():
        print("当前执行function 2")
        time.sleep(2)
        print("当前执行function 2")
    
    def func3():
        print("当前执行function 3")
        time.sleep(3)
        print("当前执行function 3")
    
    if __name__ == '__main__':
        f1 = func1()
        f2 = func2()
        f3 = func3()
        tasks = [
            f1, f2, f3
        ]
        # 一次性启动多个任务(协程)
        # asyncio.run(asyncio.wait(tasks))
        t2 = time.time()
        print(t2 - t1)
    

     

    结果

     

    当前是function 1
    当前是function 1
    当前是function 2
    当前是function 2
    当前是function 3
    当前是function 3
    6.002589225769043

     

    协程实例

    async def func1():
        print("当前执行function_1")
        await asyncio.sleep(1)
        print("当前执行function_1")
    
    
    async def func2():
        print("当前执行function_2")
        await asyncio.sleep(2)
        print("当前执行function_2")
    
    
    async def func3():
        print("当前执行function_3")
        await asyncio.sleep(3)
        print("当前执行function_3")
    
    
    async def main():
        # 第一种写法
        # f1 = func1()
        # await f1  # 一般await挂起操作放在协程对象前面
        # 第二种写法(推荐)
        tasks = [
            asyncio.create_task(func1()),  # py3.8以后加上asyncio.create_task()
            asyncio.create_task(func2()),
            asyncio.create_task(func3())
        ]
        await asyncio.wait(tasks)
    
    
    if __name__ == '__main__':
        t1 = time.time()
        # 一次性启动多个任务(协程)
        asyncio.run(main())
        t2 = time.time()
        print(t2 - t1)
    

     

    结果

    当前执行function_1
    当前执行function_2
    当前执行function_3
    当前执行function_1
    当前执行function_2
    当前执行function_3
    3.0129427909851074

     

    对比与发现

    1. 一共三个任务
    2. 非协程写法里
      1. 如果使用非协程写法,  time.sleep(1)  ,将会导致IO阻塞。 因此程序会在设定的等待时间结束后, 才会往下执行。
      2. 耗时 6.002589225769043
      3. 打印的顺序自上而下
    3. 协程写法里
      1. 耗时3.0129427909851074
      2. 可以反推: 总耗时 = await 挂起时间最长的那个任务所花的时间(function 3) + 切换协程上下文的所需开销的时长(0.01)
    4. 异步协程的语法结构

     

     

    异步协程最简实例

    import asyncio
    import aiohttp
    import aiofiles
    
    # 构造无数个urls
    urls = [
        "https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png",
        "https://inews.gtimg.com/newsapp_bt/0/12171811596_909/0.png",
        "http://www.soso.com/soso/images/logo_index_sosox2.png"
    ]
    
    async def aiodownload(url):
        # 异步下载功能的函数, 以下三个步骤都是IO操作
            # 1.发送请求 aiohttp
            # 2. 得到图片内容 async
            # 3. 保存到文件 aiofiles
    
        filename = url.rsplit("/", 1)[1]
    
        async with aiohttp.ClientSession() as session:  # 类似同步的 requests
            async with session.get(url) as resp:        # 类似同步的 resp = requests.get()
                # 请求回来. 异步写入文件
                async with aiofiles.open(filename, mode='wb') as f:
                    # await f.write(await resp.content.read())  # 读取内容是异步的. 需要await挂起, resp.text()
                    # Response对象的read()和text()方法会将响应一次性全部读入内存,这会导致内存爆满,导致卡顿,影响效率。 因此采取字节流的形式,每次读取4096个字节并写入文件。
                    while True:
                        pic_stream = await resp.content.read(4096)
                        if not pic_stream:
                            break
                        await f.write(pic_stream)
        print(f'{filename} 已下载')
    
    async def main():
        # tasks = []
        # for url in urls:
        #     # tasks.append(aiodownload(url))
        #     d = asyncio.create_task(aiodownload(url))
        #     tasks.append(d)
    
        # 利用推导式的简写方式
        tasks = [asyncio.create_task(aiodownload(url)) for url in urls]
        await asyncio.wait(tasks)
    
    if __name__ == '__main__':
        asyncio.run(main())
    
    
    

     

    反思

    在爬虫的三个基本操作都是涉及到IO

    •  1.发送请求 aiohttp
    • 2. 得到图片内容 async
    • 3. 保存到文件 aiofiles

    但2和1、3有显著区别。

    • 磁盘io与网络io不同,磁盘顺序读写单个文件最快,并发读写会涉及到多个文件的切换问题,反而花了更多的时间,所以异步编程使用aiofiles要谨慎。
    • 举一个有差异但基本原理类似的例子。在同一台电脑上, 将C盘里的文件复制到D盘去, 这里有个很明显的经验是如果同时只有一个这样的复制操作, 那么“较为高效”。如果“将C盘里的文件复制到D盘去”的同时,  开启多个文件转移复制粘贴进程, 速度极为缓慢甚至进程管理亲卡死。
    • 解决的办法应该有很多种。
      • 可以考虑把文件读写任务抽离出来,放到队列里面,然后用专门的线程或进程去按顺序去处理。
      • 因此采取字节流的形式,每次读取4096个字节并写入文件