启用多线程时可能出现的竞争条件



假设我有一个云平衡问题的轻微变体,其中Process不仅有一个权重,而且有一个(正)权重的映射,例如

Map<Long, Long> groupMap = new HashMap<>();

,其中键是特定于我的域,值是权重。

在类Computer上(仍然参考云平衡示例),我有一个影子变量hist,这也是一个(Hash)Map<Long, Long>,和一个自定义侦听器更新hist:

public class HistListener implements VariableListener {
@Override
public void beforeVariableChanged(ScoreDirector scoreDirector, Object o) {
Process p = (Process) o;
if (p.getComputer() != null) { 
Computer kc = p.getComputer();
//update hist Map
scoreDirector.beforeVariableChanged(kc, "hist");
for (Map.Entry<Long, Long> entrySet:k.getGroupMap().entrySet()){
kc.getHist().put(entrySet.getKey(), kc.getHist().get(entrySet.getKey()) - k.getGroupMap().get(entrySet.getKey()));
}
scoreDirector.afterVariableChanged(kc, "hist");
}
}

afterVariableChanged差不多,只是符号反了。

我将ProcessComputer注释为@PlanningEntity,并将它们注册在solverConfig中。

没有约束,所以求解器应该能够任意地将computers分配给processe。因此,我希望hist只具有自然数(包括0)作为值。

当它与<moveThreadCount>NONE</moveThreadCount>一起运行时,确实是这样:

<"Computer"+computer.id: hist>
Computer0: {0=0, 1=0, 2=20, 3=0, 4=10, 5=20, 6=0, 7=10, 8=10, 9=20}
Computer1: {0=0, 1=10, 2=0, 3=0, 4=10, 5=0, 6=10, 7=0, 8=0, 9=0}
Computer2: {0=0, 1=0, 2=0, 3=0, 4=0, 5=0, 6=0, 7=0, 8=0, 9=0}

完全运行<moveThreadCount>AUTO</moveThreadCount>相同的代码,我在hist中部分得到负值:

Computer0: {0=0, 1=-20, 2=30, 3=0, 4=-40, 5=50, 6=-10, 7=30, 8=40, 9=150}
Computer1: {0=0, 1=-40, 2=-20, 3=0, 4=-90, 5=-50, 6=-40, 7=-20, 8=-20, 9=-30}
Computer2: {0=0, 1=80, 2=-20, 3=0, 4=30, 5=-30, 6=50, 7=0, 8=-20, 9=-50} 

当我在process上重构groupMap的键和在computer上重构hist的键作为单独的影子变量时,这种差异就消失了。

trace日志提示存在竞争条件,即多个线程同时访问hist。(根据Oracle文档,我只需要一个synchronizedMap实现,如果映射是结构改变,即,如果键被添加或删除-我不这样做。)

使用Map作为影子变量极大地增强了我的解决方案的灵活性,如果多线程支持这一点就太好了。我知道我可能会修复这个非常简单的例子与适当的ConstraintProvider。我的实际问题比这复杂得多,不能用ConstraintProviders处理。

问题:是否有可能在多线程上下文中有一个基于Map的结构作为影子变量?

如果不可能,我建议在optaplanner 8.29.0的文档中添加一个简短的说明。最终版本(我正在使用的版本)。

我看了一下optaplanner中关于Lists作为PlanningVariables的问题,但我不知道这些问题与我的问题有什么关系。

是否有可能在多线程上下文中有一个基于Map的结构作为影子变量?

是的,因为多线程环境中的每个move线程内部都有自己的ScoreDirectorworkingSolution。从影子变量和映射的角度来看,它是单线程的。

什么会把这搞砸呢?

  • 在你的数据集中坏的@PlanningId,所以Move.rebase()操作出错。id重复或缺少id。OptaPlanner可以检测其中的大部分。这不是你的问题。
  • 模型中的克隆计划不完整。可能就是这样。这还会导致在单线程上下文中尚未看到的问题,特别是当终止结束时,最后一个有效的解决方案与最后一个最佳解决方案存在很大差异时。FULL_ASSERT应该检测到这些,但它们可能不会在每次运行时都发生…

每个move线程内部都有自己的workingSolution这并不完全正确。他们都有原版的计划克隆版。但是如果计划克隆没有克隆所有受阴影变量影响的数据,那么它就被破坏了。在多线程解决上下文中,这会更快地导致问题。

好了,这变得越来越复杂了。怎么解呢?

尝试在Map字段上添加@DeepPlanningClone注释。但是,创建一个影子变量已经意味着要进行深入的计划,以IIRC自动克隆它。我猜映射中的键或值也需要进行规划克隆。请阅读文档中的计划克隆部分。

最新更新