2024北航面向对象第二单元博客作业

衔于0 学生 2024-04-20 19:03:02

目录

同步块与锁

最终整体架构

UML类图

UML协作图

第一次作业——电梯的基本功能

基本的架构设计

调度器和调度策略设计

第二次作业——电梯的维护重置

基本的架构设计

调度器和调度策略设计

第三次作业——电梯的双轿厢重置

基本的架构设计

调度器和调度策略设计

避免轿厢相撞

第三次作业未来可能的扩展

稳定和易变

Bug分析

心得体会


同步块与锁

同步块与锁,是Java提供的解决线程安全问题的机制。它保证同一时刻只有一个线程进入临界区,访问临界资源。

在同步块和锁的设置上,我认为最核心的是考虑好下面的问题:第一,这个对象是不是共享资源?第二,这个对象被哪些线程共享?第三,这个对象被线程以什么样的方式共享?只要是共享资源,就要用同步块限制那些线程对它们的访问(除非均只读),在线程访问那个对象的方法上加锁。synchronized机制可以很好地解决多数情况下的同步控制问题,但有些时候为了提高并发效率,也可以使用读写锁,使同时对共享对象的读不受限制。

锁与同步块中处理语句之间的关系:同步块中的处理语句只有获得锁才能执行,锁使得同步块中的处理语句执行过程中,不会转入别的线程。

最终整体架构

UML类图

 

UML协作图

 

第一次作业——电梯的基本功能

第一次作业的要求实现:接受乘客乘坐请求(FROM、TO、Elevator),并通过实现电梯的一系列动作(上下移动、开关门、停止等),完成所有乘客的请求。

基本的架构设计

采用的主要架构模式:生产者-消费者模式。

生产者-消费者模式十分适用于本次乃至本单元作业的设计需求。输入线程不断读取乘客请求,并将其放到请求队列里,交给调度器;调度器不断拿出乘客请求,并将其放到电梯的请求队列里,交给电梯处理。这是两次生产者-消费者模型。采用这一设计模式,使得解决问题的思路和代码的架构都变得清晰。具体而言,设计一个Input类(extends Thread),用于处理输入;设计一个Dispatch类(extends Thread),用于实现分配;Input对象和Dispatch对象间共享一个请求队列RequestTable对象;设计一个Elevator类(extends Thread),用于完成请求;Dispatch对象和每个Elevator对象间都共享一个请求队列RequestTable。在Main类中创建各对象、启动线程。

调度器和调度策略设计

第一次作业由于指定了乘坐的电梯,所以调度器要做的事情就十分简单,调度策略就是读取乘客要求的电梯ID,将其分给要求的电梯即可,不能擅自改变。调度器与其它线程间的交互,都基于共享对象。如调度器基于共享的RequestTable请求队列对象从输入线程中获取请求、收到结束标志、被输入线程唤醒等,类似地,基于共享的RequestTable请求队列对象向各电梯线程发送乘客请求、发送结束标志、唤醒电梯。

第二次作业——电梯的维护重置

在第二次作业中,增加了电梯重置(reset)的要求。电梯在重置时,需要尽快停靠,取消已接收的请求,放出电梯内的乘客,开始重置,并在经过1.2秒后完成重置。重置后的电梯可能改变限乘人数和移动速度。此外,电梯收到乘客请求时,需要输出相应的RECEIVE信息,并且一个乘客请求同时最多只能由一个电梯接收,避免了“一人乘坐,六梯皆动”的现象。

基本的架构设计

基本的架构仍然为生产者-消费者的设计模式,但是需要做出一些改动:本次作业中,由于电梯会因reset而中途放出乘客、取消请求,导致一些乘客的请求可能没有完成就离开电梯。此时显然需要将这些未完成的请求重新分配,即再次放回总请求队列中。因此,电梯和输入线程一样,也成了总请求队列的生产者,需要调用同步控制的方法写总队列。同时要注意,既然电梯也成了总队列的生产者,那就不能因为输入的结束就判定“生产”的全部结束,还要增加电梯“生产”是否结束的判断条件(电梯还未处理完的reset请求数是否等于0),并且电梯“生产”结束时也要通知调度器(notifyAll),以避免调度器无限wait下去。

此外,对于“尽快停靠”要求,本次作业中我采用了对reset请求也过一遍两个队列和调度器的方式,认为这样应该也不会消耗太多时间。后来在测试中发现,由于调度逻辑(乘客请求不适合分配到任何电梯时,就sleep一下再尝试分配,此时,后输入的reset请求也会被卡住,暂时不会分配),会有少数时候出现停靠不够快的情况。为了避免在临近截止前引入bug,我采取了一些别的办法,暂时解决了这个问题,而在第三次作业中完全解决。

调度器和调度策略设计

