2. Ray远程函数与Actor基础
任务目标
本案例通过实现一个基于Ray Actor的计数器,掌握Ray的两类核心编程原语:远程函数(Ray Task)与Actor。你将看到远程函数如何并行执行无状态计算,以及Actor如何以有状态方式跨多次调用维护数据,并理解两者在Ray调度中的角色差异。
环境依赖
需要已安装 ray 与 pytest(仅用于验证)。安装命令:
pip install ray pytest
完整实现
以下代码定义了一个远程函数用于并行加法,以及一个计数器Actor。主程序依次演示:远程函数并行调用、Actor状态递增、以及通过Ray的 wait 获取任务完成状态。
import ray
import time
# 初始化Ray(若已存在则自动复用)
if not ray.is_initialized():
ray.init()
# ---------- 远程函数(Task) ----------
@ray.remote
def add(a, b):
time.sleep(0.1) # 模拟耗时计算
return a + b
# ---------- Actor(有状态服务) ----------
@ray.remote
class Counter:
def __init__(self, init_value=0):
self.value = init_value
def increment(self, step=1):
self.value += step
return self.value
def get_value(self):
return self.value
# ---------- 主程序:任务调度演示 ----------
if __name__ == "__main__":
# 1. 并行提交4个远程函数,不阻塞等待
futures = [add.remote(i, i * 2) for i in range(4)]
print("任务已提交,尚未获取结果")
# 2. 使用wait等待全部完成(演示调度状态查询)
done, not_done = ray.wait(futures, num_returns=4, timeout=5)
assert len(done) == 4, f"预期4个任务完成,实际{len(done)}"
results = ray.get(done)
print(f"远程函数结果: {sorted(results)}") # 预期 [0, 3, 6, 9]
# 3. 创建Counter Actor实例
counter = Counter.remote(10)
# 4. 串行调用increment三次(每次+2)
vals = []
for _ in range(3):
vals.append(ray.get(counter.increment.remote(2)))
print(f"Actor计数过程: {vals}") # 预期 [12, 14, 16]
# 5. 验证Actor状态保持
final_val = ray.get(counter.get_value.remote())
print(f"Actor最终值: {final_val}") # 预期 16
assert final_val == 16, "Actor状态未正确保持"
ray.shutdown()
print("验证通过")
运行与验证
直接运行脚本:
python counter_demo.py
预期输出(严格由代码逻辑推出):
任务已提交,尚未获取结果
远程函数结果: [0, 3, 6, 9]
Actor计数过程: [12, 14, 16]
Actor最终值: 16
验证通过
判定通过条件:程序无异常退出,且最后一行打印 验证通过。若在本地多核环境运行,可通过 ray.get_runtime_context().get_task_id() 观察远程函数实际分布在不同worker上(本案例未展开,仅说明调度机制)。
核心机制说明
- 远程函数:每次调用
add.remote()都会创建一个独立任务,由Ray调度到集群中的可用worker执行。任务之间无共享状态,适合纯函数式并行。 - Actor:
Counter.remote(10)在集群中创建一个有状态对象,后续所有increment.remote()调用都路由到该Actor实例,按提交顺序串行执行,从而保证计数一致性。 - 调度差异:远程函数每次调用可能被调度到不同worker;Actor的方法调用则固定由持有该Actor的worker处理,这是两者在状态管理上的根本区别。
常见问题
- 端口冲突:若多次运行未正常shutdown,可先执行
ray.shutdown()或重启终端。 - 结果顺序:远程函数返回顺序与提交顺序无关,示例中对结果排序仅为便于断言。
- Windows环境:需将主程序放入
if __name__ == "__main__"保护块,否则多进程重新导入时会递归创建任务。