【问题标题】:Correct way of using AsyncMachine and multiple objects in transitions在转换中使用 AsyncMachine 和多个对象的正确方法
【发布时间】:2020-11-06 07:52:42
【问题描述】:

我正在尝试通过 websockets 实现客户端-服务器应用程序 我有几个疑问如何正确地维护每个连接的客户端的状态。

全局机器 + 每个连接的许多对象?
机器 + 对象 - 每个连接?

所以,我已经开始了几个测试,以检查它是如何同时工作的

主机

class AsyncModel:
    def __init__(self, id_):
        self.req_id = id_

    async def prepare_model(self, _):
        print("prepare_model", self.req_id)


    async def before_change(self, _):
        print("before_change", self.req_id)


    async def after_change(self, _):
        print("After change", self.req_id)


transition = dict(trigger="start", source="Start", dest="Done",
                  prepare="prepare_model",
                  before=["before_change"],
                  after="after_change")

还有几种跑步类型

我想同时改变所有模型的状态


async def main():
    tasks = []
    machine = AsyncMachine(model=None,
                           states=["Start", "Done"],
                           transitions=[transition],
                           initial='Start',
                           send_event=True,
                           queued=True)
    for i in range(3):
        model = AsyncModel(id_=i)
        machine.add_model(model)

        tasks.append(model.start())

    await asyncio.gather(*tasks)

    for m in machine.models:
        machine.remove_model(m)

asyncio.run(main())

但输出是:


prepare_model 0
before_change 0
After change 0
prepare_model 1
before_change 1
After change 1
prepare_model 2
before_change 2
After change 2

如果我创建机器 + 模型:


async def main():
    tasks = []

    for i in range(3):
        model = AsyncModel(id_=i)
        machine = AsyncMachine(model=model,
                               states=["Start", "Done"],
                               transitions=[transition],
                               initial='Start',
                               send_event=True,
                               queued=True)


        tasks.append(model.start())

    await asyncio.gather(*tasks)

输出是:

prepare_model 0
prepare_model 1
prepare_model 2
before_change 0
before_change 1
before_change 2
After change 0
After change 1
After change 2

正确的方法是什么?

更新

我希望每个正在运行的模型都有可用的 contextvar,以便能够正确记录模型调用的其他模块的所有活动,而不是将某些标识符显式传递给每个外部函数调用(outisde 模型类)
查看某种示例https://pastebin.com/qMfh0kNb,它没有按预期工作,断言触发

【问题讨论】:

  • 欢迎来到 StackOverflow!这对您来说可能并不明显,但您的问题很难理解。目前还不清楚“机器”指的是什么,代码的作用是什么(例如,您从不显示add_model 的定义)或者您期望每个sn-p 的输出类型。我建议您编辑以下答案:1)修剪不必要的代码,但使示例可运行,2)清楚地表示您期望的输出(以及为什么),以及 3)提供一个清晰且可回答的问题。您现有的问题,“正确的方法是什么?”除非人们已经了解您的目标,否则它会含糊不清。

标签: python python-asyncio transition pytransitions


【解决方案1】:

“什么是正确的方法?”这个问题的常见答案是“嗯,这取决于......”。如果不清楚您想要实现什么,我只能回答我可以在您的帖子中确定的一般性问题。

对于transitions,我应该为每个型号使用一台机器还是为所有型号使用一台机器?

使用transitions 时,模型是有状态的并且包含转换回调。机器在那里充当一种“规则手册”。因此,当机器具有相同的配置时,对于大多数用例,我建议对所有型号使用一台机器。在大多数情况下,使用具有相同配置的多台机器只会增加内存占用和代码复杂性。在我的脑海中,我可以想到一个用例,其中拥有多台具有相同配置的机器可能会很有用。但首先你可能想知道为什么两个版本的行为不同,即使我刚才说应该没有区别。

为什么在使用一个 AsyncMachine 与多个 AsyncMachines 时调用回调的顺序不同?

如果没有自定义参数,使用一个AsyncMachine 或多个AsyncMachines 没有区别。但是,您在构造函数中传递了queued=True,根据Documentation 这样做:

