仿真 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
, 让街道充满各种不同行为模式的汽车,为你的无人汽车模拟真实的交通环境。