华侨大学计算机科学与技术学院
操作系统实验报告
进程同步
课程名称: 操作系统实验
实验项目名称: 进程同步
学 院: 计算机科学与技术学院
专业班级:
姓 名:
学 号:
目录
1.描述及需求分析... 2
1.1 实验目标... 2
1.2 问题描述... 3
1.3实验要求... 3
1.4 输入形式... 3
1.5 输出形式... 3
2实验设计... 3
3. 无信号量实现线程互斥... 4
3.1使用类以及变量的说明... 4
3.2包含的核心方法及函数解析... 4
3.3 实验结果与分析... 6
4 互斥信号量mutex实现线程互斥... 6
4.1 使用类以及变量的说明... 6
4.2 包含的核心方法及函数解析... 7
4.3 实验结果与分析... 8
5 同步信号量full empty实现线程同步... 8
5.1 实验设计... 8
5.2 使用类以及变量的说明... 9
5.3 包含的核心函数解析... 9
5.4 实验结果与分析... 10
6 同时使用信号量实现生产者-消费者的互斥与同步... 10
6.1设计思想... 10
6.2 使用类以及变量的说明... 11
6.3 包含的核心方法及函数解析... 11
6.4 实验结果与分析... 13
7实验总结... 13
8 附录... 14
无信号量实现线程互斥... 14
互斥信号量mutex实现线程互斥... 17
同步信号量full empty 实现线程同步... 18
同时使用信号量实现生产者-消费者的互斥与同步... 20
1. 理解进程同步的两种制约关系:互斥与同步。
2. 掌握利用记录型信号量解决进程同步问题的方法。
3. 加深对进程同步控制的理解。
以生产者-消费者模型为基础,在Windows环境下创建一个控制台进程(或者界面进程),在该进程中创建读者写者线程模拟生产者和消费者。写者线程写入数据,然后将数据放置在一个空缓冲区中供读者线程读取。读者线程从缓冲区中获得数据,然后释放缓冲区。当写者线程写入数据时,如果没有空缓冲区可用,那么写者线程必须等待读者线程释放出一个空缓冲区。当读者线程读取数据时,如果没有满的缓冲区,那么读入线程将被阻塞,直到新的数据被写进去。
设计并实现一个进程,该进程拥有一个生产者线程和一个消费者线程,它们使用N个不同的缓冲区(N为一个确定的数值,本实验中取N=16)。可以作如下的实验尝试,并观察和记录进程同步效果:
一个互斥信号量mutex,用以阻止生产者线程和消费者线程同时操作缓冲区列表。
一个信号量full,当生产者线程生产出一个物品时可以用它向消费者线程发出信号。
一个信号量empty,消费者线程释放出一个空缓冲区时可以用它向生产者线程发出信号。
无
【线程名】生产/消费一个产品,现库存
生产者、消费者共享一个初始为空、大小为max_size=16的缓冲区。
只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待(同步)
只有缓冲区不空时,消费者才能从中取出产品,否则必须等待(同步)
缓冲区是临界资源,各进程必须互斥地访问。(互斥)
Producer生产者类:定义线程任务类Producer实现Runnable接口,含Storage对象成员变量、无参构造、有参构造,重写run方法,run方法做的事情是线程启动时,会自动循环执行休眠一秒,调用Storage对象的produce方法,生产一个产品的操作。
Consumer消费者类:定义线程任务类Consumer实现Runnable接口,含Storage对象成员变量、无参构造,有参构造,重写run方法,run方法做的事情是线程启动时,会自动循环执行休眠一秒,调用Storage对象的consume方法,消费产品的操作。
Storage仓库类:声明两个私有成员变量:仓库最大容量MAX_SIZE = 16和仓库的存储载体数据结构为链表LinkedList,产品类型用Object模拟。该类提供了了consume和produce方法,以便上述两个任务类调用。
测试类Main:以有参构造器的方式初始化 Producer和Consumer任务对象,将任务对象交给Thread线程类处理,调用线程对象start方法启动线程。
Producer类的run()方法
当使用对象实现接口 Runnable 来创建线程时,启动该线程会导致在该单独执行的线程中调用对象的 run 方法,自动循环执行休眠一秒,调用Storage对象的produce方法,生产一个产品的操作。
Consumer类的run()方法
当使用对象实现接口 Runnable 来创建线程时,启动该线程会导致在该单独执行的线程中调用对象的 run 方法,自动循环执行休眠一秒,调用Storage对象的consume方法,消费产品的操作。
Storage类的produce()方法
如果仓库里再放一个产品会大于仓库的最大容量16的话,说明此时仓库已满,这时给出提示信息:生产者线程名仓库已满,调用父类Object的wait方法,将该线程放入阻塞队列,直到它被唤醒。当跳出while循环时,说明仓库还没有放满产品,满足生产的条件,执行list对象的 add方法,给出提示信息:生产者线程名生产一个产品,现库存为list.size,完成这些后list调用notify方法向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态。
Storage类的consume()方法
如果仓库容量为0的话,说明此时仓库已空,这时给出提示信息:消费者线程名仓库为空,调用父类Object的wait方法,将该线程放入阻塞队列,直到它被唤醒。当跳出while循环时,说明有库存,满足消费的条件,执行list对象的 remove方法,给出提示信息:消费者线程名消费一个产品,现库存为list.size,完成这些后list调用notify方法向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态。
刚开始仓库容量为0,若消费者线程先抢到锁对象list(此处共享资源作为锁对象),消费者线程顺利进入同步代码块,发现list.size长度为0,消费者线程自动释放锁,进入阻塞队列,等待被唤醒,此时生产者线程执行,顺利得到锁,判断不满足While条件,跳出,生产一个产品,唤醒其他沉睡的线程:“快起来吧,有产品可以消费了”并释放锁资源。………
当发现仓库的容量为满时,生产者线程给出提示:生产者线程名称仓库已满。然后调用wait方法,释放锁资源,该线程被阻塞,消费者线程顺利拿到锁,开始执行,判断经不满足While条件,跳出While循环,消费掉一个产品, list响应减1,给出提示:“消费者线程名称消费一个产品,现库存”,并唤醒其他线程。
测试类Main:
Thread p1 = new Thread(new Producer(storage));
Thread c1 = new Thread(new Consumer(storage));
p1.start();
c1.start();
‘new’两个任务类,交给两个线程,调用start方法,两个线程同步启动,自动执行为各自量身定制的run方法
生产者线程:睡1秒,生产,循环执行
消费者线程:睡3秒,消费,循环执行
Storage仓库类声明两个私有成员变量:
仓库最大容量MAX_SIZE = 16
仓库的存储载体数据结构为链表LinkedList
互斥信号量mutex,产品类型用Object模拟
该类提供了了consume和produce方法,以便上述两个任务类调用。
其他三个类和同上,只有库存Storage类不同,下同,不做赘述。
Storage类的produce()方法
如果仓库里再放一个产品小于16的话,说明还可以继续生产,调用add方法向仓库放入生产的产品,打印Log记录:“生产者线程名生产一个产品,现库存”由于要对临界区资源数量进行修改,且max_size=16>1,为了保证操作的原子性,在该操作执行之前,首先对信号量mutex执行P操作,最后执行mutex.release()使mutex资源数量加1
Storage类的consume()方法
如果仓库容量大于0的话,执行list.remove()语句,打印日志:“消费者消费一个产品,现库存为”,在该操作执行之前,首先对信号量mutex执行P操作,在该操作执行之前,首先对信号量mutex执行P操作,最后执行mutex.release()使mutex资源数量加1
final Semaphore mutex = new Semaphore(1);
Semaphore信号量类通常用于限制可以访问某些(物理或逻辑)资源的线程数。初始化成员变量,mutex为互斥信号量,初值为1。
mutex.acquire();
从此信号量获取许可,阻塞直到一个信号量可用或线程中断,相当于对mutex执行P操作
mutex.release();
释放许可证,将其返回到信号量,相当于对mutex执行V操作
Storage仓库类声明五个私有成员变量:
仓库最大容量MAX_SIZE = 16
仓库的存储载体list = new LinkedList
锁 Lock lock = new ReentrantLock()
仓库满的信号量 Condition full = lock.newCondition()
仓库空的信号量 private final Condition empty = lock.newCondition()
该类提供了了consume和produce方法,以便上述两个任务类调用。
Storage类的produce()方法
如果仓库里再放一个产品会大于仓库的最大容量16的话,说明此时仓库已满,这时给出提示信息:生产者线程名仓库已满,执行full.await();阻塞生产线程。如果ist.size() + 1 < MAX_SIZE),说明仓库还没有放满产品,满足生产的条件,执行list对象的 add方法,给出提示信息:生产者线程名生产一个产品,现库存为list.size,执行empty.signalAll()方法,唤醒empty.signalAll下的所有消费者线程,最后执行lock.unlock释放锁。
Storage类的consume()方法
如果仓库容量为0的话,说明此时仓库已空,这时给出提示信息:消费者线程名仓库为空,调用父类empty.await()方法,阻塞消费线程,直到它被唤醒。while循环条件不满足时,说明有库存,满足消费的条件,执行list对象的 remove方法,给出提示信息:消费者线程名消费一个产品,现库存为list.size,执行full.signalAll()方法,唤醒full.signalAll下的所有生产者线程,最后执行lock.unlock释放锁。
注:在Condition中,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll(),传统线程的通信方式,Condition都可以实现,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。
Lock在java.util.concurrent.locks包下,创建的Lock对象,使用lock.lock()获得锁,调用unlock释放锁。其实现提供了比使用方法和语句可以获得的更广泛的锁定操作。它们允许更灵活的结构,可能具有完全不同的属性,并且可能支持多个关联的 Condition 对象。
Condition对象full和empty在java.util.concurrent.locks.Condition包下,可以理解为等待队列的一个管理者,condition确保阻塞的对象按顺序被唤醒。Condition的强大之处在于它可以为多个线程间建立不同的Condition, 使用synchronized/wait()只有一个阻塞队列,notifyAll会唤起所有阻塞队列下的线程,而使用lock/condition,可以实现多个阻塞队列,signalAll只会唤起某个阻塞队列下的阻塞线程。
图 6-1 生产者消费者问题同步信号量关系
伪代码
Producer(){
While(1){
生产一个产品
P(empty)
P(mutex)
把产品放入缓冲区
V(mutex)
V(full)
}
}
Consumer(){
While(1){
P(full)
P(mutex)
从缓冲区消耗一个产品(释放缓冲区)
V(mutex)
V(empty)
消费产品
}
}
图 6-2 Storage类变量声明
该类提供了了consume和produce方法,以便上述两个任务类调用。
首先在放入产品之前判断有无空缓冲区,若empty的值减1后不为负数,说明有空的缓冲区可供生产者生产产品,对mutex异步信号量执行P操作,申请到资源进入临界区,将生产好的产品放入缓冲区,执行list对象的 add方法,给出提示信息:生产者线程名生产一个产品,现库存为list.size,做完这些后,在finally语句中执行mutex.release(),在退出去释放资源,最后full.release(),使得full的值加1,通知消费者已经有产品了,可以来消费了。
若empty的值减1后为负数,说明无多余的缓冲区可供生产者生产产品,生产者线程阻塞,开始执行消费者线程,在确定有产品后(empty-1>=0),对mutex异步信号量执行P操作,消费者开始从缓冲区拿走一个产品,执行list对象的 remove方法,给出提示信息:消费者线程名消费一个产品,在finally语句中执行mutex.release(),在退出去释放资源,最后执行empty.release(),使得empy的值加1,通知生产者已经有一个空的缓冲区了,可以生产了。
对比以上四种方式,我觉得使用异步信号量mutex和同步信号量empty和full解决生产者和消费者问题最优秀!省去了实现进程同步大量的if或者while条件判断,对共享数据的操作只需对信号量执行前P后V的操作。
注:以下三个实验代码只有Storage.java不一样,故只贴Srorage.java