在 asyncio 中使用生成器而不是 FSM 保持会话状态
keeping session state with generators instead of FSM in asyncio
我想编写一些 asyncio 代码来保存多个 users/connections 的会话状态。
这是一个小玩具示例:
Please tell me your name:
asd
how old are you?
40
asd is 40 years old
我认为最好的实现方式就像生成器函数:
class MessageReciever():
async def start_conversation(user_id):
await send_message(user_id, "Please tell me your name")
name = await wait_for_reply(user_id)
await send_message(user_id, "how old are you?")
age = await wait_for_reply(user_id)
await send_message(user_id, "{} is {} years old".format(name, age)
目前,我只有一个函数 async on_message(user_id, message)
,每次收到消息时都会调用该函数。这对我现在拥有的所有东西都很好,它们是无状态的。
这种理想的方法会引发许多问题:
- 我不知道如何 await() 一个由我自己的代码触发的事件。
- 如果对话时间太长,我不知道如何使对话超时。我不想将会话永远保留在 RAM 中,而且似乎没有一种简单的方法可以结束 运行 协程或将其清除到数据库中。
解决这个问题的典型方法是编写 FSM,并将每个用户的状态保存在单独的结构中,但是编写和调试大型 FSM 非常烦人,尤其是在制作原型时。我也不需要 FSM 中的任何循环。
我如何实现一种简单的方法来在 asyncio 中保持有状态 "conversations"?
处理这个问题的最好方法是停止考虑事件和回调,转而考虑使用线程。除了与普通线程不同,它可以随时中断您的程序以切换到不同的线程,您的线程只能在您使用 await
调用某些东西时被中断。
在此模型中,您倾向于将大量本地状态保留在堆栈中,除非您有需要在线程之间共享的状态。然后你应该确保当你进行 await
调用时它不会处于不一致的状态。
如果您需要启动新线程,请对您正在使用的 library/framework 使用适当的调用。对于 asyncio
,那将是 asyncio.ensure_future
(这不是一个好名字)。如果您需要在线程之间进行通信,请使用线程间(好吧,这些奇怪的线程之间)队列。对于 asyncio
,那将是 asyncio.Queue
。我建议您在执行此操作时始终指定最大队列大小,以避免一个线程在另一个线程从队列中读取它之前将大量内容发送到另一个线程的情况。
Inter-'thread' 队列 class 将有一个 await
able put
和 get
方法,因此如果队列已满,线程可以暂停等待它清空一点再发送。
如果你这样想,你就会自然而然地找到编写程序的正确方法。
要回答有关如何等待您自己的事件之一的问题,只需使用一个 'thread' 间队列和 post 您的事件,并拥有等待它的东西从该队列中读取。
asyncio
模块也有一个超时 await
的方法。这样做:
await asyncio.wait_for(thing_to_timeout, 10) # 不管返回前等待 10 秒。
如果在 thing_to_timeout
返回答案之前时间到期,它将抛出一个 asyncio.TimeoutError
。
我想编写一些 asyncio 代码来保存多个 users/connections 的会话状态。
这是一个小玩具示例:
Please tell me your name:
asd
how old are you?
40
asd is 40 years old
我认为最好的实现方式就像生成器函数:
class MessageReciever():
async def start_conversation(user_id):
await send_message(user_id, "Please tell me your name")
name = await wait_for_reply(user_id)
await send_message(user_id, "how old are you?")
age = await wait_for_reply(user_id)
await send_message(user_id, "{} is {} years old".format(name, age)
目前,我只有一个函数 async on_message(user_id, message)
,每次收到消息时都会调用该函数。这对我现在拥有的所有东西都很好,它们是无状态的。
这种理想的方法会引发许多问题:
- 我不知道如何 await() 一个由我自己的代码触发的事件。
- 如果对话时间太长,我不知道如何使对话超时。我不想将会话永远保留在 RAM 中,而且似乎没有一种简单的方法可以结束 运行 协程或将其清除到数据库中。
解决这个问题的典型方法是编写 FSM,并将每个用户的状态保存在单独的结构中,但是编写和调试大型 FSM 非常烦人,尤其是在制作原型时。我也不需要 FSM 中的任何循环。
我如何实现一种简单的方法来在 asyncio 中保持有状态 "conversations"?
处理这个问题的最好方法是停止考虑事件和回调,转而考虑使用线程。除了与普通线程不同,它可以随时中断您的程序以切换到不同的线程,您的线程只能在您使用 await
调用某些东西时被中断。
在此模型中,您倾向于将大量本地状态保留在堆栈中,除非您有需要在线程之间共享的状态。然后你应该确保当你进行 await
调用时它不会处于不一致的状态。
如果您需要启动新线程,请对您正在使用的 library/framework 使用适当的调用。对于 asyncio
,那将是 asyncio.ensure_future
(这不是一个好名字)。如果您需要在线程之间进行通信,请使用线程间(好吧,这些奇怪的线程之间)队列。对于 asyncio
,那将是 asyncio.Queue
。我建议您在执行此操作时始终指定最大队列大小,以避免一个线程在另一个线程从队列中读取它之前将大量内容发送到另一个线程的情况。
Inter-'thread' 队列 class 将有一个 await
able put
和 get
方法,因此如果队列已满,线程可以暂停等待它清空一点再发送。
如果你这样想,你就会自然而然地找到编写程序的正确方法。
要回答有关如何等待您自己的事件之一的问题,只需使用一个 'thread' 间队列和 post 您的事件,并拥有等待它的东西从该队列中读取。
asyncio
模块也有一个超时 await
的方法。这样做:
await asyncio.wait_for(thing_to_timeout, 10) # 不管返回前等待 10 秒。
如果在 thing_to_timeout
返回答案之前时间到期,它将抛出一个 asyncio.TimeoutError
。