OO第二单元博客作业

林子淇-23371263 2025-04-14 22:06:43

概念理解与辨析

进入电梯这个单元,我们一下子多了许多概念,需要完整的理解才能够进行程序设计,否则容易导致架构设计混乱。

策略

LOOK、ALS、自由竞争、影子电梯……学长博客里的这些术语究竟讲的是不是同一种东西?

本质上来说它们都是电梯策略,但是在方向上进行细分,却大致分为两种:

  1. 电梯调度策略:也可以叫做群控策略,是指系统存在多台电梯时,如何将请求分配给某一电梯。在 hw_5 中暂不涉及,因为每一次请求都指定了某一电梯。
  2. 电梯运行策略:是指对于单个电梯,面临不同楼层多个请求时,如何规划运行线路。这是 hw_5 最占用时间的部分,也是这个作业存疑之处

所以,LOOK、ALS 这种可以叫做电梯运行策略,自由竞争、影子电梯这些可以叫做电梯调度策略

自由竞争:所有电梯都能接收到新请求,先接到用户先得。
影子电梯:分别将当前请求分配给每一个电梯,分别模拟(不睡觉)运行直到所有电梯运行完毕,将请求分配给当前最佳的电梯。

在线/离线

  1. 在线算法是指程序在接受输入的时候必须立即做出决策,不能等待未来的输入。程序在任何时刻都只能得知到目前为止的输入,不能获知未来将至的输入。
  2. 离线算法是指程序在开始处理之前就可以获取完整的输入数据,可以一次性看到所有的输入,进行全局最优决策。

课程组的各种限制(时间戳加密等)决定了我们必须使用在线算法而不是离线算法来编写程序。

但是离线算法并非毫无作用,对于一组数据,一定存在一个最佳规划,使得它的性能最好。离线算法可以被用来找到这一组最佳规划,来评估我们在线算法的性能。

然而对于这次作业,真的存在最优的离线算法吗?我不能确定,并对此存疑。

设计模式

  • 问:生产者-消费者模式,与我们以前学习到的工厂模式、单例模式等,是否是同一个东西?
  • 答:它们都是设计模式,但前者是专用于多线程设计中,后面的部分则是面向对象设计通用的模式。

在电梯单元中,我们主要提及了生产者-消费者模式。这种设计模式脱胎于 Guard Suspension 模式,从多线程设计的角度上来说,本质上两者没有什么区别。

生产者-消费者模式是在 Guard Suspension 这种多线程设计模式的基础上,再加入将流程抽象为“生产”与“消费”两部分这种面向对象的思想形成的。

两者处理多线程协作的方法就是,设计一个请求队列,将作为提供者与接收者的桥梁。提供者通过 put() 方法提供请求,接收者通过 take() 方法获得请求。

Guard Suspension 的精髓就是当 put/take 方法不可用时,让执行处理的线程进行等待

线程是什么

钟鼓楼的博客说:“线程是一个过程,而不是一个对象。”

我大致能够理解他想表达的意思,但这句话说的不完全正确。Java 将 Thread 设计成了一个类,又将 Runnable 设计为一个接口,表明线程就是一个对象。

学长真正想表达的意思是,在设计时我们需要将执行流程(线程)数据模型(资源)区分开来,实现关注点分离

线程应该被视为控制对象的执行流程,而非对象本身的一种形态。面向对象的思想让我们在代码上将线程视为类,但我们在审视代码执行过程的时候,线程这一对象不同于以往,它不是流程中某一实际的对象,它就是流程本身

线程协作

当我们在 notify 的时候我们在 notify 什么?

我们在编码的过程中使用 notify/wait 和 synchronized 来协调多个线程共享统一资源的过程。经典的代码如下:

class Buffer {
    private List<Data> dataList;
    public synchronized void put(Data data) {
        while (dataList.size() == capacity) {
            this.wait();  // who's waiting?
        }
        dataList.add(data);
        this.notify();    // who's notifying?
    }
}

