共享状态与黑板模式
正在读取学习记录…
共享状态是多位处理者共同读写的一份任务数据。黑板模式把协作围绕这份数据组织起来:每位参与者读取需要的区域,把自己的产物写回指定位置,其他参与者根据新数据继续工作。
黑板与群聊有一个明显区别:群聊按消息到达顺序排列,黑板按数据含义组织。订单已付款120元、分析者建议退120元、协调者已确认候选,这三句话长得很像,却属于事实、建议和处理结果三个不同区域。
如果都放进同一个“最新金额”字段,最后发言的人就可能覆盖业务事实。热闹有了,订单没了。
两位读者拿到旧快照时会发生什么
黑板初始版本为v0。订单读取者写入paid=120后成为v1。分析者基于v1写建议,版本成为v2。
协调者也曾读过v1,随后想提交结果。此时它的快照已经过期。程序检查预期版本是否等于当前版本,拒绝这次提交,要求重新读取。协调者读到v2后再提交,版本成为v3。
这种做法叫乐观并发控制:读的时候不长期锁住数据,写的时候确认“我读取后数据没有被别人修改”。版本冲突不说明对方一定写错;它说明本次提交的依据可能落后了。
保存完整程序并运行
在自己的学习目录打开PowerShell,创建一个新的子目录:
mkdir l6-shared-state
cd l6-shared-state把下面完整代码保存为 shared_state_demo.py。固定数据和运行入口都已包含,文件名保留 .py 后缀。
"""带版本检查的单进程黑板。"""
from copy import deepcopy
OWNERS = {"facts": "order_reader", "proposals": "analyst", "result": "coordinator"}
class Blackboard:
def __init__(self):
self.version = 0
self.data = {"facts": {}, "proposals": {}, "result": {}}
def snapshot(self):
# 返回副本,读者改自己的快照不会修改黑板。
return self.version, deepcopy(self.data)
def commit(self, actor, section, changes, expected_version):
if section not in OWNERS or OWNERS[section] != actor:
raise PermissionError("该角色无权修改此区域")
if expected_version != self.version:
raise ValueError("版本冲突,请重新读取")
if not isinstance(changes, dict):
raise ValueError("更新必须是字典")
# 先验证,再整体替换;这段演示由一个进程顺序执行。
updated = deepcopy(self.data)
updated[section].update(deepcopy(changes))
self.data = updated
self.version += 1
return self.version
def main():
board = Blackboard()
version, _ = board.snapshot()
board.commit("order_reader", "facts", {"paid": 120}, version)
print(f"事实写入:v{board.version};paid={board.data['facts']['paid']}")
old_version, _ = board.snapshot()
board.commit("analyst", "proposals", {"refund": 120}, old_version)
print(f"建议写入:v{board.version};refund={board.data['proposals']['refund']}")
try:
board.commit("coordinator", "result", {"approved": True}, old_version)
except ValueError as error:
print("主管提交:" + str(error))
current, _ = board.snapshot()
board.commit("coordinator", "result", {"approved": True}, current)
print(f"重新读取后:v{board.version};approved={board.data['result']['approved']}")
try:
board.commit("analyst", "facts", {"paid": 150}, board.version)
except PermissionError as error:
print("越界修改:" + str(error))
if __name__ == "__main__":
main()保存后执行 dir,确认能看到 shared_state_demo.py,再运行:
ls
python -X utf8 shared_state_demo.py程序输出:
事实写入:v1;paid=120
建议写入:v2;refund=120
主管提交:版本冲突,请重新读取
重新读取后:v3;approved=True
越界修改:该角色无权修改此区域分开保存事实、建议和结果
OWNERS规定facts只能由order_reader写,proposals只能由analyst写,result只能由coordinator写。分析者建议退款120元可以写proposals;它尝试把facts中的已付款改为150,程序拒绝。
commit中的actor是应用传入的执行角色。接入跨进程消息时,这个值应从已认证身份映射出来,不能让待处理消息自报“我是order_reader”就获得事实写权限。黑板划分字段与身份校验要共同成立。
snapshot返回深拷贝,即包括内部嵌套字典的副本。参与者修改自己手上的快照,不会偷偷修改共享黑板。commit也复制调用者的changes,防止提交后再改原字典影响已写入状态。
版本冲突后应该做什么
主管持有old_version=1,却在v2时提交。程序先比较版本再写入,所以失败没有修改result,也没有增加版本。重新读取后提交得到v3,说明冲突处理不是简单重试原来的旧版本。
重新读取之后还应重新判断。假设订单读取者在期间发现已经发货,协调者的旧“允许退款”建议可能需要改变;不能把版本号改成最新值,内容却继续用旧判断。例子期间只增加建议,业务事实没变,所以同一结果仍能提交。
统一版本简单,但不同区域的更新也会冲突。订单事实与纯文字润色完全独立时,可以考虑区域版本或按字段合并。这样会减少无关冲突,也增加跨字段一致性检查的难度。若退款金额依赖订单事实,就不能把它们当作完全独立的两份数据。
多进程时,检查与提交必须是同一个动作
代码在单进程中顺序执行检查和替换,没有启动并发线程。两个数据库客户端同时读v2、同时检查通过时,真正的存储端必须保证只有一个版本匹配的提交成功。
数据库可以使用“WHERE version=2”的条件更新,或在事务中检查并递增版本;进程内并发可以用锁保护这段操作。把检查和写入分成两个独立网络请求,会留下两位写者一起覆盖的时间窗口。
黑板还应保留数据来源和状态变化记录。facts区域并不是贴上标签就成为真相;写入者需要附订单编号、读取时间或资料版本,让后续判断能找到依据。过期建议也应可识别,避免误当最新批准结果。
让提交顺序产生可见变化
把main中协调者第一次commit的old_version换成board.version,运行python -X utf8 shared_state_demo.py。第一次主管提交会成功,后面重新读取后的提交仍会成功,因此最终版本升到v4。
这次变化说明版本门禁检查的是调用时给出的预期值。它并没有替你证明内容基于最新快照生成。恢复old_version后,再运行同一命令,就能重新看到冲突与重新读取路径。
若出现版本冲突,先检查快照取得的位置和中间写入;若出现PermissionError,查看角色与区域的OWNERS映射。不要为消除报错而允许所有角色写所有字段。
参考阅读:LangGraph Graph API中的state与reducers,比较“替换字段”和“按规定合并字段”的语义。数据库事务与持久化是存储实现还需补齐的部分。
运行配套检查
把下面测试完整保存为 test_shared_state_demo.py,与主程序放在同一目录。5项检查覆盖正常区域写入、快照副本、旧版本不写入、越权拒绝和提交数据的后续修改。
查看测试代码:test_shared_state_demo.py
"""保护快照、字段归属和旧版本提交。"""
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import shared_state_demo as demo
class BlackboardTests(unittest.TestCase):
def test_normal_fact_and_proposal(self):
board = demo.Blackboard()
board.commit("order_reader", "facts", {"paid": 120}, 0)
board.commit("analyst", "proposals", {"refund": 120}, 1)
self.assertEqual(board.version, 2)
self.assertEqual(board.data["facts"], {"paid": 120})
def test_snapshot_is_independent(self):
board = demo.Blackboard()
_, snapshot = board.snapshot()
snapshot["facts"]["paid"] = 999
self.assertEqual(board.data["facts"], {})
def test_stale_commit_does_not_change_anything(self):
board = demo.Blackboard()
board.commit("order_reader", "facts", {"paid": 120}, 0)
before = board.snapshot()
with self.assertRaises(ValueError):
board.commit("analyst", "proposals", {"refund": 120}, 0)
self.assertEqual(board.snapshot(), before)
def test_role_cannot_promote_opinion_to_fact(self):
board = demo.Blackboard()
with self.assertRaises(PermissionError):
board.commit("analyst", "facts", {"paid": 150}, 0)
self.assertEqual(board.version, 0)
def test_input_dict_cannot_change_committed_value(self):
board = demo.Blackboard()
changes = {"order": {"paid": 120}}
board.commit("order_reader", "facts", changes, 0)
changes["order"]["paid"] = 999
self.assertEqual(board.data["facts"]["order"]["paid"], 120)
if __name__ == "__main__":
unittest.main()python -X utf8 -m unittest -v test_shared_state_demo.py测试名称后应显示 ok,末尾显示 Ran 5 tests 和 OK。若找不到模块,先用 dir 确认两个文件保存在同一目录。
课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 两个执行者都读取版本1,甲已写成版本2。乙如何提交旧结果更稳妥
两个执行者都读取版本1,甲已写成版本2。乙如何提交旧结果更稳妥?
对应知识点:L6-shared-state
简答题 · 解释机制
共享黑板中的事实记录应带哪些可核对字段?
对应知识点:L6-shared-state
代码题 · 核对给定数据
用字典更新保留amount字段,并加入policy字段。
对应知识点:L6-shared-state
场景题 · 分析具体变化
订单执行者和政策执行者写同一份状态时,怎样分清各自的职责?
对应知识点:L6-shared-state
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。