OO Unit2 Elevator Scheduling

OO Unit2 Elevator Scheduling

本单元三次作业架构相似,整体采用生产者消费者模型,这种设计模式服务于本次作业三个主要特点:支持并发、输入输出解耦合、线程安全易保证。

  • 支持并发:多线程的优势在于并发提升运算效率,当涉及到多任务处理时,通常可以将主要部分使用多个线程,配以互斥锁/信号量/条件变量来实现同步块/原语/临界区来解决竞态条件、线程间通信等问题。

    具体到本次场景中,可以并行的逻辑主要有两部分:输入并生产需求、完成需求并输出。需求的完成过程作为一个整体,不存在较明显的可并行逻辑(需求处理模块间明显可并行则应采用流水线模型),适合采用基本生产者消费者模型

  • 输入输出解耦合:输入输出位于两个线程优势还在于解耦合,在该抽象层次上保证了单一职责

  • 线程安全易保证:在该模型下主体逻辑中,中间托盘类作为唯一的共享资源,是唯一需要保证线程安全的部分

hw5

层次化设计

结构较简单,仅展示电梯时序图

 

主要工作量在于电梯内部职能的实现。电梯的行动是归为四状态有限状态机,状态转移由电梯自己的dispatcher确定,电梯本身只负责执行,以此实现机制与策略分离。

策略与机制
  • 策略:look,优先接最远

  • 机制:量子电梯

    优势主要在两方面:

    • 充分晚决策、充分扩大了接受可受理请求的孔径时间(而没有消耗额外的移动时间)使可受理请求尽量不会被落下

    • 移动时间被充分缩短,实现瞬移

    前者是完全正向优化,通过在移动过程中可以原地开关门接人实现;后者是与数据相关的不完全优化(随便起的名字),通过halt后第一次移动瞬移实现。

这部分线程安全问题,只需要在托盘类上都加锁即可,关于量子电梯的实现流程大致如下:

synchronized (pool) {
  while (System.nanoTime() - startT < 200000000) {
    pool.wait(200 - (System.nanoTime() - startT) / 1000000);
    chosenGot = pool.get(block).size() > 0;
    if (chosenGot) { break; }
  }
}

