python协程实现

1. 协程概念

   协程,也可以被称为微线程,是一种用户态内的上下文切换技术。简而言之,就是通过一个线程实现代码块的相互切换执行。
   进程和线程是内核级别的程序,而协程是函数级别的程序,是可以通过人为调用函数来实现的,核心机制就是 事件循环

  • 事件循环
      通过注册事件与事件处理函数。当发生了特定事件后,通过循环来知晓事件的发生,并调用注册的处理函数。总结起来就是三个步骤:注册、监听、处理。
  • 任务状态
      协程的任务状态有阻塞、就绪、运行。当一协程在运行状态,遇到了I/O操作,便会到事件循环中监听I/O操作完成了的事件,并注册自身的上下文以及自身的唤醒函数,之后该协程就变为阻塞状态。当监听的事件发生后,阻塞状态的协程就会被唤醒,进入就绪状态,将在下一次事件循环继续运行。

2. greenlet 实现协程

pip install greenlet
from greenlet import greenlet

def func1():
    print(1)                # 第二步:输出1
    gr2.switch()            # 第三步:切换到func2函数
    print(2)                # 第六步:输出2
    gr2.switch()            # 第七步:切换到func2函数,从上一次执行的位置继续往后执行

def func2():
    print(3)                # 第四步:输出3
    gr1.switch()            # 第五步:切换到func1函数,从上一次执行的位置继续往后执行

gr1 = greenlet(func1)
gr2 = greenlet(func2)

gr1.switch()                # 第一步:去执行func1函数

3. @asyncio.coroutine 和 yield from 实现协程

yield from 关键字用于被asyncio装饰的函数时,即可实现协程。在协程中,只要和IO任务类似的、耗费时间的任务都需要使用yield from 来进行中断,达到异步功能。

import asyncio

@asyncio.coroutine
def func1():
    print(1)
    yield from asyncio.sleep(2)         # 使用yield from来进行任务切换
    print(2)

@asyncio.coroutine
def func2():
    print(3)
    yield from asyncio.sleep(2)
    print(4)

