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

1. 前置基础

1.1 什么是GIL?

2. 进程

 

3. 线程

 

4. 协程

4.1 非协程实例

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

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

 

4.2 协程实例

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

 

4.2.1 对比与发现

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

 

 

4.3 异步协程最简实例

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())


 

4.3.1 反思

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

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

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

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

本站文章除单独注明外均为原创,本文链接https://bowmanjin.com/432,未经允许请勿转载。

0

评论0

请先

没有账号? 注册  忘记密码?