Python27_协程游戏理解
使用一个游戏来理解【协程】的执行过程
模拟代码
python
import asyncio
import time
# ==================== 战争游戏:协程调度演示 ====================
class WarClick:
"""记录战争开始后的虚拟时间"""
def __init__(self):
self.start = time.time()
def now(self):
"""返回第几天(1真实秒=1游戏天)"""
return int(time.time() - self.start)
def reportProcess(self, msg):
print(f"[第{self.now():2d}天] {msg}")
war_click = WarClick()
# ==================== 任务定义 ====================
async def attackCity(cityName, needDay):
"""主线任务:攻城"""
war_click.reportProcess(f"🏰 【主线】开始攻打 {cityName},预计{needDay}天")
# 模拟攻城过程,分阶段报告
for day in range(1, needDay + 1):
await asyncio.sleep(1) # 1秒=1天
attack_progress = int(day / needDay * 100)
if day < needDay:
war_click.reportProcess(f" ⏳ {cityName} 攻坚中... ({attack_progress}%)")
war_click.reportProcess(f"✅ 【主线】{cityName} 攻陷!获得战利品!")
return f"{cityName}控制权"
async def fetchAttackTsk(teamName, teamTsk, needDay):
"""支线任务:小分队行动"""
war_click.reportProcess(f"🗡️ 【支线】{teamName} 出发执行「{teamTsk}」,预计{needDay}天")
for day in range(1, needDay + 1):
await asyncio.sleep(1)
if day == needDay // 2:
war_click.reportProcess(f" 📡 {teamName} 传回中期报告:任务进行中...")
war_click.reportProcess(f"✅ 【支线】{teamName} 完成「{teamTsk}」!带回重要情报!")
return f"{teamTsk}情报"
async def smartGeneral():
"""主调度逻辑"""
print("=" * 60)
print("⚔️ 战争开始:协程调度演示")
print("=" * 60)
print("规则:1秒 = 1游戏天\n")
# ========== 第1阶段:攻A ==========
lootA = await attackCity("城池A", 3)
print()
# ========== 第2阶段:开启支线 + 继续主线 ==========
war_click.reportProcess("📜 【战略决策】将军下令:两路小分队同时出发!")
war_click.reportProcess("💡 【战术要点】将军不等待,主力立即转向城池B!")
print()
# 🔥 创建任务(派兵出发)但不等待!
task_children_a = asyncio.create_task(fetchAttackTsk("小分队a", "烧毁粮仓", 8))
task_children_b = asyncio.create_task(fetchAttackTsk("小分队b", "截断补给线", 4))
# 给事件循环一个机会启动任务
await asyncio.sleep(0)
print()
# ========== 第3阶段:攻B(与小分队并行)==========
lootB = await attackCity("城池B", 5)
print()
# ========== 第4阶段:攻C(继续并行)==========
war_click.reportProcess("📜 【战略决策】将军:'C城也要推,不等小分队了!'")
print()
lootC = await attackCity("城池C", 4)
print()
# ========== 第5阶段:收集团队结果 ==========
war_click.reportProcess("📬 【战后清点】检查各小分队进展...")
# 检查哪些已经完成
task_children_finish_list = []
task_children_running_list = [task_children_a, task_children_b]
for task_children in list(task_children_running_list):
if task_children.done():
task_children_finish_list.append(task_children)
task_children_running_list.remove(task_children)
war_click.reportProcess(f" ✅ 已完成:{task_children.result()}")
# 等待未完成的(如果有)
for task_children in task_children_running_list:
war_click.reportProcess(f" ⏳ 等待 {task_children.get_name() if hasattr(task_children, 'get_name') else '任务'} 完成...")
task_result = await task_children
war_click.reportProcess(f" ✅ 刚刚完成:{task_result}")
print()
print("=" * 60)
war_click.reportProcess("🏆 【战争结束】最终战果:")
print(f" 主线收获:{lootA}、{lootB}、{lootC}")
print(f" 支线收获:{task_children_b.result()}、{task_children_a.result()}")
print("=" * 60)
# ==================== 运行 ====================
if __name__ == "__main__":
# 设置事件循环策略(Windows兼容性)
if asyncio.get_event_loop_policy().__class__.__name__ == 'WindowsSelectorEventLoopPolicy':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
# 运行战争模拟
asyncio.run(smartGeneral())
- 执行结果打印
console
============================================================
⚔️ 战争开始:协程调度演示
============================================================
规则:1秒 = 1游戏天
[第 0天] 🏰 【主线】开始攻打 城池A,预计3天
[第 1天] ⏳ 城池A 攻坚中... (33%)
[第 2天] ⏳ 城池A 攻坚中... (66%)
[第 3天] ✅ 【主线】城池A 攻陷!获得战利品!
[第 3天] 📜 【战略决策】将军下令:两路小分队同时出发!
[第 3天] 💡 【战术要点】将军不等待,主力立即转向城池B!
[第 3天] 🗡️ 【支线】小分队a 出发执行「烧毁粮仓」,预计8天
[第 3天] 🗡️ 【支线】小分队b 出发执行「截断补给线」,预计4天
[第 3天] 🏰 【主线】开始攻打 城池B,预计5天
[第 4天] ⏳ 城池B 攻坚中... (20%)
[第 5天] 📡 小分队b 传回中期报告:任务进行中...
[第 5天] ⏳ 城池B 攻坚中... (40%)
[第 6天] ⏳ 城池B 攻坚中... (60%)
[第 7天] 📡 小分队a 传回中期报告:任务进行中...
[第 7天] ✅ 【支线】小分队b 完成「截断补给线」!带回重要情报!
[第 7天] ⏳ 城池B 攻坚中... (80%)
[第 8天] ✅ 【主线】城池B 攻陷!获得战利品!
[第 8天] 📜 【战略决策】将军:'C城也要推,不等小分队了!'
[第 8天] 🏰 【主线】开始攻打 城池C,预计4天
[第 9天] ⏳ 城池C 攻坚中... (25%)
[第10天] ⏳ 城池C 攻坚中... (50%)
[第11天] ✅ 【支线】小分队a 完成「烧毁粮仓」!带回重要情报!
[第11天] ⏳ 城池C 攻坚中... (75%)
[第12天] ✅ 【主线】城池C 攻陷!获得战利品!
[第12天] 📬 【战后清点】检查各小分队进展...
[第12天] ✅ 已完成:烧毁粮仓情报
[第12天] ✅ 已完成:截断补给线情报
============================================================
[第12天] 🏆 【战争结束】最终战果:
主线收获:城池A控制权、城池B控制权、城池C控制权
支线收获:截断补给线情报、烧毁粮仓情报
============================================================
这是一个绝佳的类比!让我用战争游戏的场景来讲透协程的工作原理。
战争设定
主军队(主线程/事件循环):你亲自率领的主力部队
城池A、B、C:主线任务节点
支线任务:偷袭敌方粮道
小分队a:烧毁粮仓(耗时30天)
小分队b:截断补给线(耗时7天)
同步方式(愚蠢的将军)
python
import time
def 攻打城池(name, days):
print(f"【主线】开始攻城: {name},预计{days}天")
time.sleep(days) # ← 傻等!主力全军发呆
print(f"【主线】{name}攻陷!")
return f"{name}战利品"
def 小分队任务(name, days):
print(f"【支线】{name}出发,耗时{days}天")
time.sleep(days) # ← 同样傻等,没人干活
print(f"【支线】{name}完成!")
return f"{name}情报"
# 愚蠢的调度:一件一件傻等
def 愚蠢将军():
攻打城池("城池A", 3)
# 派小分队(但将军傻等他们回来)
小分队任务("烧粮仓", 30) # 主力发呆30天!
小分队任务("截补给", 7) # 再发呆7天!
攻打城池("城池B", 5) # 37天后终于继续
攻打城池("城池C", 4)
# 总耗时:3+30+7+5+4 = 49天
问题:将军派了小分队,却站在原地等他们回来报告,主力完全停滞!
协程方式(聪明的将军)
python
import asyncio
async def 攻打城池(name, days):
print(f"【主线】🏰 开始攻城: {name},预计{days}天")
await asyncio.sleep(days) # ← 挂起!主力去干别的
print(f"【主线】✅ {name}攻陷!")
return f"{name}战利品"
async def 小分队任务(name, days, 任务类型):
print(f"【支线】🗡️ {name}出发({任务类型}),耗时{days}天")
await asyncio.sleep(days) # ← 挂起!小分队"离线"执行
print(f"【支线】✅ {name}完成!")
return f"{name}情报"
async def 聪明将军():
start = asyncio.get_event_loop().time()
# === 第1阶段:攻A ===
await 攻打城池("城池A", 3)
# 第3天:A攻陷
# === 第2阶段:开启支线,但绝不傻等! ===
print("\n📜 将军下令:两路小分队同时出发!")
# 创建任务 = 派兵出发(只是登记,不立即执行细节)
任务_烧粮 = asyncio.create_task(小分队任务("小分队a", 30, "烧粮仓"))
任务_截补 = asyncio.create_task(小分队任务("小分队b", 7, "截补给线"))
# 🔥 关键:将军不等待!立即继续主线
print("💡 将军:'你们去干,我继续推B城!'\n")
# === 第3阶段:攻B(与小分队并行)===
# 第3-8天:攻B的同时,小分队b在干活(第3-10天)
await 攻打城池("城池B", 5)
# 第8天:B攻陷,但小分队还没完
# === 第4阶段:攻C(继续并行)===
print("\n💡 将军:'C城也要推,不等他们了!'")
await 攻打城池("城池C", 4)
# 第12天:C攻陷
# === 第5阶段:检查支线 ===
print(f"\n⏰ 当前时间:第{asyncio.get_event_loop().time() - start:.0f}天")
print("📬 将军:'看看小分队进展...'")
# 小分队b应该完成了(第10天完成),a还在跑(第33天完成)
完成列表, 未完成列表 = await asyncio.wait(
[任务_烧粮, 任务_截补],
timeout=0 # 不等待,只看当前状态
)
for task in 完成列表:
print(f" ✅ 已完成:{task.result()}")
# 必须等小分队a(如果还没完)
if 任务_烧粮 in 未完成列表:
print(" ⏳ 小分队a还在烧粮,等他们...")
粮仓情报 = await 任务_烧粮 # 这里才真正等待
print(f" ✅ 终于等到:{粮仓情报}")
# 总耗时:max(3+5+4, 30) = 30天(而不是49天!)
asyncio.run(聪明将军())
执行时间线图解
天数: 0 3 8 10 12 30 33
|----|----|----|----|----|----|
主线: [攻A][----攻B----][-攻C-]
出发 | 派小分队a/b 等a完成
| ↓↓↓↓↓↓↓↓↓
支线a: [----------烧粮仓----------] 30天
支线b: [--截补给--] 7天
↑
第3天同时出发,互不阻塞!
实际耗时 = 30天(由最长的小分队a决定)
而不是 3+30+7+5+4 = 49天
核心机制对照
| 战争概念 | 协程概念 | 实际行为 |
|---|---|---|
| 将军发布命令 | create_task() |
登记任务,安排执行 |
| 将军继续推进 | await主线任务 |
挂起等待,但事件循环调度其他任务 |
| 小分队独立行动 | 协程在后台"挂起" | 不是真后台,是事件循环记得他们 |
| 战报送达 | await task |
恢复挂起点,获取返回值 |
| 将军统筹全局 | 事件循环(Event Loop) | 单线程调度所有协程 |
关键洞察
python
# 为什么小分队"不阻塞"却能在"单线程"跑?
async def 本质理解():
"""
协程的"并行"是幻觉:
第3天:将军派兵 → 写进日程表 → 继续走
第4天:将军攻B → 小分队b"理论上"在进行第2天工作
第5天:将军攻B → 小分队b"理论上"在进行第3天工作
...
实际上CPU只在处理将军的代码
但事件循环在"记账":小分队b应该完成了
当将军代码await时,事件循环检查:
"哦,b的7天到了,标记完成"
"""
pass
将军的智慧:
"我不需要亲自盯着小分队烧粮,我只需要记得我派了他们 ,然后相信时间到了他们会完成。在这期间,我的主力一刻不停!"
这就是 asyncio.create_task() + await 的组合艺术:启动不等待,需要时再收结果。