欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

操作系统编程实践课程设计报告

程序员文章站 2022-06-24 16:59:47
...

引言

本学期的操作系统编程实践主要是讲多线程编程实现进程的同步和利用编程实现生产者-消费者问题。

目前,许多流行的多任务操作系统都提供有线程机制,线程就是程序中的单个顺序控制流。利用多线程进行程序设计,就是将一个程序或者说是一个进程的任务划分为执行的多个部分,也就是线程。每一个线程为一个顺序的单控流,而所有的线程都是并发执行的,这样,多线程程序就可以实现并行计算,高效利用多处理器。线程可分为用户级线程和内核级线程两种基本类型。用户级线程不需要内核支持,可以在用户程序中实现,线程调度、同步与互斥都需要用户程序自己完成。内核级线程需要内核参与,由内核参与,有内核完成线程调度并提供相应的系统调用,用户程序可以通过这些接口函数对线程进行一定的控制和管理。

关于多线程问题,多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用率来提高系统的效率。线程是在同一时间需要完成多项任务的时候实现的。

最简单的比喻多线程就像火车的每一节车厢,而进程则是火车。车厢离开火车是无法跑动的,同理火车也不能只有一节车厢。多线程的出现就是为了提高效率。

 

2018.07.05

 

 

一、线程的创建与启动

1、线程与进程

(1) 进程:所谓的进程就是指在计算机系统中能独立运行并作为资源分配的基本单位,它是由一组机器指令、数据和堆栈等组成,是一个能独立运行的活动实体。

(2) 线程:通常一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没用存在的意义。线程可以利用进程所拥有的资源,在引入线程的操作系统中通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序见并发执行的程度。

(3) 进程和线程的区别:进程和线程的主要区别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

① 简而言之,一个程序至少有一个进程,一个进程至少有一个线程。

② 线程的划分尺度小于进程,使得多线程程序的并发性高。

③ 另外,进程在执行工程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

④ 线程中执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立运行,必须已存在应用程序中,由应用程序提供多个线程执行控制。

⑤ 从逻辑角度来看,多线程的意义在于应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度个管理以及资源分配。

2、Java中的Thread类和Runnable

Thread类和Runnable类是Java中创建线程的两种方式,即通过集成Thread类,重写Threadrun()方法,将线程运行的逻辑放在其中,另外一种是通过Runnable接口实例化Thread类。其实在接触这两种方法后发现,这完全是两个不同的实现多线程,一个是多个线程分别完成自己的任务,一个是多个线程共同完成一个任务。其实在实现一个任务用多个线程来做也可以用继承Thread类来实现知识比较麻烦,一般我们用实现Runnable接口来实现就比较简明了。大多数情况下,如果只想重写run()方法,而不重写其他Thread方法,那么应使用Runnable接口。

3、三种创建线程的方法

(1) 通过Runnable接口创建线程类:定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体;创建Runnable实现类的实例,并以此实例作为Threadtarget来创建Thread对象,该Thread对象才是真正的线程对象;同时,调用start()方法启动线程。话不多说,上代码:

package org.yang;
/**
 * Runnable的实现类,是线程的执行的主体
 * run函数是入口
 * @author Yang
 *
 */
class MyRunnable implements Runnable{

	private String msg;
	public MyRunnable(String msg) {
		this.msg = msg;
		
	}
	@Override
	public void run() {//线程的入口
		while(true) {
			try {
				Thread.sleep(1000);
				System.out.println(msg);
			} catch (InterruptedException e) {
				e.printStackTrace();
				break;
			}
		}
	}
	
}

public class TestThread {
	public static void main(String[] args) {
		Thread thread1 = new Thread(new MyRunnable("Hello"));//创建一个线程
		thread1.start();//启动线程

		Thread thread2 = new Thread(new MyRunnable("yang"));//创建一个线程
		thread2.start();//启动线程
	}
}

(2) 通过Runnable创建匿名类来实现线程的实例:创建一个Runnable的匿名类,并重写Run()方法;创建Thread子类的实例,即创建了线程的对象;调用线程对象的start()方法启动线程。来,上代码:

package org.yang;

public class TestThread2 {
	public static void main(String[] args) {
		Runnable runnable = new Runnable() {  //匿名类  引用就是指针
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				while(true) {
					try {
						Thread.sleep(1000);
						System.out.println("yang!");
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
						break;
					}				
				}
			}
		};
		Thread thread = new Thread(runnable);
		thread.start();
	}
}

(3) 通过lamda表达式实现:

package org.yang;

