Python学习 Day11 进程和线程(二)

三、异步IO和协程

3.1 多任务

无论是多线程还是多进程,一旦数量过去,效率一定是提不上去的。因为虽然时间片的切换是有时间损耗的,切换的越多,时间损耗越多。如果有几千个任务同时进行,操作系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。

此外是否采取多进程和多线程还要考虑是执行CPU密集型任务还是IO密集型任务。如果是CPU密集型任务,任务的执行主要依赖CPU运算,任务越多,不必要的任务切换的时间损耗就越多,效率就越低。如果是IO密集型任务,如果采取多任务,在某个任务等待IO操作时,其他任务可以先进行,从而减少整体的IO等待时间,减少CPU运转的空档期,提高CPU运转效率。所以IO型任务可以使用多任务。

常见的多任务形式除了多进程、多线程,还包括异步IO、协程。

3.2 非阻塞编程

3.2.1 协程和异步IO

Coroutine(协程)是一种程序组件,其行为类似于子例程(subroutine)或函数,但与这些不同的是,协程可以在其执行过程中被挂起(suspend)和恢复(resume),而不需要终止或调用方等待其完成。协程通常与生成器(generator)和异步I/O库一起使用,以构建非阻塞的、事件驱动的程序。

异步IO是一种编程模型,它允许程序在等待IO操作时继续执行其他任务,从而提高程序的效率和响应能力。

异步IO与协程紧密配合。因为协程可以在等待IO操作时挂起,并在IO操作完成时恢复执行。这种挂起和恢复的能力使得异步IO操作可以无缝地集成到协程的执行流程中,从而实现真正的非阻塞并发编程。

3.2.2 async def和await

async def和await是实现异步编程的关键工具。我们可以通过async def关键字创建(定义)协程,并通过await表达式挂起和恢复执行。 

async def:定义一个异步函数,这样的函数可以包含等待IO操作(或其他异步操作)的挂起点。

await:用于标记一个挂起点。await后常跟一个返回awaitable对象(通常是另一个协程或异步操作)的表达式。

当异步函数中的await表达式被执行时,函数会暂停执行,直到awaitable对象(通常是另一个协程或异步操作)完成,并恢复执行。这使得异步函数能够在等待IO操作(比如网络请求或数据库查询)时释放控制权,允许其他任务继续执行。

异步函数通过asyncio.run()函数或者获取事件循环函数来运行。尝试一个简单的异步IO:

import asyncio


async def cc(num):
    count = 0
    count += num
    # 假设这个加上的动作需要0.1s
    await asyncio.sleep(0.1) # 挂起等待0.1秒
    return count


async def main():
    result = await cc(10) # 挂起等待cc(10)执行
    print(result)


if __name__ == '__main__':
    # asyncio.run运行协程
    asyncio.run(main())
    # 获取事件循环并运行主协程
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

3.2.3 asyncio库

① 几个概念

1)Task对象:

  • Task是Future的子类,用于表示一个并发任务。它封装了一个协程(coroutine)对象,使得这个协程可以在事件循环中被调度和执行。
  • create_task函数可以创建Task对象。创建后,该协程就会被添加到事件循环中等待被调度执行。

2)Future对象:

  • 用于表示尚未完成的异步操作结果的占位符,在异步编程中,Future对象通常用于接收异步操作的结果
  • gather函数可以执行一组异步操作(如协程,其他Future对象)并等待它们全部完成。并返回一个包含所有结果的Future对象。

3)事件循环:

  • 事件循环是一个事件队列,所有待执行的任务会加入事件循环挂起,当一个异步程序处于挂起等待IO操作完成时,事件循环会从队列中取出下一个任务来补位使用主线程,从而避免了线程的CPU闲置,提高了线程的并发性能,实现单线程的非阻塞编程。待执行的任务可以是IO操作,定时器事件,用户交互等
  • 事件循环最大作用是可以在单线程内以非阻塞的方式处理并发任务,避免了线程切换的开销
② 常用函数
asyncio.create_task(coro)创建一个Task对象,来执行给定的协程。coro是由一个async def定义的异步函数。对于创建的Task对象,我们通常会用await等待任务完成并获取结果,或通过gather函数并发执行并获取结果。
asyncio.gather(*aws, loop=None)作用是并发执行任意个个awaitable对象,如协程和任务,在全部对象执行完后,返回一个包含所有结果的Future对象。Future对象是列表的形式。其中*aws表示一个解包的列表或者元组,也可以直接传入任意个awaitable变量(如Task对象)。loop=None可省略,省略默认获取当前事件循环,python3.7以上版本支持。
asyncio.get_event_loop()获取当前事件循环。如果当前线程没有事件循环,这个函数会创建一个事件循环,并设为当前时间循环。
asyncio.run_until_complete(coro)通常与get_event_loop()一起用,用于运行传入的协程或Future对象并返回结果
asyncio.run(main())这个函数是启动异步函数最直接的方法,他创建一个事件循环,传入协程,运行完后自动退出循环。
asyncio.sleep(s)创建一个挂起指定秒数的协程,常用于模拟网络延迟和测试中引入等待时间
③ 案例

运用协程并发执行提高效率的案例:

假设要在一个线程内下载三个文件,单个文件的下载时间是2秒。

