“什么是正确的方法?”这个问题的常见答案是“嗯,这取决于......”。如果不清楚您想要实现什么,我只能回答我可以在您的帖子中确定的一般性问题。
对于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后,将调用全局函数process和Model.nested。
process 被调用两次:一次在模型循环中,一次在Model.nested 回调中。从Model.request 更改的context_message 不可访问,但Model.nested 的更改可用于process。
怎么样?因为process 和Model.request 共享相同的父上下文(Model 可以检索context_message 的当前值),但是当Model 设置该变量时,它仅在其当前本地上下文中可用,无法由稍后调用(在另一个回调中)process。如果您希望 process 可以访问本地更改,则需要从回调中触发它,就像在 Model.nested 中所做的那样。
长话短说:AsyncMachine 的回调确实共享相同的父上下文,但无法访问彼此的本地上下文,因此更改 值 无效。但是,当上下文变量是 reference(如 context_model)时,对模型的更改可以在其他回调中访问。
使用 transitions 事件队列 (queued=True) 并依赖 contextvars 需要一些额外的考虑,因为 - 正如文档所述 - “在处理队列中的事件时,触发器调用将始终返回 True,因为在排队时无法确定涉及排队调用的转换是否最终会成功完成。即使只处理单个事件也是如此。"。触发的事件可能只添加到队列中。紧接着,它的上下文在事件被处理之前被留下。如果您需要排队处理和上下文变量并且也无法从 INSIDE 模型回调中调用函数,您应该检查 asyncio.Lock 并将您的调用包装到 loop 但保留 queued=False 以防止函数调用在完成之前返回。