如果启用队列处理,则在触发下一个转换之前完成转换

这就是为什么您的单台机器会一次考虑一个转换,在转移到下一个事件/转换之前处理 ONE 模型的所有回调。 由于每台机器都有自己的事件/转换队列,因此在使用多台机器时会立即处理事件。通过 queued=True 在您的示例中对多台机器没有影响。通过不传递 queued 参数或传递 queued=False(默认值),您可以在一台机器上获得相同的行为。我对您的示例进行了一些修改以进行说明:

from transitions.extensions import AsyncMachine
import asyncio


class AsyncModel:
    def __init__(self, id_):
        self.req_id = id_

    async def prepare_model(self):
        print("prepare_model", self.req_id)

    async def before_change(self):
        print("before_change", self.req_id)

    async def after_change(self):
        print("after change", self.req_id)


transition = dict(trigger="start", source="Start", dest="Done",
                  prepare="prepare_model",
                  before="before_change",
                  after="after_change")

models = [AsyncModel(i) for i in range(3)]


async def main(queued):
    machine = AsyncMachine(model=models,
                           states=["Start", "Done"],
                           transitions=[transition],
                           initial='Start',
                           queued=queued)

    await asyncio.gather(*[model.start() for model in models])
    # alternatively you can dispatch an event to all models of a machine by name
    # await machine.dispatch("start")

print(">>> Queued=True")
asyncio.run(main(queued=False))
print(">>> Queued=False")
asyncio.run(main(queued=False))

所以这取决于你需要什么。使用一台机器,您可以同时使用queued=True 对事件进行顺序处理或使用queued=False 进行并行处理。

您提到有一个用例可能需要多台机器......

在文档中有这样一段话:

您应该考虑将 queued=True 传递给 TimeoutMachine 构造函数。这将确保事件是按顺序处理的,并避免在超时和事件接近发生时可能出现的异步竞速条件。

当使用超时事件或其他连续发生的事件时,当同时处理同一模型上的多个转换时,可能会出现racing conditions。因此,当此问题影响您的用例并且您需要在单独的模型上并行处理转换时,拥有多台具有相同配置的机器可能是一种解决方案。

如何使用AsyncMachine 中的上下文?

这对我来说是薄冰,我可能不正确。我可以尝试简要总结一下我目前对事物为何以某种方式表现的理解。考虑这个例子:

from transitions.extensions import AsyncMachine
import asyncio
import contextvars

context_model = contextvars.ContextVar('model', default=None)
context_message = contextvars.ContextVar('message', default="unset")

def process():
    model = context_model.get()
    print(f"Processing {model.id} Request {model.count} => '{context_message.get()}'")


class Model:

    def __init__(self, id):
        self.id = id
        self.count = 0

    def request(self):
        self.count += 1
        context_message.set(f"Super secret request from {self.id}")

    def nested(self):
        context_message.set(f"Not so secret message from {self.id}")
        process()


models = [Model(i) for i in range(3)]


async def model_loop(model):
    context_model.set(model)
    context_message.set(f"Hello from the model loop of {model.id}")
    while model.count < 3:
        await model.loop()


async def main():
    machine = AsyncMachine(model=models, initial='Start', transitions=[['loop', 'Start', '=']],
                           before_state_change='request',
                           after_state_change=[process, 'nested'])
    await asyncio.gather(*[model_loop(model) for model in models])

asyncio.run(main())

输出:

# Processing 0 Request 1 => 'Hello from the model loop of 0'
# Processing 0 Request 1 => 'Not so secret message from 0'
# Processing 1 Request 1 => 'Hello from the model loop of 1'
# Processing 1 Request 1 => 'Not so secret message from 1'
# Processing 2 Request 1 => 'Hello from the model loop of 2'
# Processing 2 Request 1 => 'Not so secret message from 2'
# Processing 0 Request 2 => 'Hello from the model loop of 0'
# Processing 0 Request 2 => 'Not so secret message from 0'
# Processing 1 Request 2 => 'Hello from the model loop of 1'
# Processing 1 Request 2 => 'Not so secret message from 1'
# Processing 2 Request 2 => 'Hello from the model loop of 2'
# Processing 2 Request 2 => 'Not so secret message from 2'
# Processing 0 Request 3 => 'Hello from the model loop of 0'
# Processing 0 Request 3 => 'Not so secret message from 0'
# Processing 1 Request 3 => 'Hello from the model loop of 1'
# Processing 1 Request 3 => 'Not so secret message from 1'
# Processing 2 Request 3 => 'Hello from the model loop of 2'
# Processing 2 Request 3 => 'Not so secret message from 2'

