Post

SPOOLing 模拟共享打印机

SPOOLing 模拟共享打印机

什么是假脱机技术?

假脱机技术(Spooling)是一种在多道程序环境下,将IO操作与CPU操作分离的技术。在打印系统中,多个打印任务可以同时提交到打印队列中,由假脱机技术对这些任务进行管理和调度,实现打印机的共享使用,每个程序都认为自己独占了打印机。本课程设计旨在通过Java可视化实现假脱机技术的共享打印机模拟,让其他学习者直观地了解假脱机技术的工作原理和优势。

4f27e3522f8536dd5ed3f.png

1. 如何模拟?

下图展示了模拟过程(输出队列是输出井,假脱机管理器是管理程序)

通过事件监听器,用来处理用户界面的各种交互操作,如提交任务和重置打印队列的按钮。

5a534dbf34f8d1c4fb7cc.png

我使用一个优先级阻塞队列模拟打印任务的排队过程,它可以视作逻辑上磁盘缓冲区里的输出井。每个任务按照优先级和时间戳进行排序。用户通过可视化界面提交打印任务,系统将为每个任务分配一个唯一的编号(ID),并将其加入到队列中。打印机模拟为一个单线程执行服务 ExecutorService,每当空闲,依次从队列中取出任务进行打印。打印过程中,系统会显示当前打印任务的信息,以及等待队列中的任务列表。用户还可以通过界面重置打印队列,清空输出井里的任务

在传统SPOOLing系统中,会为每个打印请求创建一个请求表,记录请求的详细信息。在这个模拟程序中,PrintTask类的实例实际上就扮演了打印请求表的角色,它包含了打印内容、优先级、唯一ID(相当于请求标识)等信息。通过outputQueue(等待打印的任务队列、输出井)来管理这些请求,模拟了将请求表挂到假脱机文件队列上的过程。

最终,通过为每个用户进程分配一个逻辑上的存储区,系统实现了对打印机的共享。每个用户都感觉到自己在独占打印机,实际上是系统在快速地为多个用户服务。

数据结构说明

1.打印任务类(PrintTask)

1
2
3
4
5
6
7
8
9
10
11
   static int idCounter = 0; 
  // 静态变量,用于生成唯一的任务编号 (构造方法里  this.id = ++idCounter;)
   private int id; // 任务编号
   private String content; // 打印内容
   private int priority; // 任务优先级
   private long timestamp; // 任务提交时间戳
 
   public PrintTask(String content, int priority) { ... }

//  构造方法及访问器方法...
 
  1. 打印假脱机模拟类(PrintSpoolerSim)
1
2
3
4
public class PrintSpoolerSim extends JFrame {
   private PriorityBlockingQueue<PrintTask> outputQueue = new PriorityBlockingQueue<>(10, Comparator.comparingInt(PrintTask::getPriority).thenComparingLong(PrintTask::getTimestamp));
   // ...
 }
  • outputQueue:这是一个PriorityBlockingQueue类型的队列,用于存储等待打印的任务。它是一个优先级队列,根据任务的优先级和提交时间戳进行排序,确保高优先级的任务或先提交的任务能够优先得到处理。假设队列的大小被初始化为10,那么意味着最多可以同时存储10个任务。

submitTask()

9e5deab921b34d7a9b27b.png

processTasks()

5de874408cebd20f0114b.png

resetQueue()becfa62b21e73d687d410.png

2. PriorityBlockingQueue 的使用和优先级处理

我是如何实现任务的优先级处理的?

答:为了实现任务的优先级处理,我使用了 PriorityBlockingQueue。这个队列支持优先级排序,任务按照优先级和时间戳进行排序。我们在创建队列时传入了一个比较器先比较任务的优先级,如果优先级相同再比较时间戳。这确保了即使在多线程环境下,高优先级的任务也能优先处理。

1
2
private PriorityBlockingQueue<PrintTask> outputQueue = new PriorityBlockingQueue<>(10, 
    Comparator.comparingInt(PrintTask::getPriority).thenComparingLong(PrintTask::getTimestamp));

