【Python】协程以及多进程+协程的使用

原文作者:我辈李想
版权声明:文章原创,转载时请务必加上原文超链接、作者信息和本声明。



前言

在 Python 中,同步和异步通常是指代码执行的模式。

同步:当程序执行同步操作时,程序将等待该操作完成,然后才执行下一段代码。这种模式被称为同步模式。在同步模式下,程序必须等待操作完成之后才能继续执行下一步操作。

异步:在异步模式下,程序执行操作时,不需要等待该操作完成,而是可以继续执行其他操作。当该操作完成后,程序将通知其结果。这使得程序可以执行多个操作而不必等待每个操作完成。异步操作通常使用回调函数来处理操作结果。


一、异步编程

1.python中的异步

Python中的协程(coroutine)是一种异步编程的方式,在 Python3.5版本后,可以使用 async 和 await 关键字来实现异步操作。async 关键字用于定义异步函数,而 await 关键字用于等待异步函数的结果。异步函数通常返回一个协程对象,该对象可以在 await 关键字后使用。

另外,Python 还提供了 asyncio 模块来实现异步操作,该模块提供了事件循环和协程的支持。使用事件循环可以管理多个协程,而协程可以在事件循环中运行,以实现非阻塞的 I/O 操作。

2.非阻塞的 I/O 操作

  1. 异步数据库查询:可以使用异步的数据库驱动程序(如aiomysqlaiopg等)进行数据库查询操作,使用await关键字来等待查询结果的返回,从而避免了阻塞整个进程。

  2. 异步HTTP请求:使用异步的HTTP客户端库(如httpxaiohttp等)发送异步的HTTP请求,使用await关键字等待响应结果的返回,从而避免了阻塞整个进程。

  3. 异步文件读写操作:在处理文件读写、网络通信等IO操作时,可以使用异步的IO库(如aiofilesasyncio等)进行异步操作,通过await关键字等待IO操作的完成,从而提高程序的性能。

  4. 异步任务调度:使用FastAPI内置的BackgroundTasks类可以异步执行后台任务,例如发送邮件、推送通知等,通过await关键字等待任务的完成,从而不会阻塞主请求的处理。

二、协程的使用

想要在我们的程序中使用协程,需要明确三个步骤,异步操作、任务队列、事件循环。

1.异步操作

这里的异步操作就是上边提到的非阻塞的 I/O 操作,我们需要使用 async 和 await 关键字完善函数。await 只能在async 函数中使用,同步函数无法调用。

import asyncio

# io等待
async def download(url):
    await asyncio.sleep(1)  