触发事件已转发到设置两个上下文变量的模型循环。两者都被process 使用,这是一个使用上下文变量进行处理的全局函数。当触发转换时,Model.request 将在转换之前被调用并增加Model.count。更改Model.state后,将调用全局函数processModel.nested

process 被调用两次:一次在模型循环中,一次在Model.nested 回调中。从Model.request 更改的context_message 不可访问,但Model.nested 的更改可用于process。 怎么样?因为processModel.request 共享相同的父上下文(Model 可以检索context_message 的当前值),但是当Model 设置该变量时,它仅在其当前本地上下文中可用,无法由稍后调用(在另一个回调中)process。如果您希望 process 可以访问本地更改,则需要从回调中触发它,就像在 Model.nested 中所做的那样。

长话短说:AsyncMachine 的回调确实共享相同的父上下文,但无法访问彼此的本地上下文,因此更改 无效。但是,当上下文变量是 reference(如 context_model)时,对模型的更改可以在其他回调中访问。

使用 transitions 事件队列 (queued=True) 并依赖 contextvars 需要一些额外的考虑,因为 - 正如文档所述 - “在处理队列中的事件时,触发器调用将始终返回 True,因为在排队时无法确定涉及排队调用的转换是否最终会成功完成。即使只处理单个事件也是如此。"。触发的事件可能只添加到队列中。紧接着,它的上下文在事件被处理之前被留下。如果您需要排队处理和上下文变量并且也无法从 INSIDE 模型回调中调用函数,您应该检查 asyncio.Lock 并将您的调用包装到 loop 但保留 queued=False 以防止函数调用在完成之前返回。

【讨论】:

  • 感谢您的回答!首先,我的想法是将 contextvars 与 AsyncMachine 一起使用 - 让每个模型在每个自己的上下文中运行,并有自己的 contextvars 可用。但我无法做到。我尝试了不同的方法来为 contextvars 创建模型/机器/设置值 - 没有任何帮助。我认为可能有某种方法可以做到这一点.. 可能就像 LockedMachine 一样?
  • @Eugene:我添加了一段关于contextvarAsyncMachine 的内容。异步 (AsyncMachine) 或线程 (LockedMachine) 是否更适合您的需求,我无法确定。在这两种情况下,都应该可以共享上下文变量。当涉及到并行处理(异步或线程)时,深入了解正在发生的事情是一个好主意。将代码扔到问题上直到它(现在)工作可能会导致将来难以调试问题。
  • 我添加了一些我正在尝试做的示例功能pastebin.com/qMfh0kNb,但它没有按预期工作。顺便说一句,我还更新了主要问题 - 主要目的
  • 在文档中它说:“重要说明:在处理队列中的事件时,触发器调用将始终返回 True,因为在排队时无法确定涉及排队调用的转换是否会最终成功完成。即使只处理一个事件也是如此。当您在另一个上下文中使用queued=True 调用循环时,触发器将被添加到事件队列中,并且调用在实际执行之前返回/退出上下文。换句话说:一个排队的函数可能不会在它被触发的上下文中执行。
  • 我还有一个关于队列状态处理的问题 - 可以为状态机中的每个添加模型创建单独的队列吗?因此,每个运行模型的状态将被并行处理,而不是对所有模型都按顺序处理。
猜你喜欢
  • 1970-01-01
  • 2017-04-25
  • 2011-03-25
  • 1970-01-01
  • 2021-02-12
  • 2021-07-30
  • 2021-07-01
  • 2014-11-06
  • 1970-01-01
相关资源
最近更新 更多