public class TestThread3 {
	public static void main(String[] args) {
//		new Thread(new Runnable() {
//			
//			@Override
//			public void run() {
//				while(true) {
//					try {
//						Thread.sleep(1000);
//						System.out.println("yang!");
//					} catch (InterruptedException e) {
//						// TODO Auto-generated catch block
//						e.printStackTrace();
//						break;
//					}				
//				}
//			}
//		}).start();
		//lamda表达式 (java 1.8+)
		new Thread(()-> {
			while(true) {
				try {
					Thread.sleep(1000);
					System.out.println("yang!");
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
					break;
				}				
			}
		}).start();
	}

}

二、线程的简单同步(同步块)

1、同步的概念和必要性(为什么要同步?可以举例说明)

(1) 同步的概念:同步,实际上就是让一个进程停下脚步处于等待状态直到另一个进程向它发出继续执行的信号后,两个进程再继续并发执行的一种通信机制。关于线程的同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回结果,同时其它线程也不能调用这个方法。

(1) 必要性:线程同步就是线程排队。同步就是排队。线程同步的目的就是避免线程“同步”执行。只有共享资源的读写访问才需要同步。如果不是共享资源,那么就根本没有同步的必要。 只有“变量”才需要同步访问。如果共享的资源是固定不变的,那么就相当于“常量”,线程同时读取常量也不需要同步。至少一个线程修改共享资源,这样的情况下,线程之间就需要同步。 多个线程访问共享资源的代码有可能是同一份代码,也有可能是不同的代码;无论是否执行同一份代码,只要这些线程的代码访问同一份可变的共享资源,这些线程之间就需要同步。

2、synchronize关键字和同步块

SynchronizeJava中的关键字,是一种同步锁。它修饰的对象有以下几种:

(1) 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这和代码块的对象;

(2) 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;

(3) 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;

(4) 修饰一个类,其作用的范围是synchronize后面括号括起来的部分,作用主对象是这个类的所有对象。

关于Java同步块

Java同步块(synchronize block)用来标记方法或者代码块是同步的。Java同步块用来避免竞争。Java中的同步块用synchronize标记。同步块在Java中是同步在某个对象上,所有同步在一个对象上的同步块同时只能被一个线程进入并执行操作。所有其他等待进入该同步块的线程将被阻塞,直到执行该同步块中的线程退出。

3、实例(话不多说,上代码)

package org.yang;
/**
 * 如何使用同步块,来实现同步操作
 * @author Yang
 *
 */
public class TextSync {
	static int c = 0;
	static Object lock = new Object();//1、随便建立一个变量,作为锁变量
	public static void main(String[] args) {
		Thread[] threads = new Thread[1000];
		for(int i = 0;i < 1000; i++) {
			final int index = i;//4、建立一个final变量,方便在lamba中使用
			threads[i] = new Thread(() ->{
				synchronized (lock) {//2、创建一个同步块,需要一个锁
					System.out.println("thread " + index + "enter");//输出
					int a = c;//获取c的值
					a++;//将值加一
					try {//模拟复杂处理过程
						Thread.sleep((long)(Math.random()*1000));
					}catch (InterruptedException e) {
						// TODO: handle exception
						e.printStackTrace();
					}
					c = a;//存回去
					System.out.println("thread "+index+"leave");//输出
				}//3、这是块的终结
			});
			threads[i].start();//线程开始
		}
		for(int i = 0;i < 1000;i++) {
			try {
				threads[i].join();//等待thread i 完成
			}catch (InterruptedException e) {
				// TODO: handle exception
				e.printStackTrace();
			}
		}//循环后,所有线程都完成了
		System.out.println("c = "+c);//输出c的结果
	}

}

三、生产者-消费者(producer-consumer)问题

1、进程同步问题之生产者-消费者问题表述

有一群生产者进程在生产产品,并将这些产品提供给消费者进程去消费。为使生产者进程与消费者进程能并发执行,在两者之间设置了一个具有n个缓冲区的缓冲池,生产者进程将其所生产的产品放入一个缓冲区中;消费着进程可从一个缓冲区中取走产品去消费。尽管所有的生产者进程和消费者进程都是以异步方式运行的。但它们之间必须保持同步,既不允许消费者进程到一个空缓冲区去取产品,也不允许生产者进程向一个已装满产品且尚未被取走的缓冲区中投放产品。

2、进程同步问题之生产者-消费者实现思路

基本思想:信号量机制

假定在生产者和消费者之间的公用缓冲池中具有n个缓冲区,这时可利用互斥信号量实现诸进程对缓冲池的互斥使用;利用信号量emptyfull分别表示缓冲池空缓冲区和慢缓冲区的数量。又假定这些生产者和消费者互相等效,只要缓冲池未满,生产者便可将消息送入缓冲池;只要缓冲池未空,消费者便可从缓冲池中取走一个消息。