单线程内使用异步编程:

import asyncio
from time import time


async def download(name):
    print('下载的电影名:%s' % name)
    # 模拟下载时间2s
    await asyncio.sleep(2)
    print('%s下载完成' % name)
    
    
async def main():
    start = time()
    tasks = [asyncio.create_task(download(name)) for name in ['霸王别姬', '花样年华', '东邪西毒']]
    result = asyncio.gather(*tasks) # 生成一个包含所有协程的Future对象
    await result # 运行完毕Future对象并返回结果
    end = time()
    print('共耗时%.2fs' % (end-start))
    # print(result)    # 错误读取Future对象的结果:<_GatheringFuture finished result=[None, None, None]>
    
    
if __name__ == '__main__':
    asyncio.run(main())
"""
下载的电影名:霸王别姬
下载的电影名:花样年华
下载的电影名:东邪西毒
霸王别姬下载完成
花样年华下载完成
东邪西毒下载完成
共耗时2.00s
"""

单线程内不使用异步编程:

import asyncio
from time import time, sleep


def download(name):
    print('下载的电影名:%s' % name)
    # 模拟下载时间2s
    sleep(2)
    print('%s下载完成' % name)


def main():
    start = time()
    # 单线程内依次下载三个文件
    download('霸王别姬')
    download('花样年华')
    download('东邪西毒')
    end = time()
    print('共耗时%.2fs' % (end-start))
    # print(result)    # 错误读取Future对象的结果:<_GatheringFuture finished result=[None, None, None]>


if __name__ == '__main__':
    main()
"""
下载的电影名:霸王别姬
霸王别姬下载完成
下载的电影名:花样年华
花样年华下载完成
下载的电影名:东邪西毒
东邪西毒下载完成
共耗时6.01s
"""

所以,想充分利用CPU的多核特性,可以多进程+协程结合使用。一方面充分利用多核CPU,一个核心运行一个进程/线程,实现并行计算;另一方面可以运用协程实现非阻塞编程,提高单个线程的效率。两者结合可以获得更高的CPU性能。 

四、练习

4.1 使用多进程对复杂任务“分而治之

完成1~100000000求和的计算密集型任务

直接求和(结果在代码后):

import time


def main():
    num_sum = 0
    start = time.time()
    for i in range(100000001):
        num_sum += i
    end = time.time()
    print(num_sum)
    print('耗时%.2fs' % (end-start))
    
    
if __name__ == '__main__':
    main()
"""
5000000050000000
耗时6.28s
"""

使用多进程计算(结果在代码后):

"""
多进程计算后汇总
"""
from multiprocessing import Process, Queue
from time import time


def split(a, b, num):
    """计算a-b拆分成num份分别计算"""
    diff = b - a + 1
    if diff % num == 0:
        # 把a-b分成num份,然后加入列表分别求值
        ss = diff // num
        ls = []
        for i in range(1, num + 1):
            ls.append([i for i in range(a + (i - 1) * ss, a + i * ss + 1)])
        return ls
    else:
        return None


def calculate(num_list, queue):
    num = sum(num_list)
    queue.put(num)    # 把计算结果放进某个队列


def main():
    ls = split(1, 100000000, 10)
    ps = []
    q = Queue() # 定义一个队列,存放计算结果
    results = []
    for i in range(10):
        p = Process(target=calculate, args=(ls[i], q))
        p.start()
        ps.append(p)
    for i in range(10):
        results.append(q.get())
    start = time()
    for p in ps:
        p.join()
    sum_value = sum(results)
    end = time()
    print(sum_value)
    print('耗时%.2fs' % (end - start))


if __name__ == '__main__':
    main()
"""
电脑运行多进程会报错。。得不到结果,其他小伙伴可以试试
"""

使用多线程计算(结果在代码后):

"""
多线程计算后汇总
"""
from threading import Thread
from time import time


def split(a, b, num):
    """计算a-b拆分成num份分别计算"""
    diff = b - a + 1
    if diff % num == 0:
        # 把a-b分成num份,然后加入列表分别求值
        ss = diff // num
        ls = []
        for i in range(1, num + 1):
            ls.append([i for i in range(a + (i - 1) * ss, a + i * ss + 1)])
        return ls
    else:
        return None


def calculate(num_list, results):
    return results.append(sum(num_list))


def main():
    ls = split(1, 100000000, 8)
    results = []
    ps = []
    for i in range(8):
        p = Thread(target=calculate, args=(ls[i], results))
        p.start()
        ps.append(p)
    start = time()
    for p in ps:
        p.join()
    sum_value = sum(results)
    print(sum_value)
    end = time()
    print('耗时%fs' % (end - start))


if __name__ == '__main__':
    main()
"""
5000000500000008
耗时0.002512s
"""

上面的代码需要大概6秒左右的时间,而下面的代码只需要不到1秒的时间,跟骆昊大神学习的只比较了运算的时间,没有考虑列表创建及切片操作花费的时间,实际上如果算上前面的时间还挺慢的。

全部时间:

def main():
    start = time()
    ls = split(1, 100000000, 8)
    ...
    print(sum_value)
    end = time()
    print('耗时%fs' % (end - start))


if __name__ == '__main__':
    main()
"""
5000000500000008
耗时6.148524s
"""

  • 24
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值