在调度策略方面,我选择均匀分配。当时我认为均匀分配和随机分配应该效果相同,并且均匀分配因为分配方式更有规律可循(123456循环),也更有利于减少随机因素,复现bug。但是强测成绩出乎我的意料,随机分配强测成绩普遍在95分以上,并且单测试点几乎没有低于90的;而均匀分配只有90.0几分,单测试点甚至惊现了八个85,这个数据已经足以说明问题。分析之后我认为,这主要是由于耗电量的差异导致的,因为测试表明均匀分配总运行时间总是明显小于随机分配,在时间上有优势。但均匀分配方法下,几乎任意时刻总有6个电梯一起在动,而随机分配则未必。比如6个请求,均匀分配会将6个请求分给6部电梯去完成,而随机分配却很少能在6次随机中恰好随完1-6六个数。看似每一次的微小差异,最终带来了总性能的巨大差异。因此,在第三次作业中,我决定将均匀分配改为随机分配,以满足耗电量的要求。

在调度器与其它线程的交互方面,多了一种电梯线程与调度器线程的交互,即调度器线程要得知电梯线程是否还有未处理完的reset请求。这一交互仍然基于共享对象进行。电梯线程与调度器线程共享总请求队列,我们便在总请求队列中维护一个未处理完的reset请求的数量,该量由输入线程、电梯线程写,由调度器线程读,由此实现交互。

第三次作业——电梯的双轿厢重置

本次作业要求实现电梯的双轿厢重置,即在收到相关指令时,立即开始与上一次实现的电梯重置同样的操作,重置结束后,原本的单轿厢变为分别在上区、下区运行的两个轿厢。两个轿厢分别在划分好的上区和下区运行,共享同一个换乘楼层,重点在于保证两轿厢不在换乘楼层相“撞”。此外,双轿厢电梯有巨大的耗电量优势。

基本的架构设计

在课上得知要实现双轿厢电梯时,我一直以为是一开始就有12个轿厢,所以构思的是一开始就开12个线程。等到晚上指导书发布才发现,是用第二类重置指令重置为双轿厢,比我想的难度大了不少。但一开始就开好12个线程的想法已经形成了思维定势,我最终未跳出原有的思维定势,而是继续采用这一想法,经过一些调整形成了我的最终架构。

双轿厢电梯,我是这样实现的:在单轿厢电梯DCreset时,直接将原单轿厢的elevatorType由空串改为"-A",继续使用,并在DCreset方法内调用另一轿厢的start方法,启动B轿厢线程。轿厢1~6用作原单轿厢、轿厢A,轿厢7~12用作轿厢B,并且1和7、2和8......6和12一一对应。为了在原单轿厢中调用另一轿厢的start方法,原单轿厢必须拥有另一轿厢的引用,这通过在构造方法中作为参数传入实现。

调度器和调度策略设计

如上所述,本次作业我主要使用了随机策略。此外,为利用双轿厢电梯的耗电量优势,还限制:有双轿厢电梯时,优先在双轿厢电梯中随。但是如果一个请求是因为换乘而被扔回的请求,那么强制将其分回原电梯的另一轿厢,这主要是为了避免多次换乘,分回原电梯的另一轿厢后必定不会再次换乘。

调度器与其它线程的交互,多了一种电梯线程因换乘而扔回请求给调度器重新分配的情形。调度器要得知是否还有可能被扔回的请求(即:是否还有未完成乘客请求),这同样通过共享的对象实现。类似上次判断是否还有未完成reset请求的处理方法,在总队列中维护一个未完成的乘客请求数量,由输入线程、电梯线程写,调度器线程读,实现有无未完成乘客请求的判断。

避免轿厢相撞

为了避免双轿厢相撞,我又设置了一个双轿厢间的共享变量TransferState类,让双轿厢电梯去争夺TransferState对象的“锁”,争抢到后才可以去换乘楼层。其实这样做的原因是,换乘楼层本身很像一个“共享资源”同一时刻仅允许一个轿厢“访问”,若轿厢A想要“访问”时轿厢B正在“访问”换乘楼层,则轿厢A对换乘楼层的“访问”必须暂时阻塞,等到轿厢B“释放”换乘楼层时才可以“访问”。这与互斥的概念不谋而合,因此可以采用解决互斥问题的手段,用个TransferState类的对象表示换乘楼层,将两个轿厢对换乘楼层的互斥争夺转化为对TransferState对象的“锁”的争夺,这样,不能同时位于换乘楼层的问题就迎刃而解了。具体代码实现如下:

 

第三次作业未来可能的扩展

加入横向电梯:两类电梯实现Elevator接口,共用一部分行为(如move、openAndClose等),而各用一部分行为(如moveDirection)即可。

更复杂的维护请求:处理流程可以沿用,而在具体维护内容和结果上可以重新编写。

电梯的启用和禁用:初始时,六个电梯都可以启用,电梯接收禁用指令后要离开电梯系统,直到重新接收启用指令为止。禁用的处理方式与reset类似,但重新启用要由信号给出,并且存在禁用后未启用的情况。

稳定和易变

