Python黑魔法--异步IO(asyncio)协程 (三)

接: https://blog.csdn.net/Enjolras_fuu/article/details/83869864

不同线程的事件循环

很多时候,我们的事件循环用于注册协程,而有的协程需要动态的添加到事件循环中。一个简单的方式就是使用多线程。当前线程创建一个事件循环,然后在新建一个线程,在新线程中启动事件循环。当前线程不会被block。

import time
import asyncio
from threading import Thread

now = lambda: time.time()


def start_loop(loop):
    # 开启事件循环
    asyncio.set_event_loop(loop)
    loop.run_forever()


def more_work(x):
    print('More work {}'.format(x))
    time.sleep(x)
    print('Finished more work {}'.format(x))


start = now()
# 创建一个新的事件循环
new_loop = asyncio.new_event_loop()
# 在当前线程去创建 在新的线程中去开启循环
t = Thread(target=start_loop, args=(new_loop,))
# 开启线程
t.start()
print('TIME: {}'.format(time.time() - start))

# 当前程序执行的主线程不会被阻塞 
# 新线程中会按照顺序执行 call_soon_threadsafe 方法中注册的 more_work 方法
# 在新线程中 time.sleep 操作是同步阻塞的 因此运行完毕 more_work 需要大致 6+3 
new_loop.call_soon_threadsafe(more_work, 6)
new_loop.call_soon_threadsafe(more_work, 3)

启动上述代码之后,当前线程不会被block,新线程中会按照顺序执行call_soon_threadsafe方法注册的more_work方法,后者因为time.sleep操作是同步阻塞的,因此运行完毕more_work需要大致6 + 3

新线程协程

import time
import asyncio
from  threading import Thread

now = lambda: time.time()


def start_loop(loop):
    asyncio.set_event_loop(loop)
    loop.run_forever()


async def do_some_work(x):
    print('Waiting {}'.format(x))
    await asyncio.sleep(x)
    print('Done after {}s'.format(x))


def more_work(x):
    print('More work {}'.format(x))
    time.sleep(x)
    print('Finished more work {}'.format(x))


start = now()
# 主线程中创建一个 new_loop
new_loop = asyncio.new_event_loop()
# 创建子线程 在其中开启无限事件循环
t = Thread(target=start_loop, args=(new_loop,))
t.start()
print('TIME: {}'.format(time.time() - start))

# 在主线程中新注册协程对象
# 这样即可在子线程中进行事件循环的并发操作 同时主线程又不会被 block 
# 一共执行的时间大概在 6 s 左右 
asyncio.run_coroutine_threadsafe(do_some_work(6), new_loop)
asyncio.run_coroutine_threadsafe(do_some_work(4), new_loop)

上述的例子,主线程中创建一个new_loop,然后在另外的子线程中开启一个无限事件循环。主线程通过run_coroutine_threadsafe新注册协程对象。这样就能在子线程中进行事件循环的并发操作,同时主线程又不会被block。一共执行的时间大概在6s左右。

master-worker主从模式

对于并发任务,通常是用生成消费模型,对队列的处理可以使用类似master-worker的方式,master主要用户获取队列的msg,worker用户处理消息。
为了简单起见,并且协程更适合单线程的方式,我们的主线程用来监听队列,子线程用于处理队列。这里使用redis的队列。主线程中有一个是无限循环,用户消费队列。

import time
import asyncio
from threading import Thread
import redis


def get_redis():   # 返回一个 redis 连接对象
    connection_pool = redis.ConnectionPool(host='127.0.0.1', db=3)
    return redis.Redis(connection_pool=connection_pool)


def start_loop(loop):  # 开启事件循环
    asyncio.set_event_loop(loop)
    loop.run_forever()


async def worker(task):
    print('Start worker')

    while True:
        # start = now()
        # task = rcon.rpop("queue")   # 从 redis 中 取出的数据
        # if not task:
        #     await asyncio.sleep(1)
        #     continue
        print('Wait ', int(task))  # 取出了相应的任务
        await asyncio.sleep(int(task))
        print('Done ', task, now() - start)


now = lambda: time.time()
rcon = get_redis()
start = now()
# 创建一个事件循环
new_loop = asyncio.new_event_loop()
# 创建一个线程 在新的线程中开启事件循环
t = Thread(target=start_loop, args=(new_loop,))
t.setDaemon(True)  # 设置线程为守护模式
t.start()  # 开启线程

try:
    while True:
        task = rcon.rpop("queue")   # 不断从队列中获取任务
        if not task:
            time.sleep(1)
            continue

        # 包装为 task ins, 传入子线程中的事件循环
        asyncio.run_coroutine_threadsafe(worker(task), new_loop)
except Exception as e:
    print('error', e)
    new_loop.stop()   # 出现异常 关闭时间循环
finally:
    pass

当我们发起了一个耗时5s的操作,然后又发起了连个1s的操作,可以看见子线程并发的执行了这几个任务,其中5s awati的时候,相继执行了1s的两个任务。

停止子线程