看着这种代码,我们需要思考三个(哲学)问题:

  1. 谁在通知/等待?
  2. 在哪里(声明)通知/等待?
  3. 通知/等待谁?

这其中第 2 个问题看这段代码就能够知道:通知/等待的动作是在资源中声明的。

初看这段代码,看到 this.wait() 我们很容易误以为是 this 所指的对象,也就是资源在等待。然而事实上我们需要从执行流程的角度去看待这一件事。

这一 put() 方法一定是在某一个线程的 run() 方法中被调用的。

class Producer extends Thread {
    private final Buffer buffer;
    
    public Producer(Buffer buffer) {
        this.buffer = buffer;
    }
    
    @Override
    public void run() {
        try {
            for (int i = 1; i <= 10; i++) {
                buffer.put(i); // <-- call put
                Thread.sleep(100);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

所以事实上,this.wait() 是指“调用该方法的线程在该资源上等待”。

对象只是线程之间协作的媒介,真正等待、执行和通知的都是线程

多线程设计流程

基于以上观点,我们在进行多线程设计时,应该:

  1. 先明确独立的执行流程有哪些。
  2. 确定执行流程之间有哪些需要共享的资源。
  3. 设计执行流程之间的协作关系。

hw_5 架构设计

多线程流程设计

hw_5

时序图上是看不出锁机制的存在的,但我们可以很清楚的看到一个线程唤醒另一个线程的机制。

在被两个线程共享的资源的时间线上,为什么能够看到两种色块交替出现?

就是因为前一线程执行完毕之后,改变了这一资源的状态,于是选择通知与其共享这一资源的其他线程。

调度器线程将请求调度给理想的电梯线程。所以实际上,与 DeliverThread 交互的 WaitingTable 对象不止于一个。不过不同的电梯之间并没有什么影响,所以实际上这并不会对线程安全造成什么影响。

事实上,调度器将请求分配给电梯,使得每一电梯只能看到楼层内属于自己的请求。也就是说,每一电梯都认为自己是这栋楼内唯一的一座电梯,这是某种层次上的抽象与封装

策略模式

这其中,电梯的调度与运行策略是如何加装进去的?这就要用到面向对象设计中的策略模式。

public final class DeliverThread extends Thread {
    private final DeliverPolicy policy = new CommonDeliverPolicy();
    
    @override
    public void run() {
        try {
            ...
            int elevatorId = policy.determine(...context...);
            ...
        } catch (Exception e) {
            e.printStackTrace(System.err);
        }
    }
}

public interface DeliverPolicy {
    int determine(Task task, HashMap<Integer, Elevator> elevators);
}

public final class CommonDeliverPolicy implements DeliverPolicy {
    @Override
    public int determine(Task task, HashMap<Integer, Elevator> elevators) {
        return task.getElevatorId();
    }
}

策略模式要求我们设计一个接口以及其中的方法,在需要使用策略的上下文对象中就可以创建一个策略对象,将需要的上下文数据传入给它,使用其方法执行该策略。

这里我将策略对象设置为 final,但其实完全可以取消 final 这一特性,并设置一个 setPolicypublic 方法,在运行过程中动态设置该对象的策略。这种动态性是由接口保证的,即接口中声明了方法,则实现该接口的类中重写的这一方法的签名一定是完全相同的。

状态机

在设计电梯运行策略的过程中,我决定使用状态机的方法来设计电梯。在画了一阵子之后,我最终得到下面这样的状态转移图:

hw_5_state

与我一开始设想的不同的是,电梯运行策略决策的结果不是电梯的状态,而是电梯的行为。

换句话说,电梯运行策略进行决策的依据是电梯的状态,包括但不限于图上的圆圈;决策的结果是电梯的行为,是指图上状态转移过程中的动作。

这里和钟鼓楼的博客里讲的有所不同。他认为,电梯是没有等待行为的,电梯线程才有等待行为。

他说的很对,然而按照上面的策略模式的例子来看,我们终究是将状态机放在电梯线程下运行。电梯运行策略决策的结果,也就是电梯的行为,最终的执行者也是电梯线程。所以这里我觉得使用 WAIT 是没有关系的。事实上编码过程中有所曲折,所幸最终的代码还是比较可观的。

public class ElevatorThread extends Thread {
    private final RunningPolicy policy;

    @override
    public void run() {
        try {
            ...
            Behavior eb = policy.determine();
            eb.action();
            ...
        } catch (Exception e) {
            e.printStackTrace(System.err);
        }
    }
}

public interface Behavior {
    void action();
}

public abstract class ElevatorBehavior implements Behavior {
    protected final Elevator elevator;

    @Override
    public abstract void action();
}

public class ElevatorWait extends ElevatorBehavior {
    @Override
    public void action() {
        WaitingTable waitingTable = elevator.getWaitingTable();
        synchronized (waitingTable) { // current Thread wait in context of waitingTable
            try {
                waitingTable.wait();
            } catch (Exception e) {
                e.printStackTrace(System.err);
            }
        }
    }
}

hw_6 迭代

第六次作业和第五次作业相比,架构上没有发生变化。对于新增的临时调度请求,我将临时调度设计为电梯的一个状态,新增了一个对应的行为类,修改电梯运行策略。于是状态图变为如下:

img

此外,如何正确结束调度线程是我遇到的新问题。在前一次作业中,调度器线程只需要等待输入线程结束后退出即可;而这次作业,由于电梯线程需要将已经加入电梯(轿厢或候乘表)的乘客请求放回至主请求队列中,我们需要考虑什么时候结束调度线程。

最后,我设计了让调度线程等待所有电梯线程进入等待状态。此时检查所有电梯是否为空,且输入线程已经结束,调度线程就可以结束了。随后就是电梯线程的结束,与调度线程相同。

hw_6 这周我没有太多时间编写代码,最后交上了事。hw_7 开启这周,我决心重构 hw_6,这也是我悲剧的开始。

hw_6 重构

hw_7 开启这周,我首先复盘了我第六次作业代码。我发现仍有许多地方设计的不够好:

  1. 各个类的调用都非常混乱,参数传入传出,很不好看
  2. 没有认真设计调度策略
  3. 量子电梯没有设计好
  4. SCHE有待优化

于是我开始着手于这些重构。

单例模式

为了解决参数冗余问题,我使用单例模式改写了原来的类,将唯一的主请求队列、调度器线程改为了饿汉单例模式,给电梯线程和电梯相关资源(轿厢、候乘表)设计了单例管理类。我还将代码中各处的锁集中起来,设计了一个锁的管理类:默认每一个对象持有一个锁及其条件,使用时将该对象作为参数传入,在 HashTable 里查找对应的锁即可。

这样的写法非常优美,解决了绝大部分参数冗余的问题。

量子电梯优化

起初,我对于量子电梯的认识仅限于“比 400ms 睡少一点”,即记录电梯前一状态,由于代码执行需要耗时间,睡眠时只需要睡到前一状态的时间 +400ms 的时间即可。

现在我发现,原来量子电梯相关优化不止这一点,这还可以包括弹射起步、开门等待等等。在原先的设计中,我的开门行为是先出乘客、马上进乘客,然后睡眠 400ms;然而这显然不是最优的,其实我们可以先睡眠 400ms,再进乘客,这样又可以顺带处理 400ms 中新加入的乘客。

至于开门等待特性,我们可以设计当电梯在等待时,不是无条件休眠 400ms,而是在候乘表上有条件等待 400ms。当电梯被唤醒时,检查是否为当前楼层有新乘客加入,如有则再次开门接客。

以上都是我在前几次作业没有注意到的细节,实际上可以给我的代码带来一些优化。

SCHE 处理

第六次作业提交时我并没有让处于 SCHEDULED 状态的电梯参与调度,这也导致了我会被如下的测试数据卡死:

[1.0]SCHE-1-0.2-F1
[49.9]SCHE-5-0.2-F5
[49.9]SCHE-3-0.2-F5
[49.9]SCHE-2-0.2-F5
[49.9]SCHE-4-0.2-F5
[49.9]SCHE-6-0.2-F5
[49.9]1-PRI-100-FROM-B4-TO-F7
[49.9]2-PRI-99-FROM-B4-TO-F7
[49.9]3-PRI-98-FROM-B4-TO-F7
[49.9]4-PRI-97-FROM-B4-TO-F7
.....

这一测试数据使得五部电梯同时处于调度状态,而在这期间加入的所有请求会全部调度给剩下的一部电梯,导致运行时间非常长。(当然由于放宽了时间限制,实际上并不会TLE。)

实际上,SCHEDULED 状态中的电梯也是可以参与调度的。我们只需要为每个电梯设计一个 buffer,当该电梯处于临时调度状态中时将任务加入到 buffer 中,等到临时调度结束刷新一下 buffer 即可。刷新 buffer 时再输出 RECEIVE 相关请求。

新的调度策略

我重新设计了一个调度策略,在一个时间窗口内收集一批乘客请求,将这批乘客请求一起发送给调度策略进行自由竞争模拟决策。我称之为影子自由竞争,因为影子电梯需要模拟加入一个任务直到所有电梯运行结束,而影子自由竞争也是模拟,需要模拟加入多个任务直到每个任务被所有电梯接到为止。这个策略理论上可以规避先前自由竞争策略耗电量高的问题。然而实际测试下来,该策略的性能分还是较为随机,对于同一个测试点,仍有可能出现20分以上的耗电量差。不过也不失为一个尝试的方法。

我花了一个晚上实现了这个策略,又花了三天的时间来调试它。最后我成功完成了它,但也来不及完成第七次作业了……

JUnit、投喂包和评测相关

JUnit

OO 第二单元电梯,为了方便我们进行输入输出,课程组提供了 dataInput 程序对我们编译好的 jar 包进行管道输入,并提供一个 elevatorX.jar 作为我们代码的依赖。然而在使用过程中有诸多不便:

  1. 每次测试程序,都需要打包一遍,再从 out/ 文件夹下拖出来
  2. Windows 下 powershell 还运行不了(奇奇怪怪,是为什么呢?)

即便好心的睿睿准备了一个更好用的数据投喂机,但它需要作为 main 方法来使用,而且还不能上传到公测提交中(当然会查重)。

这时候,我们可以使用曾经在 oop 中接触到的 JUnit 方法,将 testMain/dataInput 等定时向我们的程序提供输入的方法编写为一个测试单元,运用测试单元就可以不用担心两个 main 方法的问题啦。

因为 JUnit 方法虽然可以上传提交,但是仍然会经过查重,这里不提供详细代码。所以下面仅提供一个我的操作流程,供大家参考。

软盘+蟒蛇图标

看到 dataInput 那个经典图标,我就知道这是 PyInstaller 打包的 Python 程序,于是从网上搜索了如何将它反编译出源码,并顺利得到了 dataInput 的源码。嗯,这么点源码打包成 5MB 的程序,太有 PyInstaller 那味了。

AI 机器,小子!

其实不管是 dataInput 的源码,还是 TestMain 的源码,都有被查重的风险,所以最简单的办法当然是拿着这些源码去找 AI,让它根据源码为我们写一个 MainClass.main()(或者 Main.main() 之类)的 JUnit 方法就好了。不过 AI 生成也不一定完全能够保证不被查重(

然后照着 oop 的方法设置 JUnit 就好了。

输入输出重定向

我发现测试方法的运行配置里没有“重定向输入”的选择,只有“将控制台输出保存到文件”的选择。也就是说我们得在 JUnit 方法里实现输入输出重定向。

我们可以使用 java.nio.file 下的 PathsPathFiles 等类来实现重定向。后面带 s 的俩类提供了许多静态方法。

Path testFilePath = Paths.get(System.getProperty("user.dir"), "stdin.txt");
Path outputFilePath = Paths.get(System.getProperty("user.dir"), "usrout.txt");
List<String> lines = Files.readAllLines(testFilePath);
PrintStream outputStream = new PrintStream(Files.newOutputStream(outputFilePath.toFile().toPath()));
System.setOut(outputStream);

这里我们将标准输出重定向到了 usrout.txt 文件,这个文件在用户当前工作目录下,也就是我们的项目的根目录下。

stdin.txt 就是带时间戳的输入,也放在同一目录下,使用 Paths.get()Files.readAllLines() 读取到 List<String> 中。

清理线程

实际使用 JUnit 方法时遇到了一些问题:

  1. MainClass.main() 方法一般都是创建线程、启动线程,然后就不管这些线程的死活了。
  2. JUnit 方法执行完成之后,IDEA 就直接把整个程序结束了。

也就是说,当 JUnit 方法把输入定时交给程序完成之后,程序就结束了,电梯线程以及其他线程就会被终止。

即便我们在 JUnit 方法里创建 main 方法的线程(比如叫 mainThread),然后在最后使用 mainThread.join() 等待线程结束。

但我们等待的只是 mainThread 这一线程的结束:当 main 方法运行完成之后,它就结束了。我们并不能确定 main 方法中创建的其他线程的状态。

所以解决办法有两个:

  1. main 方法中创建并收集线程,并在最后使用 thread.join() 方法等待所有线程结束。
  2. 在 JUnit 方法中一开始记录程序运行中的所有线程;在方法结束之前,再次记录程序运行中的所有线程,减去前面的进程,就可以得到 main 方法中创建的进程了。之后再等待这些线程结束就好了。

对于第二个方法,部分代码如下:

Set<Thread> initThreads = new HashSet<>(Thread.getAllStackTraces().keySet());
// ... after start main thread
Set<Thread> curThreads = new HashSet<>(Thread.getAllStackTraces().keySet());
curThreads.removeAll(initThreads);
Set<Thread> userThreads = new HashSet<>();
for (Thread t : curThreads) {
    if (t.isAlive() && !t.isDaemon()) {
        userThreads.add(t);
    }
}

这里需要排除一些 Daemon 守护线程,一般作垃圾回收、释放内存等用途。

魔改投喂包

前言

目前我们输入带时间戳的数据要么是使用dataInput,要么使用类似助教发在第五次作业讨论区里的帖子里的方法。

但两种办法都有共同的问题:由于这两种方法使用的时钟的起始点(也就是方法中输入流初始化的时间)和 TimableOutput 时钟的起始点(即主类里调用 TimableOutput.initStartTimestamp() 的时间)不完全相同,导致输入和输出流总存在一定时间的差异。(好吧,这其实没什么……)

所以我尝试了魔改官方包,将 dataInput 的功能加入到官方包中,同时对官方包其他部分做了一定的修改,使得魔改版本可以无缝衔接原版官方包的使用,同时也具有了 dataInput 的功能

链接

以下是魔改包的北航云盘链接,更新了三次作业所需的 elevatorX.jar。

注意!!!这是魔改包!!!不是官方包!!!

魔改包的一切行为以及使用魔改版官方包造成的任何问题均与课程组无关!!!

如果有 BUG,您可以随意地向我指出,我会尽快更改。

魔改有风险,使用需谨慎!一切因为使用魔改包导致的评测错误问题,作者概不负责!

如果您不同意上述观点,请不要打开下面的链接,请不要下载或使用魔改包。

https://bhpan.buaa.edu.cn/link/AAAFA902F6C80B4CA9BA560B35B1890B58
文件夹名:OO_unofficial_jar
有效期限:永久有效
提取码:6666

过程

好吧,其实我本人感觉问题不是特别大,但出于保命需要还是在上面加了很多声明,希望大家理解。

只要你不因为使用魔改版而改变了你程序中原本遵守官方包的行为,你大概率就不会遇到因使用魔改版而导致的评测问题。

这么说的理由是提交评测时根本不看你往文件夹里塞了什么依赖包,评测机在乎的大概只有你的源码(未经

由于可能有潜在的问题,我还是将修改的部分在下面向大家展示,也方便大家一起讨论,看看哪些地方可能会有 bug。

获取源码

设法在 IDEA 安装目录下找到 plugins\java-decompiler\lib\java-decompiler.jar,把这个 jar 包复制到一个方便使用的目录下。

把官方包复制到复制出来的 jar 包同目录下,再创建一个文件夹,随便命名,比如 src

使用命令 java -jar .\java-decompiler.jar -log=warn elevator2.jar src/ 反编译官方包,得到的结果在 src 目录下,也是个 jar 包,解压即可得到含有源码的文件夹。

此外,也可以反编译出官方投喂程序的源码,不展开讲了。

null 修复
  • 问题:官方包执行 TimableOutput.println(null); 会报错 NullPointerException
  • 原因:官方包 TimableOutput 类下的私有类 ObjectWithTimestamp 的方法 toString() 实现中直接调用了 this.getObject().toString(),当 this.getObject() 为空时报错。
  • 解决:把 this.getObject().toString() 改为 this.getObject()

这个修复可能有风险,原本因为 null 而产生报错,现在不会了,只会输出 “null”。

虽然这样,但两种情况一样是错的,本地很好检查(输出多一行 null 肯定不对吧)。

修复的理由只是想让 TimableOutput.println() 的行为更贴近 System.Out.println() 罢了。如果感冒,可以自行把这个修复改回去。

定时输入支持

主要对 ElevatorInput 类做出了修改。

  • 添加一个私有静态成员 private static final Pattern TIMESTAMP_PATTERN,用于解析时间戳。
  • nextRequest 方法头部添加一些代码(见下),用于定时输入。

具体添加代码如下:

private static final Pattern TIMESTAMP_PATTERN = Pattern.compile("^\\[(?<time>\\d+(\\.\\d+)?)](?<command>.*)$");

public Request nextRequest() {
   while(this.scanner.hasNextLine()) {
      String line = this.scanner.nextLine();
      line = line.trim();

      if (line.isEmpty()) {
         continue;
      }

      Matcher matcher = TIMESTAMP_PATTERN.matcher(line);
      String commandPart = line;

      if (matcher.matches()) {
         try {
            double timestampSeconds = Double.parseDouble(matcher.group("time"));
            commandPart = matcher.group("command").trim();
            long targetRelativeMillis = (long) (timestampSeconds * 1000);
            long currentRelativeMillis = TimableOutput.getRelativeTimestamp();
            long delayMillis = targetRelativeMillis - currentRelativeMillis;

            if (delayMillis > 0) {
               Thread.sleep(delayMillis);
            }

            if (commandPart.isEmpty()) {
               continue;
            }
         } catch (NumberFormatException e) {
            System.err.println("Invalid timestamp format in input line: " + line);
            return null;
         } catch (InterruptedException e) {
            System.err.println("Input reading thread interrupted.");
            Thread.currentThread().interrupt();
            return null;
         }
      }

      line = commandPart;
      // original code below ...
   }

   return null;
}

代码逻辑也很简单,做了一些输入预处理,然后开睡,遇到错误直接返回。循环条件 this.scanner.hasNextLine() 从官方文档看来是阻塞的,因此大概不会造成 CTLE 的问题。

统一时间戳

为了防止多次初始化时间戳导致时间一直归零,课程组的源码是这么写的:

private static long startTimestamp = 0L;

public static synchronized void initStartTimestamp() {
    if (startTimestamp == 0L) {
        startTimestamp = System.currentTimeMillis();
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        PrintStream printStream = new PrintStream(stream);
        println((Object)"This is random start string.", printStream);
        printStream.close();
    }
}

所以在我们的程序中,只有第一次调用 TimableOutput.initStartTimestamp() 时才有效,后面如果还有调用是无效的。

魔改版去除了这一限制,主要是为了在第一次输入时再进行一次初始化,以对齐输入和输出的时钟。

主要是考虑到控制台输入,当用户很晚才有输入时,用户输入的时间(与初始化零点的相对值)大于输入数据中时间戳的时间(与初始化零点的相对值),输入线程根本没睡就直接传入数据了,这会使得电梯系统运行逻辑与预期逻辑不符。

魔改版修改的部分如下:

TimableOutput 类:

public static synchronized void initStartTimestamp() {
    // if (startTimestamp == 0L) {
        startTimestamp = System.currentTimeMillis();
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        PrintStream printStream = new PrintStream(stream);
        println((Object)"This is random start string.", printStream);
        printStream.close();
    // }
}

ElevatorInput 类:

private boolean isTimableOutputInitialized = false;
public Request nextRequest() {
    while(this.scanner.hasNextLine()) {
        // add these lines in the front of the while
        if (!this.isTimableOutputInitialized) {
            TimableOutput.initStartTimestamp();
            this.isTimableOutputInitialized = true;
        }
        // below is the code mentioned above ...

于是,在魔改版中,真正初始化时间的点在第一次输入时,而不是 main 方法头部。只要保留 main 方法头部的初始化,就可以(在时序上)与官方包保持相同的行为。

编译打包

在编写测评机的时候,我遇到了编译打包源码的问题。

首先是编译源码,javac 不能识别并找到官方包依赖。

其次是打包时,jar 也不能识别并找到官方包依赖。

查了一番资料后,我得到了以下解决方法:

javac -d <project_dir> <main_class_path> -sourcepath <src_path> -classpath <path_to_elevator1_jar> #带依赖编译源码
jar xf <path_to_elevator1_jar> # 解压官方包
# 将解压后的文件复制到 <project_dir> 下
jar cfm <jar_name> <manifest_path> -C <project_dir> . # 将文件夹打包成 jar

其中:

  • <project_dir> 就是源码的工作目录
  • <main_class_path>main 方法所在类文件的位置
  • <src_path> 是指 main 方法所在类文件的所在目录,注意和 <project_dir> 的不同
  • <manifest_path> 是在 jar 包中固定位置与格式的一个 MANIFEST.MF 文件,可以参考其他打包好的 jar 包编写。

然后就可以得到正确的包了。

评测 RTLE 和 CTLE

可以在 Python 中这里使用 psutil 模块监视程序的 CPU 时间,使用 time.monotonic() 获取单调时钟,具体可以看代码。

import subprocess, psutil, time

CPU_TIME_LIMIT = 10
WALL_TIME_LIMIT = 120
jar_path = "" # PUT JAR FILE PATH HERE
stdin_path = "" # PUT STDIN FILE PATH HERE
stdout_path = "" # PUT STDOUT FILE PATH HERE

with open(stdin_path, 'r') as stdin_file, open(stdout_path, 'w') as stdout_file:
    process = subprocess.Popen(['java', '-jar', jar_path], stdin=stdin_file, stdout=stdout_file)
    running = True
    start_time = time.monotonic()
    while running:
        exit_code = process.poll()
        if exit_code is not None: running = False # process exited
        else: 
            current_time = time.monotonic()
            try:
                proc = psutil.Process(process.pid)
                cpu_times = ps_proc.cpu_times()
                current_cpu_time = cpu_times.user + cpu_times.system
                if current_cpu_time > CPU_TIME_LIMIT: break # CTLE break
            except psutil.NoSuchProcess: running = False # process disappeared!
            if current_time - start_time > WALL_TIME_LIMIT: break # RTLE break
            time.sleep(0.5) # 轮询检查
    process.wait()
...全文
93 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

269

社区成员

发帖
与我相关
我的任务
社区描述
2025年北航面向对象设计与构造
学习 高校
社区管理员
  • Alkaid_Zhong
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