仿真 server 默认为异步模式,它会尽可能快地进行仿真,根本不管客户是否跟上了它的步伐。接下来我会更详细地介绍这个原因,并提出解决方案。

# 仿真步长

simulation 里的时间与真实世界是不同的, simulation 里没有 “一秒” 的概念,只有 “一个 time-step " 的概念。这个 time-step 相当于仿真世界进行了一次更新(比如小车们又往前挪了一小步,天气变阴了一丢丢),它在真实世界里的时间可能只有几毫秒。

仿真世界里的这个 time-step 其实有两种,一种是 cvariable time-step , 另一种是 fixed time-step .

  • cvariable time-step . 顾名思义,仿真每次步长所需要的真实时间是不一定的,可能这一步用了 3ms, 下一步用了 5ms, 但是它会竭尽所能地快速运行。这是仿真默认的模式:
settings = world.get_settings()
settings.fixed_delta_seconds = None # Set a `cvariable time-step`
world.apply_settings(settings)
  • fixed time-step . 在这种时间步长设置下,每次 time-step 所消耗的时间是固定的,比如永远是 5ms. 设置代码如下:
settings = world.get_settings()
settings.fixed_delta_seconds = 0.05 #20 fps, 5ms
world.apply_settings(settings)

# 同步模式

看到这里,我相信各位小伙伴们已经猜到了, carla simulation 默认模式为异步模式 + cvariable time-step , 而同步模式则对应 fixed time-step .

在异步模式下, server 会自个跑自个的, client 需要跟随它的脚步,如果 client 过慢,可能导致 server 跑了三次, client 才跑完一次,这就是为什么咱们照相机储存的照片会掉帧的原因。

而在同步模式下, simulation 会等待客户完成手头的工作后,再进行下一次更新。假设 simulation 每次更新只需要固定的 5ms, 但我们客户端储存照片需要 10ms, 那么 simulation 就会等照片储存完才进行下一次更新,也就是说,一个真正 cycle 耗时 10ms ( simulation 更新与照片储存是同时开始进行的)。设置代码关键部分如下:

def sensor_callback(sensor_data, sensor_queue, sensor_name):
    if 'lidar' in sensor_name:
        sensor_data.save_to_disk(os.path.join('../outputs/output_synchronized', '%06d.ply' % sensor_data.frame))
    if 'camera' in sensor_name:
        sensor_data.save_to_disk(os.path.join('../outputs/output_synchronized', '%06d.png' % sensor_data.frame))
    sensor_queue.put((sensor_data.frame, sensor_name))
    
    settings = world.get_settings()
    settings.synchronous_mode = True
    world.apply_settings(settings)
    
    camera = world.spawn_actor(blueprint, transform)
    sensor_queue = queue.Queue()
    camera.listen(lambda image: sensor_callback(image, sensor_queue, "camera"))
    
    while True:
        world.tick()
        data = sensor_queue.get(block=True)

这段代码首先注意到的是 world.tick() 这个函数。它只出现于同步模式,意思是让 simulation 更新一次。然后我们还会发现这里用了 python 自带的 Queue , queue.get 有一个功效,就是在它把列队里所有内容都提取出来之前,会阻止任何其他进程越过自己这一步,相当于一个 blocker 。如果没有这个 queue ,你会发现仿真虽然设置成了同步模式,还是照样会自个跑自个的。

所以你可以这样理解, settings.synchronous_mode = True 让仿真的更新要通过这个 client 来唤醒,但这并不能保证它会等该 client 其他进程运行完,必须要再加一个 queue 来阻挡一下它,逼迫它等着该客户其他线程搞定。也就是说,启动同步模式,让你的 server 学会等待客户的必要条件有三个:

settings.synchronous_mode = True
world.tick()
Thread Blocker(such as Queue)

当你将上述代码加到我们上一次的程序里,会发现,照片是不掉帧了,但是小车一动也不动了。这里是因为同步模式下汽车要使用 autopilot 必须依附于开启同步模式的 traffic manager . 至于这个 traffic manager 是何方神圣,咱们下期会详细讲述,现在你只需要如何操作:

traffic_manager = client.get_trafficmanager(8000)
traffic_manager.set_synchronous_mode(True)
ego_vehicle = world.spawn_actor(ego_vehicle_bp, transform)
ego_vehicle.set_autopilot(True, 8000)

# 注意事项

  1. 目前 carla 只支持单客户同步,也就是说,如果你有 Npython scripts , 只能在其中一个 client 里设置同步模式,而其他 client 只能异步模式。这些处于异步模式的客户首先通过 world.wait_for_tick() 等待 server 更新,一旦更新了它们会立刻通过 world.on_tick 里的 callback 来提取这个更新的 wordsnapshot 里面的信息(比如 timestamp , 这个 on_tick() 我会在以后的分享里详细说明举例,现在暂且用不到).
# Wait for the next tick and retrieve the snapshot of the tick.
world_snapshot = world.wait_for_tick()
# Register a callback to get called every time we receive a new snapshot.
world.on_tick(callback)
  1. 在你设置了同步模式的 client 完成了它的任务准备停止 / 销毁时,千万别忘了将世界设置回异步模式,否则 server 会因为找不到它的同步客户而卡死。
try:
.......
finally:
    settings = world.get_settings()
    settings.synchronous_mode = False
    settings.fixed_delta_seconds = None
    world.apply_settings(settings)

# 总结

同步 / 异步模式因为涉及到多线程的问题,设置和使用的时候要格外小心,一般设置同步模式的客户主要是用来做数据储存与采集的。在下一期里,我将会讲到如何通过神秘的 Traffic Manager , 让街道充满各种不同行为模式的汽车,为你的无人汽车模拟真实的交通环境。