如果一切正常,那么上面的例子很完美。可是,需要停止程序,直接ctrl+c,会抛出KeyboardInterrupt错误:

try:
    while True:
        task = rcon.rpop("queue")   # 不断从队列中获取任务
        if not task:
            time.sleep(1)
            continue

        # 包装为 task ins, 传入子线程中的事件循环
        asyncio.run_coroutine_threadsafe(worker(task), new_loop)
except KeyboardInterrupt as e:
    print('error', e)
    new_loop.stop()   # 出现异常 关闭事件循环
finally:
    pass

可是实际上并不好使,虽然主线程try了KeyboardInterrupt异常,但是子线程并没有退出,为了解决这个问题,可以设置子线程为守护线程,这样当主线程结束的时候,子线程也随机退出。

new_loop = asyncio.new_event_loop()
# 创建一个线程 在新的线程中开启事件循环
t = Thread(target=start_loop, args=(new_loop,))
t.setDaemon(True)  # 设置线程为守护模式
t.start()  # 开启线程

线程停止程序的时候,主线程退出后,子线程也随机退出才了,并且停止了子线程的协程任务。

aiohttp

在消费队列的时候,我们使用asyncio的sleep用于模拟耗时的io操作。以前有一个短信服务,需要在协程中请求远程的短信api,此时需要是需要使用aiohttp进行异步的http请求。大致代码如下:
server.py

import time
from flask import Flask

app = Flask(__name__)


@app.route('/<int:x>')
def index(x):
    time.sleep(x)
    return "{} It works".format(x)


@app.route('/error')
def error():
    time.sleep(3)
    return "error!"


if __name__ == '__main__':
    app.run(debug=True)

/接口表示短信接口,/error表示请求/失败之后的报警。
async-custoimer.py

import time
import asyncio
from threading import Thread
import redis
import aiohttp


def get_redis():
    connection_pool = redis.ConnectionPool(host='127.0.0.1', db=3)
    return redis.Redis(connection_pool=connection_pool)


rcon = get_redis()


def start_loop(loop):
    asyncio.set_event_loop(loop)
    loop.run_forever()


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


async def do_some_work(x):
    print('Waiting ', x)
    try:
        # 在fetch的时候进行了异常的捕获 
        # 如果发生了异常就去请求错误的url路径 
        # 如果没有做这个异常处理,即使子线程发生了异常,子线程的事件循环也不会退出,主线程也不会退出
        # 找到将子线程的异常raise传播到主线程的方法??
        ret = await fetch(url='http://127.0.0.1:5000/{}'.format(x))
        print(ret)
    except Exception as e:
        try:
            print(await fetch(url='http://127.0.0.1:5000/error'))
        except Exception as e:
            print(e)
    else:
        print('Done {}'.format(x))

new_loop = asyncio.new_event_loop()
t = Thread(target=start_loop, args=(new_loop,))
t.setDaemon(True)
t.start()

try:
    while True:
        task = rcon.rpop("queue")
        if not task:
            time.sleep(1)
            continue
        asyncio.run_coroutine_threadsafe(do_some_work(int(task)), new_loop)
except Exception as e:
    print('error')
    new_loop.stop()
finally:
    pass

对于redis的消费,还有一个block的方法:

try:
    while True:
        # 使用 brpop 方法 会 block 住 task  如果主线程有消息 才会进行消费 
        _, task = rcon.brpop("queue")
        asyncio.run_coroutine_threadsafe(do_some_work(int(task)), new_loop)
except Exception as e:
    print('error', e)
    new_loop.stop()
finally:
    pass

使用 brpop方法,会block住task,如果主线程有消息,才会消费。测试了一下,似乎brpop的方式更适合这种队列消费的模型。

协程消费

主线程用于监听队列,然后子线程的做事件循环的worker是一种方式。还有一种方式实现这种类似master-worker的方案。即把监听队列的无限循环逻辑移动到协程中。程序初始化就创建若干个协程,实现类似并行的效果。

import time
import asyncio
import redis

now = lambda : time.time()

def get_redis():
    connection_pool = redis.ConnectionPool(host='127.0.0.1', db=3)
    return redis.Redis(connection_pool=connection_pool)

rcon = get_redis()

async def worker():
    print('Start worker')

    while True:
        start = now()
        task = rcon.rpop("queue")
        if not task:
            await asyncio.sleep(1)
            continue
        print('Wait ', int(task))
        await asyncio.sleep(int(task))
        print('Done ', task, now() - start)

def main():
    asyncio.ensure_future(worker())
    asyncio.ensure_future(worker())

    loop = asyncio.get_event_loop()
    try:
        loop.run_forever()
    except KeyboardInterrupt as e:
        print(asyncio.gather(*asyncio.Task.all_tasks()).cancel())
        loop.stop()
        loop.run_forever()
    finally:
        loop.close()

if __name__ == '__main__':
    main()

这样做就可以多多启动几个worker来监听队列。一样可以到达效果。

转载

https://www.jianshu.com/p/b5e347b3a17c
在此感谢大佬!

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值