# 网络请求
async def fetch(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()

async函数返回的是协程对象,无法直接调用。

2.事件循环

基于python同步和异步的差异,我们无法直接运行fetch,asyncio提供了事件循环执行异步函数。

# python 3.5/3.6
import asyncio
url = 'https://www.example.com/page1'
loop = asyncio.get_event_loop()
results = loop.run_until_complete(fetch(url))
# python 3.7
import asyncio
url = 'https://www.example.com/page1'
asyncio.run(fetch(url))

3.单个任务

# 网络请求
import asyncio
import aiohttp

async def fetch(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()
url = 'https://www.example.com/page1'
asyncio.run(fetch(url))

4.批量任务

我们需要批量执行fetch函数,fetch返回的是协程对象,我们需要把任务放在队列中,可以使用asyncio.gather或asyncio.wait方法。

async def crawl(urls):
	tasks = []
	for url in urls:
	    tasks.append(fetch(url, session))
	return await asyncio.gather(*tasks)

批量任务示例代码

import asyncio
import aiohttp

async def fetch(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()

async def crawl(urls):
	tasks = []
	for url in urls:
	    tasks.append(fetch(url))
	return await asyncio.gather(*tasks)
        
urls = ["https://www.example.com/page1",
        "https://www.example.com/page2",
        "https://www.example.com/page3"]
asyncio.run(crawl(urls))

其他示例

import asyncio
import aiohttp

async def fetch(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()

urls = ["https://www.example.com/page1",
        "https://www.example.com/page2",
        "https://www.example.com/page3"]
tasks = [fetch(url) for url in urls]
asyncio.run(asyncio.wait(tasks))

5.同步调用异步

同步函数调用asyncio.run的返回值是一个列表,与任务队列的顺序一致。

def main():
    urls = ["https://www.example.com/page1",
            "https://www.example.com/page2",
            "https://www.example.com/page3"]
    results = asyncio.run(crawl(urls))

    for result in results:
        print('result')
    return results
    
main()

6.网路同步请求和异步请求对比

源码如下:

import asyncio
import datetime

import aiohttp
import requests


def old(url):
    responseStr = requests.get(url=url).text


async def fetch(url,):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()


async def crawl(urls):
    tasks = []
    for url in urls:
        tasks.append(fetch(url))
    return await asyncio.gather(*tasks)


def main():
    urls = ["https://www.example.com/page1", "https://www.example.com/page2", "https://www.example.com/page2",
            "https://www.example.com/page2", "https://www.example.com/page2", "https://www.example.com/page3"]
    loop = asyncio.get_event_loop()
    results = loop.run_until_complete(crawl(urls))

    for result in results:
        print(result)


def old_main():
    urls = ["https://www.example.com/page1", "https://www.example.com/page2", "https://www.example.com/page2",
            "https://www.example.com/page2", "https://www.example.com/page2", "https://www.example.com/page3"]
    for url in urls:
        a = old(url)

if __name__ == "__main__":
    # 运行异步主函数
    a_t = datetime.datetime.now()
    # old_main()
    main()
    b_t = datetime.datetime.now() - a_t
    print("time", b_t)

三、自定义协程

Python中协程的实现主要有两种方式:使用生成器(yield关键字)和使用async/await关键字。

  1. 使用生成器(yield关键字)实现协程:
def coroutine():
    print("Coroutine started")
    value = yield
    print("Coroutine received:", value)

c = coroutine()
next(c)  # 启动协程
c.send("Hello")  # 发送数据给协程

在上面的示例中,coroutine函数是一个生成器函数,通过yield关键字来实现协程的暂停和恢复。首先需要调用next©来启动协程,然后通过c.send(value)来发送数据给协程,协程会在yield处暂停,并返回接收到的数据。

  1. 使用async/await关键字实现协程:
import asyncio

async def coroutine():
    print("Coroutine started")
    value = await asyncio.sleep(1)  # 模拟异步操作
    print("Coroutine received:", value)

asyncio.run(coroutine())

在上面的示例中,coroutine函数是一个协程函数,通过async关键字来定义。使用await关键字来等待异步操作的完成,并返回结果。在这个例子中,使用asyncio.sleep(1)来模拟一个异步操作,等待1秒钟后返回结果。

需要注意的是,在使用async/await关键字实现协程时,需要借助于事件循环(event loop)来驱动协程的执行。使用asyncio.run()来创建一个事件循环,并执行协程函数。

无论是使用生成器还是async/await关键字,协程都可以在遇到IO等待时暂停执行,并将控制权交给事件循环,从而实现了非阻塞的异步操作。这使得协程成为处理IO密集型任务的理想选择。

写一个协程三方库

要编写一个协程的第三方库,需要深入了解Python的协程机制以及协程库的设计原理。以下是一个简单示例,展示了一个使用生成器实现的简易协程库:

import queue

class Coroutine:
    def __init__(self, func):
        self.func = func
        self.queue = queue.Queue()

    def start(self):
        self._step()

    def _step(self, value=None, exc=None):
        try:
            if exc:
                result = self.func.throw(exc)
            else:
                result = self.func.send(value)
            if isinstance(result, Coroutine):
                result.queue = self.queue
                result._step()
            else:
                self.queue.put(result)
        except StopIteration as e:
            self.queue.put(e.value)
        except Exception as e:
            self.queue.put(e)
        finally:
            if not self.queue.empty():
                self.queue.get_nowait()._step()

    def send(self, value=None):
        self._step(value)

    def throw(self, exc):
        self._step(exc=exc)

    def join(self):
        return self.queue.get()

使用这个简易协程库可以实现协程的调度和运行。以下是一个示例,展示了如何使用这个库来调度协程的执行:

def coroutine1():
    while True:
        value = yield "coroutine1"
        print("coroutine1 received:", value)

def coroutine2():
    while True:
        value = yield "coroutine2"
        print("coroutine2 received:", value)

def main():
    coro1 = Coroutine(coroutine1())
    coro2 = Coroutine(coroutine2())
    
    coro1.start()
    coro2.start()
    
    for i in range(5):
        print("Sending value:", i)
        result1 = coro1.join()
        print("Result from coroutine1:", result1)
        
        result2 = coro2.join()
        print("Result from coroutine2:", result2)

if __name__ == "__main__":
    main()

这个示例中,我们定义了两个简单的协程函数coroutine1coroutine2,它们会不断地接收值并打印出来。在main函数中,我们创建了两个Coroutine对象分别对应这两个协程函数,并调用start方法来启动它们的执行。然后我们通过调用join方法来等待协程执行完成,并获得协程的返回值。

这只是一个简单的示例,实际编写一个全功能的协程库需要考虑更多的细节和实现方式。但这个示例可以帮助你理解协程库的基本工作原理和实现方式。

四、多进程+协程

当执行大量cpu计算和io操作时,可以通过多进程+协程的方式提高效率,多进程可以充分利用多核提高cpu计算能力,协程可以异步减少io等待。有一个第三方库aiomultiprocess,让你能用几行代码就实现多进程与协程的组合。

  1. pip 安装
pip install aiomultiprocess
  1. 使用
import asyncio
import httpx
from aiomultiprocess import Pool
 
async def get(url):
    async with httpx.AsyncClient() as client:
        resp = await client.get(url)
        return resp.text
 
 
async def main():
    urls = [url1, url2, url3]
    async with Pool() as pool:
        async for result in pool.map(get, urls):
            print(result)  # 每一个URL返回的内容
 
if __name__ == '__main__':
    asyncio.run(main())
  • 33
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我辈李想

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值