3. 线程安全和任务执行

我的系统如何保证线程安全和任务的顺序执行?

答:为了保证线程安全和任务的顺序执行,我使用了 ExecutorService,具体来说是 SingleThreadExecutor。这个执行器确保了任务是按顺序执行的,每次只运行一个任务,这避免了并发执行可能带来的问题。

1
private ExecutorService printerService = Executors.newSingleThreadExecutor();

4. GUI实时更新机制

我使用了 SwingUtilities.invokeLater 方法。这个方法确保所有的界面更新操作都在事件调度线程上执行,这样可以避免线程冲突和界面冻结。例如,在添加任务或更新打印状态时,都使用了这种方法来确保界面的响应性和数据的一致性。

1
2
3
4
SwingUtilities.invokeLater(() -> {
    queueArea.append("任务提交: " + task.getContent() + ",优先级 " + task.getPriority() + ",任务编号 " + task.getId() + "\n");
    updateWaitingQueueArea();
});

5. 异常处理和用户输入验证

在用户输入方面,我设置了输入验证来确保数据的有效性。例如,优先级必须是整数。如果用户输入无效,系统会通过对话框提示他们。运行过程中,我也添加了异常处理机制,比如在打印任务执行时捕获中断异常,以确保系统稳定运行并及时响应用户操作。

1
2
3
4
5
6
try {
    int priority = Integer.parseInt(priorityField.getText());
    submitTask(content, priority);
} catch (NumberFormatException e) {
    JOptionPane.showMessageDialog(this, "优先级必须是整数", "输入错误", JOptionPane.ERROR_MESSAGE);
}

6. 性能优化和未来改进方向

答:目前系统的性能能满足基本的使用需求,但在处理大量任务或高并发场景下可能会有性能瓶颈。未来,我打算引入更复杂的任务调度算法,比如基于预估打印时间的动态优先级调整。此外,为了适应多用户环境,我计划支持多台打印机并行处理任务,这可以通过配置多个线程池来实现。这样,系统的处理能力和效率将大大提高。

7. 代码展示、实际运行示例

简单演示一下系统的实际运行情况

  • 启动程序后,用户在上方的输入框中输入打印内容和优先级,点击提交按钮。任务会被添加到队列中,并显示在队列区。
  • 随后,系统会自动开始打印任务,每个字符打印间隔两秒。

  • 在打印过程中,打印区会实时更新显示当前打印的内容。

  • 用户也可以点击重置按钮,清空所有任务,包括正在执行的任务。

演示输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
任务提交: 打印测试文本打印测试文本打印测试文本打印测试文本,优先级 1,任务编号 1
任务提交: 打印测试文本1,优先级 2,任务编号 2
任务提交: 打印测试文本2,优先级 2,任务编号 3
任务提交: 打印测试文本3,优先级 3,任务编号 4
任务提交: 打印测试文本4,优先级 1,任务编号 5
任务提交: 打印测试文本5,优先级 1,任务编号 6
任务提交: 打印测试文本6,优先级 2,任务编号 7
任务提交: 打印测试文本7,优先级 3,任务编号 8
任务提交: 打印测试文本8,优先级 4,任务编号 9
任务提交: 打印测试文本9,优先级 1,任务编号 10
任务提交: 打印测试文本10,优先级 1,任务编号 11
任务提交: 打印测试文本11,优先级 1,任务编号 12
任务提交: 打印测试文本12,优先级 3,任务编号 13
打印队列已重置

8. 参考文献

  1. Java API 文档 https://docs.oracle.com/javase/8/docs/api/

  2. 《Java 核心技术 卷 I 基础知识》(第十版)凯 S. 霍斯特曼

  3. 《Java 并发编程实战》布莱恩·戈茨

  4. 《计算机操作系统》汤小丹等
  5. mermaid 介绍文档,用于生成流程图 https://mermaid.js.org/intro/
This post is licensed under CC BY 4.0 by the author.