仿真 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) |
# 注意事项
- 目前
carla只支持单客户同步,也就是说,如果你有N个python 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) |
- 在你设置了同步模式的
client完成了它的任务准备停止 / 销毁时,千万别忘了将世界设置回异步模式,否则server会因为找不到它的同步客户而卡死。
try: | |
....... | |
finally: | |
settings = world.get_settings() | |
settings.synchronous_mode = False | |
settings.fixed_delta_seconds = None | |
world.apply_settings(settings) |
# 总结
同步 / 异步模式因为涉及到多线程的问题,设置和使用的时候要格外小心,一般设置同步模式的客户主要是用来做数据储存与采集的。在下一期里,我将会讲到如何通过神秘的 Traffic Manager , 让街道充满各种不同行为模式的汽车,为你的无人汽车模拟真实的交通环境。