3、使用Java语言实现生产者-消费者问题的代码

(1) Queue.java

package org.yang;

import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import javax.swing.event.ListSelectionEvent;

public class Queue {//队列
	//1、建立一个锁,两个信号量
	private Lock lock = new ReentrantLock();//锁
	private Condition fullC;//信号量
	private Condition emptyC;//信号量
	private int size;
	public Queue(int size) {
		this.size = size;
		//2、为信号量赋初值
		fullC = lock.newCondition();
		emptyC = lock.newCondition();
	}
	LinkedList<Integer> list = new LinkedList<Integer>();
	/**
	 * 入队
	 * @return
	 */
	public boolean EnQueue(int data) {
		lock.lock();
		while (list.size()>=size) {
			try {
				fullC.await();
			}catch (InterruptedException e) {
				// TODO: handle exception
				lock.unlock();
				return false;
			}
			
		}
		list.addLast(data);
		emptyC.signalAll();
		lock.unlock();
		return true;
	}
//	public void EnQueue(int data) {
//		list.addLast(data);
//	}
	/**
	 * 出队
	 * @return
	 */
	public int DeQueue() {
		lock.lock();
		while (list.size() == 0) {//如果 队列为空,则等待生产者唤醒
			try {
				emptyC.await();
			} catch (InterruptedException e) {
				lock.unlock();
				return -1;
			}
			//return -1;//返回-1表示失败
		}
		int r = list.removeFirst();//获取队列头部
		fullC.signalAll();//唤醒所有生产者 
		lock.unlock();//解锁
		return r;
	}
	public boolean isFull() {
		return list.size() >= size;
	}
	public boolean isEmpty() {
		return list.size() == 0;
	}

}

(2) TestPc.java

package org.yang;

public class TestPc {
	static Queue queue = new Queue(5);
	public static void main(String[] args) {
		//创建三个生产者
		for(int i = 0;i< 3;i++) {
			final int index = i;
			new Thread(()-> {
				while(true) {
					int data = (int)(Math.random()*1000);
					System.out.printf("producer thread %d wang to EnQueue %d\n",index,data);
					queue.EnQueue(data);
					System.out.printf("producer thread %d wang to EnQueue %d success",index,data);
					sleep();//随机休息一段时间				
				}

			}).start();
		}
		//创建消费者
		for(int i = 0;i < 3;i++) {
			final int index = i;
			new Thread(()-> {
				while(true) {
					System.out.printf("customer thread %d wang to DeQueue\n",index);
					int data = queue.DeQueue();
					System.out.printf("customer thread %d wang to DeQueue %d success",index,data);
					sleep2();				
				}

			}).start();			
		}
	}
	//sleep随机时间
	public static void sleep() {
		int t = (int)(Math.random()*100);
		try {
			Thread.sleep(t);
		}catch (InterruptedException e) {
			// TODO: handle exception
			e.printStackTrace();
		}
	}
	public static void sleep2() {
		int t = (int)(Math.random()*1000);
		try {
			Thread.sleep(t);
		}catch (InterruptedException e) {
			// TODO: handle exception
			e.printStackTrace();
		}
	}
}

4、测试

(1) 当生产者能力超出消费者能力时的表现

通过调用sleep()函数产生随机数的频率来判断,当其值小的时候说明生产者生产能力高于消费者的消费能力。

//sleep随机时间
public static void sleep() {
	int t = (int)(Math.random()*100);
	try {
		Thread.sleep(t);
	}catch (InterruptedException e) {
		// TODO: handle exception
		e.printStackTrace();
	}
}

(2) 当生产者能力弱于消费者能力时的表现

通过调用sleep2()函数产生随机数的频率来判断,当其值大的时候说明生产者生产能力弱于消费者的消费能力。

public static void sleep2() {
	int t = (int)(Math.random()*1000);
	try {
		Thread.sleep(t);
	}catch (InterruptedException e) {
		// TODO: handle exception
		e.printStackTrace();
	}
}

四、总结

通过之前对操作系统课程的理论知识的学习,了解了进程,线程的同步问题,这次课程设计(编程实践)将理论知识应用起来,更明确直观的体会进程与线程的同步问题,解决了理论知识阶段学习的盲区,收获颇丰。

这次编程实践是使用Java语言进行多线程编程,Java的多线程编程自己平时几乎没用到(哈哈哈哈,知识掌握浅薄),通过这次机会感觉到多线程的魅力,也认识到自己以后也要加深Java语言的学习。