山东大学Nachos课设实验一(六部分全)

Task 1.1 Join

  • 实验要求
  • 实验关键代码
  • 关键代码分析
  • 实验测试代码
  • 测试结果分析

实验要求

◆ Implement KThread.join();
◆ Note that another thread does not have to call join(), but if it is called, it must be called only onceThe result of calling join() a second time on the same thread is undefined, even if the second caller is a different thread than the first caller;
◆ A thread must finish executing normally whether or not it is joined;

实验关键代码

变量声明

private ThreadQueue waitForJoinQueue = null;//等待join方法结束的线程队列  
private int JoinCount = 0;//定义调用join的次数

方法实现

关键方法主要为join()方法和finish()方法。

Join()方法:

public void join() {
    Lib.debug(dbgThread, "Joining to thread: " + toString());
    Lib.assertTrue(this != currentThread);
    if(++JoinCount!=1){//如果调用超过一次,则调用失败
        System.out.println("sorry,只能调用一次!");
        return;
    }
    boolean preState = Machine.interrupt().disable();//关中断
    //让this线程成为waitForJoinQueue队列的头,表明只有先执行this线程,才会去执行队列里的线程
    waitForJoinQueue.acquire(this);
    waitForJoinQueue.waitForAccess(currentThread);//将当前线程加入到waitForJoinQueue队列里
    currentThread.sleep();//当前线程睡眠
    Machine.interrupt().restore(preState);//恢复中断
}

Finish()方法中在线程sleep()之前添加的核心代码为:

KThread x = currentThread.waitForJoinQueue.nextThread();
if(x!=null)   x.ready();

关键代码分析

思路分析

当前执行线程(如a线程)调用某线程(如b线程)的join()方法,结果要让a线程睡眠,而让b线程先于a线程执行,b线程执行结束a线程才可继续执行。

方法解释

1、join()方法
将a线程暂时保存到b线程的waitForJoinQueue队列中【在以上代码中采用waitForJoinQueue来保存】,然后让a线程睡眠。

2、finish()方法
在b线程执行结束时(线程结束时会调用finish()方法),扫描一下waitForJoinQueue,如果有线程,则将它唤醒。

关键点和难点

关键点:理清join()方法的含义,要想办法将睡眠的线程存起来,到唤醒时将其唤醒。
难点:理解finish()方法是每个线程执行结束时必会执行的方法。

实验测试代码

public static void joinSelfTest(){
    final KThread a = new KThread(new PingTest(1));
    System.out.println("thread 1 启动");
    a.fork();
    System.out.println("调用join方法,当前线程阻塞,thread 1 执行结束后thread 0 再执行【thread 0 为主线程】");
    a.join();
    System.out.println("thread 0 开始执行");
    new PingTest(0).run();
}

测试结果分析

测试结果分析

实验测试代码使用main线程【在测试结果中使用thread 0来表示】和a线程【在测试结果中用thread 1来表示】来对join()方法进行测试,main线程通过调用a线程的join()方法,从而让a线程先执行,执行结束后main线程再继续执行。

Task 1.2 Condition Variables

  • 实验要求
  • 实验关键代码
  • 实验测试代码
  • 关键代码分析
  • 测试结果分析

实验要求

通过使用中断启用和禁用来提供原子性,直接实现条件变量

我们已经提供了一个使用信号量的示例实现;您的工作是提供一个不直接使用信号量的等价实现(当然您仍然可以使用锁,即使它们间接使用信号量)

一旦你完成了,你将有两个提供完全相同功能的替代实现

条件变量的第二个实现必须驻留在类nachos.threads.Condition2中

实验关键代码

变量声明

private LinkedList<KThread> waitQueue;
private Lock conditionLock;

方法实现

关键代码主要为sleep()方法,wake()方法和wakeAll()方法。

Sleep()方法:

public void sleep() {
    Lib.assertTrue(conditionLock.isHeldByCurrentThread());
    conditionLock.release();//释放锁
    boolean preState = Machine.interrupt().disable();//关中断
    waitQueue.add(KThread.currentThread());//将当前线程加入到waitQueue中
    KThread.currentThread().sleep();//让当前线程睡眠
    Machine.interrupt().restore(preState);//恢复中断
    conditionLock.acquire();//申请锁
}

Wake()方法:

public void wake() {
    Lib.assertTrue(conditionLock.isHeldByCurrentThread());
    boolean preState = Machine.interrupt().disable();//关中断
    if(!waitQueue.isEmpty()){//唤醒waitQueue中的一个线程
        KThread a = waitQueue.removeFirst();//取出waitForQueue中一个线程
        a.ready();//将取出的线程启动
    }
    Machine.interrupt().restore(preState);//恢复中断
}