(经小伙伴指出,不用nanotime可能有极端情况下ms进位问题导致精度无法保证

对于halt后瞬移,我开始持怀疑态度。在充分压缩移动时间的同时,也充分压缩了可受理请求的孔径时间,容易落下人。

后来用hw5部分强测数据做了测试:

 无halt后瞬移halt后瞬移
1 76.9290 76.9500
2 78.1930 77.7820
3 77.3790 76.9960
4 76.9870 75.8280
5 68.5880 68.1640
7 71.7580 71.2010
10 54.3930 54.0150
17 96.2860 96.1190
20 106.7980 105.7200

发现halt后瞬移对大部分场景是正向优化。这告诉我们,等一个不一定等得到的人不如早点离开去追求下一个(

具体实现逻辑请看zsm学长博客:https://www.cnblogs.com/kasuganokusa/p/14701724.html

量化指标

强测得分:99.9993,互测被捅11刀

清明节玩high了,没看讨论区不知道输出时间戳需要递增。看到强测没挂点,我的心里只有感恩。

hw6

层次化设计

横向电梯时序图:

本次新增了横向电梯。主要业务和纵向电梯很相似,可惜由于checkstyle把protected ban了,通过继承实现代码复用还需要大量的getter,属实蛋疼,遂不继承。

策略与机制

对于横向电梯设计如下:

  • 策略:电梯为空则停摆,根据两侧请求数量确定初始移动方向,根据接到第一组人再次确定移动方向(也是不完全优化,若不做则类似接B-A需要转一圈,蛋疼),移动起来且电梯内有人则不转向,所有请求都受理(考虑到环形,转向使前功尽弃,应尽量避免)

  • 机制:加上halt后瞬移的量子电梯

量化指标

强测得分:99.5655,互测被捅1刀

虽然没有写评测机,但自己还是大概测了测的。最后只被一个哥发现了问题,我的心里只有感恩。

问题出在电梯原地空接后量子电梯用到的startT没有重置导致移动过快。

hw7

层次化设计

 

Controller时序图:

纵向电梯时序图:

新增换乘。可以看成边权为移动时间,点权为等待时间的无向图然后跑最短路径。不加额外限制的最短图允许多次换乘,与请求横向分量一次消除的利弊如下:

  • 横向一次完成最坏场景是到1层换乘,等待时间最多是一层横向等待时间(业务不繁忙时移动时间消耗占主导)

  • 允许多次换乘则面临多于一次的纵向等待时间、开关门次数带来额外的时间消耗

需要trade off,考虑到纵向平均等待时间>>横向平均等待时间,采用最少换乘次数,横向一次性消除请求的横向分量的策略

策略与机制

策略上,待定参数只有换乘楼层,关键在于换乘楼层的选择。

官方给出的换乘楼层确定的方式较为简单,通过移动距离静态估价,作为粒度为1的估价方案给出。我将每block(floor)电梯个数、等待时间电梯移动速度几个因素纳入考量,基于概率设计cost函数

private int strategy2(int ff, int tf, char fb, char tb) {
  // 根据电梯运行速度与电梯个数,静态确定换乘层
  // 2-9层上行下行2种状态,1、10层仅一种状态,若是scan在18种状态间循环遍历
  // 引入扫力,定义为每秒状态步进数(不考虑步进后状态之间的碰撞),等待状态数按期望计算,竖直期望等待9状态,水平期望等待2状态(单向转)
  // 运行时间与被接概率均与运行速度相关,认为与扫力呈正相关(没有进行开关门修正)
  int res = 0;
  double minCost = Double.MAX_VALUE;
  int[] v = new int[3];        // v[0]200ms  v[1]400ms  v[2]600ms
  for (int i = 0; i < 10; i++) {
    v[0] = 0;
    v[1] = 0;
    v[2] = 0;
    for (FloorElev e : floorElevs.get(i)) {
      if (((e.getPark() >> (fb - 'A')) & 1) + ((e.getPark() >> (tb - 'A')) & 1) == 2) {
        v[(int) e.getDuration() / 200 - 1]++;
      }
    }
    if (v[0] + v[1] + v[2] == 0) { continue; }
    double cost = 0;
    // 横向等待
    cost += (double) 2400 / (6 * v[0] + 3 * v[1] + 2 * v[2]);
    // 横向运行
    cost += (double) 3000 * (6 * v[0] + 3 * v[1] + 2 * v[2]) /
      (36 * v[0] + 9 * v[1] + 4 * v[2]);
    if (i + 1 != ff) {
      v[0] = blockSpeed[fb - 'A'][0];
      v[1] = blockSpeed[fb - 'A'][1];
      v[2] = blockSpeed[fb - 'A'][2];
      // 纵向等待
      cost += (double) 10800 / (6 * v[0] + 3 * v[1] + 2 * v[2]);
      // 纵向运行
      cost += (double) 1200 * (i + 1 - ff) * (6 * v[0] + 3 * v[1] + 2 * v[2]) /
        (36 * v[0] + 9 * v[1] + 4 * v[2]);
    }
    if (i + 1 != tf) {
      v[0] = blockSpeed[tb - 'A'][0];
      v[1] = blockSpeed[tb - 'A'][1];
      v[2] = blockSpeed[tb - 'A'][2];
      // 纵向等待
      cost += (double) 10800 / (6 * v[0] + 3 * v[1] + 2 * v[2]);
      // 纵向运行
      cost += (double) 1200 * (tf - (i + 1)) * (6 * v[0] + 3 * v[1] + 2 * v[2]) /
        (36 * v[0] + 9 * v[1] + 4 * v[2]);
    }
    if (cost < minCost) {
      minCost = cost;
      res = i + 1;
    }
  }
  return res;
}

需要指出,静态估价得出cost结果都是相当粗糙的。由于通过SCAN策略基于概率计算,look实际场景和SCAN相去甚远,等待时间估计很不精确。同时,被哪个电梯接上是可以根据电梯实时状态动态刻画的,此方案信息挖掘也不充分,同时最后的效果是有一个测试点较低,都说明该方案有较大提升空间。

较理想的估价方式应当是动态获取电梯楼层信息、开关门信息(满载看作一次Exception即可,影响不大),这些都是仅与电梯内乘客信息相关的可刻画参数。

机制上,我采用信号量的方式进行线程间通信,发送终止信号,大致过程如下:

//Elev
public void run(){
  if(reqFinish) Controller.accomplish();
}
​
//Controller
private final Semaphore reqLog;
public synchronized void accomplish() { reqLog.release(); }
public void run() {
  while (true) {
    if (req == null) {
      try { reqLog.acquire(reqCnt); }
      catch (InterruptedException e) { e.printStackTrace(); }
      for (int i = 0; i < 5; ++i) { blockPools.get(i).stop(); }
      for (int i = 0; i < 10; ++i) { floorPools.get(i).stop(); }
      break;
    }
  }
}

每次完成一份请求V一单位Sem,在req ==null时P reqCnt单位的Sem,发送结束信号前对所有Req进行验收。

量化指标

强测得分:99.3895,互测被捅4刀

测试力度不够,因为没有密集请求更易出现冲突的notion。还是线程安全问题,考虑不周啦!TnT

心得体会

线程安全
  • 临界区仅需要设置在修改共享资源的部分,计算过程应当在临界区外

  • 通过信号量可以起到查收、限流等操作,是线程通信的粒度最小的不二之选

层次化设计
  • 自由竞争yyds,量子电梯yyds,但细节没弄好就是在当赌徒了(

  • 职权应当充分独立

  • 设计模式的使用不要生搬硬套,以这次为例,生产者消费者模型有助于解耦合,单例模式、工厂模式却会徒增复杂度,工程性不强的项目还是应当具体分析(暴论

posted @ 2022-04-30 20:02  Lumyn  阅读(159)  评论(3编辑  收藏  举报