这三次作业的架构中,既有稳定的、可一直沿用的部分,又有易变的、随时会因需求的变动而变动的部分。

输入—调度器—电梯的三线程架构是稳定的,符合电梯运行实际,电梯新增再复杂的功能,也离不开输入—分配—处理这三个处理问题的基本流程。这个架构,和其所基于的生产者-消费者模型,是代码的“骨架”。

电梯运行时采用的LOOK策略是稳定的,捎带乘客的算法不会因为新增了别的功能改变。捎带乘客是电梯的基本功能。

电梯新支持的其它功能及其变化是可变的。就比如在二、三次作业中新增的电梯重置功能,是迭代出的功能。而其中,电梯重置时的行为(尽快停靠、放乘客、退请求等)又是固定的,重置后的结果和一些相应的实现细节是可变的。

Bug分析

本单元三次作业均未在强测和互测中出现Bug。我认为,减少Bug的关键在于做好本地测试,尤其是大量构造手工数据。手工构造数据往往能更加针对性地发现一些Bug,不要仅仅依赖于自动化构造数据。手工构造数据可以专门测试某一个功能,往往会有意想不到的收获。

以下是一些我在本地测试的过程中发现的关键Bug:

1.鼎鼎大名的“围师必阙”,即在5个电梯都在reset时输入一大批请求,此时调度器会将所有请求都分给空闲的那部电梯,导致最后只有一部电梯在实际运转而超时。此Bug由于发现,所以防了一手,采用的办法是:如果不在reset中的电梯收到的请求数量都已达最大限制12,那么就sleep一段时间再尝试分配,等待新reset完电梯或者有电梯处理完了部分请求而腾出空位。当时没有想到reset中的电梯也可以分配,只是暂不输出的方法。

2.反复开关门问题。判断本层是否可以OpenForIn时,要加上未满员的条件,否则会导致满载的电梯一直CanOpenForIn,而开门后又无法进人,导致停在原地反复开关门,程序无法运行结束。

3.结束条件的改变。在第二次作业、第三次作业中,要新增结束条件电梯的生产也已完毕,总队列的生产才算都完毕,因为总队列的生产者由输入变成了输入和电梯。遗漏结束条件的改变,会很容易导致提前结束,乘客请求未完成完。

4.对第七次作业,利用耗电量优势而产生的Bug“围师必阙2”。若分配逻辑中有“只要存在双轿厢电梯,就优先往其中分配”,而不做其它限制的话,那只在开始DCreset一部电梯,随后输入一大批请求,同样会因为请求都分配给那部双轿厢电梯,而导致超时。但大家基本都做了相应的改进,所以这个也没hack到什么人。

对于debug方法,对于多线程程序,不得不重新采用大名鼎鼎的printf大法(),如在wait前后各加一个begin wait和end wait输出判断是否死锁等。

心得体会

1.线程安全:要想做好线程安全,核心就是要做好对共享资源访问的限制,一个线程访问时,不允许其它线程访问。为了进一步提高效率,可以使用读写锁来实现允许多个线程同时读的情况。据此,我们分析好每个资源是否被共享,被谁共享,以及是否写,据此对相应的代码进行同步控制即可。

2.层次化设计:本单元中的代码结构主要表现为并列结构(输入、分派、电梯三个线程同时执行),多线程并发完成任务。在编码时,要理清完成一个请求的流程,设计相应的线程和功能模块。

...全文
72 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复
内容概要:本文提出了一种基于加权稀疏矩阵恢复与加速交替方向乘子法(ADMM)的单通道盲解混响算法,并提供了完整的Matlab代码实现。该方法旨在从仅有的单路接收信号中有效分离出原始声源信号,克服传统多通道方法对硬件的依赖。核心技术结合了信号在时频域的稀疏性先验,通过构建加权机制以增强稀疏矩阵恢复的准确性,并引入加速ADMM算法来优化求解过程,显著提升了算法的收敛速度与计算效率。该算法特别适用于麦克风阵列受限或无法部署的复杂声学环境,能够有效抑制混响干扰,从而显著提升语音信号的清晰度与后续语音识别系统的性能。; 适合人群:具备扎实的数字信号处理、凸优化理论及稀疏表示基础,从事音频信号处理、语音增强、盲源分离或相关领域研究与开发工作的研究生、科研人员及工程技术人员。; 使用场景及目标:①解决单麦克风场景下的语音混响去除难题,提升语音通信质量;②应用于智能助听器、车载语音系统、远程视频会议、人机交互等存在严重混响的实际应用场景;③为盲解卷积、稀疏信号恢复等领域的研究提供一种高效的算法实现范例与优化思路。; 阅读建议:建议读者在深入理解信号稀疏性、ADMM优化框架等理论基础上,结合所提供的Matlab代码进行实践,重点分析加权策略的设计原理及其对恢复性能的影响,并通过调整正则化参数、权重因子等关键变量,探究其在不同混响强度和噪声条件下的鲁棒性与泛化能力。

301

社区成员

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

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