WakeAll()方法:

public void wakeAll() {
    Lib.assertTrue(conditionLock.isHeldByCurrentThread());
    while(waitQueue!=null)//将waitQueue中的所有线程均唤醒
         wake();
}

关键代码分析

思路分析

为什么:对于condition,原先的nachos已经给出一种方式实现,看它内部实现会发现它使用信号量实现了条件变量,但通过查看信号量底层代码,发现信号量是靠线程的睡眠和唤醒实现的,故,可以直接使用线程的睡眠和唤醒来实现信号量。

怎么做:用一个数据结构来存放睡眠的线程,在本实验中采用了LinkedList来存放,每次调用sleep()方法,会将当前线程放入LinkedList,然后让其睡眠;每次调用wake()方法会选择LinkedList里的一个线程将其唤醒;调用wakeAll()方法会将LinkedList里所有线程唤醒。

方法解释

  1. void sleep()方法
    睡眠前先释放锁,然后将线程加入到等待队列中,然后睡眠即可,记得在最后申请锁,这是因为当唤醒该线程时,需要重新申请锁。
  2. void wake()方法
    如果等待队列不为空,则取出一个线程将其唤醒。
  3. void wakeAll()方法
    设置循环,只要等待队列不为空,则执行wake()方法。

关键点和难点

关键点:理解条件变量底层实现,可以通过看nachos已经给出的condition类的方法,分析其内部实现。
难点:理解条件变量的作用,锁的使用以及等待队列的用途。

实验测试代码

本实验测试:将用到condition的地方改为condition2,运行正常即可;
注:(由于在之后的实验用到了condition2,也可以与之后的task一起测试)

测试结果分析

将用到condition的地方全部换为condition2,运行正常。

Task 1.3 Alarm

  • 实验要求
  • 实验关键代码
  • 试验测试代码
  • 关键代码分析
  • 测试结果分析

实验要求

◆ Complete the implementation of the Alarm class, by implementing the waitUntil(long x) method.

◆ A thread calls waitUntil to suspend its own execution until time has advanced to at least now + x.

◆ This is useful for threads that operate in real-time, for example, for blinking the cursor once per second.

◆ There is no requirement that threads start running immediately after waking up; just put them on the ready queue in the timer interrupt handler after they have waited for at least the right amount of time.

◆ Do not fork any additional threads to implement waitUntil();

◆ you need only modify waitUntil() and the timer interrupt handler.

◆ waitUntil is not limited to one thread;

◆ any number of threads may call it and be suspended at any one time.

实验关键代码

变量声明

private static LinkedList<WaitForAlarmThread> waitForAlarmThreadList ;

方法实现

关键代码在waitForUntil(long x)方法和timerInterrupt()方法。

1、Void waitUntil(long x)方法:

public void waitUntil(long x) {
    // for now, cheat just to get something working (busy waiting is bad)
    boolean preState = Machine.interrupt().disable();//关中断
    long wakeTime = Machine.timer().getTime()+x;//计算唤醒的时间
    WaitForAlarmThread waitForAlarmThread = new WaitForAlarmThread(wakeTime, KThread.currentThread());
    waitForAlarmThreadList.add(waitForAlarmThread);//将线程加入到等待链表中
    KThread.sleep();//让该线程睡眠
    Machine.interrupt().restore(preState);//恢复中断   
}

2、WaitForAlarmThread类【包含线程和唤醒时间的数据结构】

class WaitForAlarmThread{
    long wakeTime;
    KThread thread;
    public WaitForAlarmThread(long wakeTime,KThread thread){
        this.wakeTime=wakeTime;
        this.thread=thread;
    }
}

3、Void timerInterrupt()方法:

public void timerInterrupt() {
    boolean preState = Machine.interrupt().disable();//关中断
    WaitForAlarmThread x;
    for(java.util.Iterator i = waitForAlarmThreadList.iterator();i.hasNext();){
        x = (WaitForAlarmThread)i.next();//取出链表中的每个线程判断是否达到唤醒时间
        if(x.wakeTime<=Machine.timer().getTime()){//如果达到唤醒时间,将其从链表中移除并唤醒该线程
            i.remove();
            x.thread.ready();
        }
    }
    Machine.interrupt().restore(preState);//恢复中断
    }

实验测试代码