tasks = [
    asyncio.ensure_future(func1()),
    asyncio.ensure_future(func2()),
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

可以把主流程封装成一个协程函数去执行:

import asyncio

@asyncio.coroutine
def func1():
    print(1)
    yield from asyncio.sleep(2)         # 只要和io任务类似的、耗费时间的任务,都需要使用yield from来进行中断
    print(2)
    return func1.__name__

@asyncio.coroutine
def func2():
    print(3)
    yield from asyncio.sleep(2)
    print(4)
    return func2.__name__

@asyncio.coroutine
def main():
    tasks = [func1(), func2()]
    done, pending = yield from asyncio.wait(tasks)      # done:已完成的任务列表;pending:未完成的任务列表
    for i in done:
        print(f"协程无序返回值:{i.result()}")

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

4. async & await 关键字实现协程

   通过 async & await 来将一个函数分成多个段,而这些段都是协程里面调度的单位,粒度比进程、线程都要小。只需要简单改造两步:
1. 把 @asyncio.coroutine 替换成 async
2. 把 yield from 替换成 await

import asyncio

async def func1():
    print(1)
    await asyncio.sleep(2)		# 在io或者耗时的操作,加上await
    print(2)
    return func1.__name__

async def func2():
    print(3)
    await asyncio.sleep(2)
    print(4)
    return func2.__name__

task1 = asyncio.create_task(func1())
task2 = asyncio.create_task(func2())
tasks = [task1, task2]
# loop = asyncio.get_event_loop()
# loop.run_until_complete(asyncio.wait(tasks))
# loop.close()
done, pending = asyncio.run(asyncio.wait(tasks))	# 上面注释的三行和这一行是等价的

同样可以把主流程封装成一个协程函数去执行:

import asyncio

async def func1():
    print(1)
    await asyncio.sleep(2)
    print(2)
    return func1.__name__

async def func2():
    print(3)
    await asyncio.sleep(2)
    print(4)
    return func2.__name__

async def main():
    task1 = asyncio.create_task(func1())
	task2 = asyncio.create_task(func2())
	tasks = [task1, task2]
    done, pending = await asyncio.wait(tasks)
    for i in done:
        print(f"协程无序返回值:{i.result()}")

asyncio.run(main())

5. 效率更高的uvloop

uvloop是asyncio事件循环的替代方案,在效率上uvloop事件循环 > python默认的asyncio事件循环。

pip install uvloop

如何替换使用?

import uvloop
import asyncio
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

# 编写asyncio的代码,和之前的一样
async def main():
    ...

# 内部的事件循环会自动变成uvloop
asyncio.run(main())

5. 协程案例

  • asyncio+支持异步操作的模块

操作redis的异步模块:aioredis

pip install aioredis==1.3.1
import asyncio
import aioredis     # 版本 aioredis==1.3.1

async def execute(address):
    print("start to connect")
  
    # 网络io操作:创建redis连接
    redis = await aioredis.create_redis(address)

    # 网络IO:redis设置哈希值
    await redis.hmset_dict("cars", key1=1, key2=2, key3=3)

    # 网络IO:redis取值
    result = await redis.hgetall("car", encoding="utf-8")

    # 关闭连接
    redis.close()

    # 关闭连接池
    await redis.wait_closed()

url_list = [execute(i) for i in ["redis://192.168.3.40:6379", "redis://192.168.3.41:6379"]]

asyncio.run(asyncio.wait(url_list))

操作mysql的异步模块:aiomysql

import asyncio
import aiomysql 

async def execute(host, passwd):
    print("start to connect")
    
    # 网络IO:先去连接ip1,遇到IO后自动切换任务,去连接ip2
    conn = await aiomysql.connect(host=host, port=3306, user="root", password=passwd, db="mysql")
    
    cur = await conn.cursor()
    
    await cur.execute("select * from user")
    
    userdata = await cur.fetchall()
    print(userdata)
    
    await cur.close()
    conn.close()
 
    print("connect over")

url_list = [execute("192.168.3.40", "root"), execute("192.168.3.41", "root")]

asyncio.run(asyncio.wait(url_list))

访问网路的异步模块:aiohttp

# 用aiohttp异步模块访问网站

import asyncio
import aiohttp

async def spider(session, url):
    print(f"web page url: {url}")
    async with session.get(url, verify_ssl=False) as response:  # 发送请求
        text = await response.text()
        print(f"page {url} result: {len(text)}")
    return text


async def main():
    async with aiohttp.ClientSession() as session:
        urls = [
            "https://python.org",
            "https://pypi.org/",
            "https://spark.apache.org/"
        ]
        tasks = [spider(session, url) for url in urls]
        done, pending = await asyncio.wait(tasks)
    return done


if __name__ == '__main__':
    asyncio.run(main())
  • asyncio + 不支持异步操作的模块
# 不使用aiohttp,去访问网站,和aiohttp做个对比

import asyncio
import requests

async def spider(url):
    print("start main")
    loop = asyncio.get_event_loop()

    # loop.run_in_executor(None, requests.get, url) :第一个参数为进程池或线程池对象,根据业务自定义,None表示使用默认的线程池。
    # 这行代码做了什么事?
    # 1. 先调用 ThreadPoolExecutor的submit方法,去线程池中申请一个线程去执行requests.get函数,并返回 concurrent.futures.Future对象(这种对象不支持await语法);
    # 2. 调用asyncio.warp_future将concurrent.futures.Future对象包装成asynci.Future对象,使其支持await语法。
    futu = loop.run_in_executor(None, requests.get, url)
    resp = await futu
    print(f"{url}: {resp}")

async def main():
    urls = [
        "https://python.org",
        "https://pypi.org/",
        "https://spark.apache.org/"
    ]
    tasks = [spider(url) for url in urls]
    await asyncio.wait(tasks)

if __name__ == '__main__':
    asyncio.run(main())

在上面的基础上,使用自定义的进程池或者线程池去访问网站

import asyncio
import requests

async def spider_user_thread_pool(thread_pool, url):
    """使用自定义的线程池"""
    loop = asyncio.get_event_loop()

    futu = loop.run_in_executor(thread_pool, requests.get, url)
    resp = await futu
    print(f"{url}: {resp}")

async def main():
    from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
    user_thread_pool = ThreadPoolExecutor(max_workers=2)
    urls = [
        "https://python.org",
        "https://pypi.org/",
        "https://spark.apache.org/"
    ]
    tasks = [spider_user_thread_pool(user_thread_pool, url) for url in urls]
    await asyncio.wait(tasks)

if __name__ == '__main__':
    asyncio.run(main())
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值