public static void AlarmTest(){
    KThread a = new KThread(new Runnable() {
        public void run() {
            System.out.println("线程1启动");
            for(int i = 0;i<5;i++){
                if(i == 2){
                    System.out.println("线程1要暂时隐退,此时时间为:"+Machine.timer().getTime()+",大约1700clock ticks之后再见");
                    new Alarm().waitUntil(800);
                    System.out.println("线程1回来了,此时时间为:"+Machine.timer().getTime());
                }
                System.out.println("*** thread 1 looped "
                           + i + " times");
                KThread.currentThread().yield();
            }
        }
    });
    a.fork();
    for(int i = 0;i<5;i++){
            if(i == 2){
                System.out.println("线程0要暂时隐退,此时时间为:"+Machine.timer().getTime()+",大约800clock ticks之后再见");
                new Alarm().waitUntil(1700);
                System.out.println("线程0回来了,此时时间为:"+Machine.timer().getTime());
            }
        System.out.println("*** thread 0 looped "
                   + i + " times");
        KThread.currentThread().yield();
    }
}

关键代码分析

思路分析

要想实现void waitForUntil (long x)方法,要将当前线程保存起来,记录线程唤醒时间,然后在达到唤醒时间时将其唤醒。

新建一个数据结构含有变量KThread线程和唤醒时间【在本实验中为waketime】,每次调用waitForUntil(long x)方法时,会将调用线程包装成新建数据结构的对象,并将其加入等待队列中,当到达唤醒时间时将其唤醒即可。

方法解释

1、Void waitForUntil (long x) 方法

通过参数与当前时间算出唤醒时间,将线程与唤醒时间包装起来,加入waitForAlarmThreadList中,然后让该线程睡眠。

2、void timerInterrupt ( ) 方法

这个方法大约每500 clock ticks执行一次,所以每次检查waitForAlarmThreadList 中的所有线程,如果线程的唤醒时间达到,则将其唤醒。

关键点和难点

关键点:知道如何将待唤醒的线程保存起来,使用数据结构将线程和唤醒时间绑在一起。
难点:理解void timerInterrupt()方法大约每500 clock ticks 执行一次。

测试结果分析

测试结果分析

线程0(在测试代码中为main线程)先执行一次,然后yield;

然后线程1(在测试代码中为a线程)执行一次,如此反复,直到当线程0执行第三次的时候,线程0调用了waitUntil(1700)方法,所以线程0会在大约1700 clock ticks 后继续执行,当线程1执行第三次时,也调用了waitForUntil()方法,参数为800,故线程1会在大约800 clock ticks 之后继续执行,通过看控制台打印结果,可知编写正确。

Task 1.4 Communicator

  • 实验要求
  • 实验关键代码
  • 关键代码分析
  • 实验测试代码
  • 测试结果分析

实验要求

◆ Implement synchronous send and receive of one word messages
– using condition variables (don’t use semaphores!)
– Implement the Communicator class with operations

• void speak(int word)

• int listen()

◆ speak() atomically waits until listen() is called on the same Communicator object, and then transfers the word over to listen(). Once the transfer is made, both can return

◆ listen() waits until speak() is called, at which point the transfer is made, and both can return (listen() returns the word)

◆ This means that neither thread may return from listen() or speak() until the word transfer has been made.

◆ Your solution should work even if there are multiple speakers and listeners for the same Communicator

实验关键代码

变量声明

Lock lock;
private int speakerNum;
private int listenerNum;
private LinkedList<Integer> words;
Condition2 listener;
Condition2 speaker;

方法实现

关键代码是peak(int word)方法和listen()方法。

Void speak(int word)方法:

public void speak(int word) {
    boolean preState = Machine.interrupt().disable();
    lock.acquire();
    words.add(word);
    if(listenerNum == 0){
        speakerNum++;
        System.out.println("暂时没有收听者,等待收听");
        speaker.sleep();
        listenerNum--;
    }else{
         speakerNum++;
         listener.wake();
         listenerNum--;
    }
    lock.release();
    Machine.interrupt().restore(preState);

Int listen()方法:

public int listen() {
    boolean preState = Machine.interrupt().disable();
    lock.acquire();
    if(speakerNum==0){
        listenerNum++;
        System.out.println("暂时没有说话者,等待说话");
        listener.sleep();
        speakerNum--;
    }else{
        listenerNum++;
        speaker.wake();
        speakerNum--;   
    }
    lock.release();
    Machine.interrupt().restore(preState);
    return words.removeLast();
}

实验测试代码

private static class Speaker implements Runnable {
    private Communicator c;
    Speaker(Communicator c) {
        this.c = c;
    }
    public void run() {
        for (int i = 0; i < 5; ++i) {
            System.out.println("speaker speaking" + i);
            c.speak(i);
            //System.out.println("speaker spoken");
            KThread.yield();
        }
    }
}
public static void SpeakTest() {
    System.out.println("测试Communicator类:");
    Communicator c = new Communicator();
    new KThread(new Speaker(c)).setName("Speaker").fork();
    for (int i = 0; i < 5; ++i) {
        System.out.println("listener listening " + i);
        int x = c.listen();
        System.out.println("listener listened, word = " + x);
        KThread.yield();
    }
}

关键代码分析

思路分析

本实验编写程序并不难,重点在理解这个问题的本质:缓冲区为0的生产者消费者问题。说者说话只要有听者就交流成功,否则阻塞等待听者;听者听话只要有说者就交流成功,否则阻塞等待说者说话。

方法解释

1、Void Speak ( int word)方法

先申请锁,将说的话存放在words链表中,并将说者人数加1;然后判断是否有听者,如果没有,则说者睡眠;唤醒后要将听者人数减1;如果有听者,则将听者唤醒,然后将听者人数减1;记得最后将锁释放。

2、int listen()方法

先申请锁,将听着人数加1,然后判断是否有说者,如果没有,则听者睡眠,唤醒后要将说者人数减1;如果有,则将说者唤醒,并将说者人数减1;最后记得将锁释放,并返回words里一条信息。

关键点和难点

关键点:理解什么时候人数增加或减少。
每次说者讲话,不管有没有听者,说者人数先加一;减一操作在每次听者将说者唤醒之后;听者人数变化同理,每次听话,不管有没有说者,人数先加一,减一操作在每次说者唤醒听者之后。

难点:如何避免听者和说者在同时执行:可使用一把互斥锁。

测试结果分析

测试结果分析

测试方法共使用两个线程,一个线程调用5次speak(int word)方法,另一个线程调用5次listen()方法,从而模拟了多个听者与说者的问题,从测试结果看,听者先听,发现没有说者,则听者睡眠;说者说话,唤醒听者,交流成功。

Task1.5 Priority Scheduling

本Task较为复杂,但只要思路清晰,知道每个方法的用途便可以轻松编写,下面我以五个方面来阐述它:

- 实验要求
- 实验关键代码
- 实验测试代码
- 代码分析
- 测试结果分析

实验要求

  • 通过完成PriorityScheduler类来实现优先级调度;
  • 为了使用优先级调度,你需要在 nachos.conf 文件中更改一行内容,需要将ThredededKernel.scheduler的值由nachos.threads.RoundRobinScheduler更改为nachos.threads.PriorityScheduler;
  • 优先级调度应该选择一个有效优先级最高的线程出队列,如果多个线程有效优先级相同,则应该选择等待时间最久的;
  • 优先级调度中需要处理优先级倒置问题(可以通过优先级捐赠的方法解决);

实验关键代码

变量声明

  • PriorityQueue类
protected KThread lockHolder = null; //队列头
protected LinkedList<KThread> waitList = new LinkedList<KThread>();12
  • ThreadState类
protected int effectivePriority = -2;//有效优先级初始化为-2
protected final int invalidPriority = -1;//无效优先级
protected HashSet<nachos.threads.PriorityScheduler.PriorityQueue> acquired = new HashSet<nachos.threads.PriorityScheduler.PriorityQueue>();//等待该线程的所有优先队列(每个优先队列里有等待线程),包括等待锁,等待join方法的队列

实现方法

PriorityQueue类
  • waitForAccess()方法
public void waitForAccess(KThread thread) {
        Lib.assertTrue(Machine.interrupt().disabled());
        getThreadState(thread).waitForAccess(this);
}
  • acquire(KThread thread)方法
public void acquire(KThread thread) {
        Lib.assertTrue(Machine.interrupt().disabled());
        getThreadState(thread).acquire(this);
}
  • nextThread()方法
public KThread nextThread() {
        Lib.assertTrue(Machine.interrupt().disabled());
        ThreadState x = pickNextThread();//下一个选择的线程
        if(x == null)//如果为null,则返回null
            return null;
        KThread thread = x.thread;
        getThreadState(thread).acquire(this);//将得到的线程改为this线程队列的队列头
        return thread;//将该线程返回
    }

  • pickNextThread()方法
protected ThreadState pickNextThread() {
    java.util.Iterator i = waitList.iterator();
    KThread nextthread;
    if(i.hasNext()){
    nextthread = (KThread)i.next();//取出下一个线程
    //System.out.println(nextthread.getName());
    KThread x = null;
    while(i.hasNext()){//比较线程的有效优先级,选出最大的,如果优先级相同,则选择等待时间最长的
        x = (KThread)i.next();
        //System.out.println(x.getName());
        int a = getThreadState(nextthread).getEffectivePriority();
        int b = getThreadState(x).getEffectivePriority();
        if(a<b){
                nextthread = x;     
        }
    }
    return getThreadState(nextthread);
    }else 
    return null;
}


ThreadState类
  • getEffectivePriority()方法
public int getEffectivePriority() {
    Lib.assertTrue(Machine.interrupt().disabled());
    if(effectivePriority == invalidPriority&&!acquired.isEmpty()){
        effectivePriority = priority;//先将自己的优先级赋给有效优先级
        for(Iterator i = acquired.iterator();i.hasNext();){//比较acquired中的所有等待队列中的所有线程的优先级
            for(Iterator j = ((PriorityQueue)i.next()).waitList.iterator();j.hasNext();){
                ThreadState ts = getThreadState((KThread)j.next());
                if(ts.priority>effectivePriority){
                    effectivePriority = ts.priority;

                }
            }
        }

        return effectivePriority;
    }else{ 

        if(effectivePriority==-2){ //表明该优先级线程队列不存在优先级捐赠
            return priority;
        }
         return effectivePriority;//如果该线程没有执行,那么它之前算的有效优先级不必重新再算一遍
    }
}

  • waitForAccess(PriorityQueue waitQueue)方法
public void waitForAccess(PriorityQueue waitQueue) {
    waitQueue.waitList.add(this.thread);//将调用线程加入到等待队列
}
  • acquire(PriorityQueue waitQueue)方法
public void acquire(PriorityQueue waitQueue) {
    waitQueue.waitList.remove(this.thread);//如果这个队列中存在该线程,删除
    waitQueue.lockHolder = this.thread;//对于readyQueue来讲,lockHolder为执行线程;对于Lock类的waitQueue来讲,lockHolder为持锁者;对于waitForJoin队列来讲,lockHolder为执行join方法的线程。
    if(waitQueue.transferPriority){//如果存在优先级翻转,则执行下面操作
        this.effectivePriority = invalidPriority;
        acquired.add(waitQueue);//将等待该线程的队列加入该线程的等待队列集合中
    }
}

实验测试代码

private static class PingTest implements Runnable {
    PingTest(int which) {
        this.which = which;
    }

    public void run() {
        for (int i=0; i<5; i++) {
        System.out.println("*** thread " + which + " looped "
                   + i + " times");
        KThread.currentThread().yield();
        }
    }

    private int which;
    }
public static void PriorityTest(){
    boolean status = Machine.interrupt().disable();//关中断,setPriority()函数中要求关中断
    final KThread a = new KThread(new PingTest(1)).setName("thread1");
    new PriorityScheduler().setPriority(a,2);
    System.out.println("thread1的优先级为:"+new PriorityScheduler().getThreadState(a).priority);
    KThread b = new KThread(new PingTest(2)).setName("thread2");
    new PriorityScheduler().setPriority(b,4);
    System.out.println("thread2的优先级为:"+new PriorityScheduler().getThreadState(b).priority);
    KThread c = new KThread(new Runnable(){
        public void run(){
            for (int i=0; i<5; i++) {
                if(i==2) 
                    a.join();
                System.out.println("*** thread 3 looped "
                           + i + " times");
                KThread.currentThread().yield();
            }
        }
    }).setName("thread3");
    new PriorityScheduler().setPriority(c,6);
    System.out.println("thread3的优先级为:"+new PriorityScheduler().getThreadState(c).priority);
    a.fork();
    b.fork();
    c.fork();
    Machine.interrupt().restore(status);
}

代码分析

思路分析

所谓线程调度,以我的个人理解,核心就是怎样选择就绪队列中下一个要执行的线程。本实验通过有效优先级比较,选择有效优先级高的为下一个执行的线程;除此之外,本实验要考虑优先级倒置的问题,优先级倒置问题可以通过捐献优先级来解决。

方法解释

名词解释
  • 队列头(lockHolder):每个队列都有一个队列头,该队列头不在队列里,但只有这个队列头先执行,队列里的线程才有可能执行。
  • 无效优先级(invalidPriority):每次调用该方法acquire(PriorityQueue waitQueue)时,this线程的有效优先级就会设置为无效优先级;
PriorityQueue类
  • void waitForAccess(KThread thread)方法

    将参数中线程加入到this队列中,代码实现中调用了ThreadState类中的waitForAccess(PriorityQueue waitQueue)方法。

  • void acquire(KThread thread)方法

    方法解释

    激活this队列(参数中的线程会成为this队列的队列头,但并不在队列里面)

    代码实现解释

    内部代码调用了ThreadState类中的acquire(KThread thread)方法;

    使用范围

    这个方法仅仅当this队列中没有元素时才可能调用。比如当一个线程正在申请没有线程正在等待的锁时,会调用该方法。

    查看代码会发现,有三个地方使用了该方法:
    1、 第一处是readyQueue,当nachos系统第一个线程执行时,会调用readyQueue.acquire(this);
    2、第二处是当调用锁的acquire方法时,如果该锁没有持锁者(即没有线程等待该锁时)会调用;
    3、第三处为KThread的join()方法,当调用join()方法时,会执行waitForJoinQueue.acquire(this);

  • KThread nextThread()方法

    方法解释

    决定this队列中下一个最可能执行的线程【如果this是readyQueue,则选择是下一个执行的线程】,最可能执行的线程是队列中有效优先级最高的线程。

    代码实现解释

    首先调用pickNextThread()方法来得到this队列下一个最可能执行的线程,如果返回为空,则该方法也返回空;如果不为空,则将返回的线程(假设线程A)设置为该队列的队列头【调用ThreadState类中的acquire(PriorityQueue q)方法】,最后返回线程A即可。

  • ThreadState pickNextThread()方法

    方法解释:选择下一个有效优先级最高的线程;

    代码实现解释

    扫描this队列中的每一个线程,通过比较每个线程的有效优先级,来选出最大有效优先级对应的线程。根据题意如果有效优先级相同,则应该选择等待时间最长的线程。对于这一点,我没有设置每个线程的等待时间,因为我的线程队列的底层实现是LinkedList,所以相同有效优先级,只要优先选择该链表前面的线程即可解决这个问题。(在代码中是这样实现的,只有当后一个线程的有效优先级大于前一个线程的有效优先级,变量nextThread的值才会改变)。

ThreadState类
  • int getEffectivePriority()方法

    方法解释

    该方法通过比较this线程的acquired中所有线程的优先级,将最高优先级捐献给this线程。【acquired为一个HashSet容器,该容器中装有所有等待该线程的队列,这些队列包括锁队列(如果该线程持有几把锁,那么等待该锁的线程队列便会在里面),waitForJoin队列(如果A线程join了该线程,则A线程会进入该线程waitForJoin队列)等,队列里放着等待的线程】

    代码实现解释

    通过this线程有效优先级的值来进行分支处理。

    为了防止每次都对等待this线程的容器进行遍历,进行简单优化:

    1、 如果线程的有效优先级等于invalidPriority且acquired容器不为空,则开始扫描容器中的每一个线程,将线程中最大的优先级赋给this线程的有效优先级,然后返回该有效优先级;
    2、如果this线程的有效优先级等于-2(每个线程的初始有效优先级均为-2),证明所比较的队列不允许优先级捐赠,则返回this线程的优先级即可;
    3、如果this线程的有效优先级不是invalidPriority,也不是-2,即证明this线程的有效优先级之前已经计算过,直接返回该线程的有效优先级即可。

  • void waitForAccess(PriorityQueue waitQueue)方法

    将this线程加入到waitQueue中。

  • void acquire(PriorityQueue waitQueue)方法

    方法解释:设置waitQueue的队列头为this线程。【在代码实现中队列头为lockHolder】

    使用范围: 只有两处使用到该方法
    a、PriorityQueue类中acquire(KThread thread)方法调用了该方法;
    b、PriorityQueue类中nextThread()方法调用了该方法。

    代码实现解释

    先将this线程从waitQueue中移除(队列头不能在队列里),在代码中使用remove(this)方法,如果this线程本来就不在waitQueue队列中则返回false,否则返回true;

    然后将队列的队列头更换为this线程;判断waitQueue队列是否允许优先级捐赠,如果允许,则将this线程的有效优先级设置为无效优先级,并将该队列加入到acquired容器中。否则不作处理【即如果不允许优先级捐赠,则有效优先级仍然为初始值-2】。

关键点和难点

  • 关键点

    理解线程调度的概念;
    
    
  • 难点

    理解PriorityQueue类中acquire方法和ThreadState类中的acquire方法的区别;
    
    怎样来保存等待线程的那些队列,在本实验中采取了HashSet容器;
    
    如何做到避免重复计算有效优先级,通过设置无效优先级来处理;
    
    如何让不允许优先级捐赠的队列避免计算有效优先级,通过设置有效优先级的初始值可以解决;
    
    理解优先级倒置问题的核心,并理解怎样解决。
    
    

测试结果分析

  • 测试结果解释

    使用join()方法来对编写的程序进行测试。

    首先给三个线程分别设置了优先级为2,4,6;如果不执行join()方法,必定会按照优先级高低顺序执行(线程3先执行,然后执行线程2,最后执行线程1);但在测试程序中当优先级为6的线程(线程3)执行到第3次时,调用了a.join()方法;如果没有发生优先级捐赠的话,线程2应该先执行,因为它的优先级是4,但由于发生了优先级捐赠,线程3将优先级捐赠给了线程1,所以线程1的有效优先级现在是6,故结果中出现了线程1先执行,然后线程3继续执行,最后线程2才执行。

Boat Program

  • 实验要求
  • 实验关键代码
  • 关键代码分析
  • 实验测试代码
  • 测试结果

实验要求

• A number of Hawaiian adults and children are trying to get from Oahu to Molokai. Unfortunately, they have only one boat which can carry maximally two children or one adult. The boat can be rowed back to Oahu, but it requires a pilot to do so
• Arrange a solution to transfer everyone from Oahu to Molokai. You may assume that there are at least two children
• The method Boat.begin() should fork off a thread for each child or adult—–parent thread
• Your mechanism cannot rely on knowing how many children or adults are present beforehand
• To show that the trip is properly synchronized, make calls to the appropriate BoatGrader methods every time someone crosses the channel
• When a child pilots the boat from Oahu to Molokai, call ChildRowToMolokai. When a child rides as a passenger from Oahu to Molokai, call ChildRideToMolokai
• Make sure that when a boat with two people on it crosses, the pilot calls the …RowTo… method before the passenger calls the … RideTo … method
• Your solution must have no busy waiting, and it must eventually end
• The idea behind this task is to use independent threads to solve a problem
• You are to program the logic that a child or an adult would follow if that person were in this situation.
• It is reasonable to allow a person to see how many children or adults are on the same island they are on
• A person could see whether the boat is at their island
• A person can know which island they are on
• All of this information may be stored with each individual thread or in shared variables
• So a counter that holds the number of children on Oahu would be allowed, so long as only threads that represent people on Oahu could access it

实验关键代码

关键代码在Boat类中

  • 变量声明
static BoatGrader bg;
static boolean boatInO;
static int num_children_O;
static int num_alduts_O;
static int num_children_M;
static int num_alduts_M;
static Lock lock;
static Condition children_condition_o;
static Condition children_condition_m;
static Condition alduts_condition_o; 
static boolean gameover;
static boolean is_pilot;
static boolean is_adult_go;
  • 方法实现
public static void begin( int adults, int children, BoatGrader b )
    {
    bg = b;
num_children_O=children;
num_alduts_O = adults;
num_alduts_M = 0;
num_children_M = 0;
boatInO = true;
lock = new Lock();
children_condition_o = new Condition(lock);
children_condition_m = new Condition(lock);
alduts_condition_o = new Condition(lock);
gameover = false;
is_pilot = true;
is_adult_go = false;
for(int i = 0;i<adults;i++){//每个大人为一个线程
    new KThread(new Runnable(){
        public void run(){
            AdultItinerary();
        }
    }).fork();;
}

for(int i = 0;i<children;i++){//每个小孩为一个线程
    new KThread(new Runnable(){
        public void run(){
            ChildItinerary();
        }
    }).fork();;
}
}


static void AdultItinerary(){
    bg.initializeAdult(); 
    lock.acquire();//申请锁
    if(!(is_adult_go&&boatInO)){//如果大人不走,或者船不在O岛,则大人睡眠
        alduts_condition_o.sleep();
    }
    bg.AdultRowToMolokai();//否则大人划至M岛
    num_alduts_M++;//M岛的大人数量+1
    num_alduts_O--;//O岛的大人数量—1
    //is_adult_go = false;
    boatInO = false;//船改至M岛
    children_condition_m.wake();//唤醒M岛的孩子线程
    is_adult_go = false;//下一次船再到O岛时,必定是小孩走
    lock.release();//释放锁
}


static void ChildItinerary(){
    bg.initializeChild(); 
    lock.acquire();//申请锁
    while(!gameover){
        if(boatInO){//如果船在O岛
            if(is_adult_go){//如果大人能走,则将O岛的大人线程唤醒,O岛的孩子线程睡眠
                alduts_condition_o.wake();
                children_condition_o.sleep();
            }
            if(is_pilot){//如果是第一个小孩,则设为舵手
                bg.ChildRowToMolokai();
                num_children_O--;//O岛小孩数量-1
                num_children_M++;//M岛小孩数+1
                is_pilot = false;//将舵手设为false
                children_condition_o.wake();//唤醒O岛的其他小孩线程
                children_condition_m.sleep();//让自己睡眠在M岛
            }else{//如果是第二个小孩,则设为游客

                bg.ChildRideToMolokai();
                boatInO = false;//将船改为在M岛
                //is_on_O = false;
                num_children_O--;//O岛的小孩数量-1
                num_children_M++;//M岛的小孩数量+1
                is_pilot=true;//将舵手设为true
                if(num_alduts_O==0&&num_children_O==0){//如果O岛的孩子和大人数量均为0,则游戏结束
                    gameover = true;
                }
                if(gameover){//如果游戏结束,则打印成功过河
                    System.out.println("成功过河!!!");
                    children_condition_o.sleep();
                }
                if(num_alduts_O!=0&&num_children_O==0){//如果O岛的大人还有,但小孩线程为0,则大人可走
                    is_adult_go = true;
                }
                children_condition_m.wake();//将M岛的其他孩子线程唤醒
                children_condition_m.sleep();//将自己睡眠在M岛
            }
        }else{//如果船在M岛
            bg.ChildRowToOahu();
            //is_on_O = true;
            boatInO = true;//设置船在O岛
            num_children_O ++;//O岛孩子数量+1
            num_children_M --;//M岛孩子线程数量-1
            if(is_adult_go){//如果大人可以走,则将O岛的大人线程唤醒
                alduts_condition_o.wake();
            }else{//否则,唤醒O岛的孩子线程
                children_condition_o.wake();
            }
            children_condition_o.sleep();//让自己睡眠在O岛
        }

    }
    lock.release();//释放锁
    }

实验测试代码

public static void selfTest()
    {
    BoatGrader b = new BoatGrader();

//  System.out.println("\n ***Testing Boats with only 2 children***");
//  begin(0, 2, b);

    System.out.println("\n ***Testing Boats with 2 children, 1 adult***");
    begin(1, 2, b);

//      System.out.println("\n ***Testing Boats with 3 children, 3 adults***");
//      begin(3, 3, b);
}

关键代码分析

  • 思路分析

    该task最重要的是读懂题意,想清楚过河策略。在我的代码实现中,采取的策略很简单,一句话概括就是:让孩子尽可能的过河。
    
    下面我来解释一下这句话。只要O岛孩子够两个,就过河,否则,让大人过河;而在如果船在M岛更简单,只能孩子回来。孩子回来之后看0岛小孩人数,如果够两个,这两个孩子走,否则,大人过河,再回来一个孩子。如此反复,知道O岛没有小孩也没有大人,则游戏结束。
    
    
  • 方法解释
    该task主要需要编写三个方法,我来一一解释:

1、void begin()方法
   在该方法中,主要需要做两件事,一、将全局变量实例化;二、为每个大人或小孩创建线程,并将线程启动。

2、void AdultItinerary()方法
    该方法是大人线程启动后调用的方法,核心是一个分支。
    如果满足大人过河的条件,则将相关变量进行更改,比如更改M岛和O岛的大人数量(num_alduts_M++;num_alduts_O--;),更改船的位置(boatInO = false;),更改大人可否过河的标记(is_adult_go = false;),并在方法最后唤醒M岛的孩子线程(children_condition_m.wake());
    如果不满足大人过河的条件,则将该大人线程睡眠在O岛,在睡眠前将O岛的孩子线程唤醒。
    
3、void ChildItinerary()方法
      该方法是小孩线程启动后调用的方法。该方法较上一个方法而言,更为复杂一点。因为孩子在O岛和M岛均可过河,同时,过河时为两个人,一个为舵手,一个为游客。
      由于孩子线程在M岛仍有可能过河(返回O岛),所以孩子线程需要在一个循环中,在我的代码中使用while(!gameover)来实现。
      进入循环后有一个分支,如果船在O岛,则判断大人过河条件是否满足,若满足,将大人线程唤醒,然后将该孩子线程睡眠在O岛。否则两个孩子过河。
      如果船在M岛,则一个孩子过河返回O岛。更改相关变量:船的位置(boatInO = true;);O岛和M岛的孩子数量(num_children_O ++;num_children_M --;);然后判断如果满足大人过河的条件,则将大人线程唤醒,否则唤醒O岛的孩子线程,然后将该线程睡眠在O岛。
      其中两个孩子过河需要判断谁是舵手,谁是游客,在我的代码实现中,采用第一个唤醒的孩子线程为舵手,第二个为游客。  
  • 8
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值