当处理器的性能的发展受到各方面因素的限制的时候,计算机产业开始用多处理器结构实现并行计算来提高计算的效率。我们使用多处理器共享存储器的方式实现了多处理器编程,也就是多核编程。当然在这样的系统结构下我们面临着各种各样的挑战,例如如何协调各个处理器之间的数据调度以及现代计算机系统固有的异步特征等等。
在接下来的一系列文章中,我将会介绍一些基础的原理以及并行程序的设计和并发程序的设计及实现,写这篇文章是对近期学习课程的总结,方便自己温故实习,感谢USTC付明老师的《多核并行计算》课程,了解更多推荐《The Art of Multiprocessor Programming, Revised Reprint》。
互斥是多处理器程序设计中常见的一种协作方式,互斥的定义:不同线程的临界区之间没有重叠。对于线程A、B以及整数j、k,或者
无死锁:如果一个线程正在尝试获得一个锁,那么总能成功地获得这个锁。若线程A调用lock()但无法获得锁,则一定存在其他的线程正在无穷次地执行临界区。
无饥饿:每一个试图获得锁的线程最终都能成功。每一个lock()调用最重都将返回。这种特性有时称为无封锁特性。
注意:无饥饿意味着无死锁。
我们通过几个算法的实现类分析:
1 . LockOne类
class LockOne implements Lock{
private boolean[] flag = new booean[2];
//线程的标识为0或1
public void lock(){
int i = ThreadID.get();// 每个线程通过调用ThreadID.get()获取自己的标识。
int j = 1-i; //若当前调用者的标识为i,则另一方为j=1-i。
flag[i] = true;
while(flag[j]){} //wait
}
public void unlock(){
int i = ThreadID.get();
flag[i] = false;
}
}
LockOne算法满足互斥特性。
证明 假设不成立,考虑每个线程在第k次(第j次)进入临界区前最后一次调用lock()方法的执行情形。
通过观察代码可以看出
writeA(flag[A] = true)->readA(flag[B]==flase)->CSA(1)
writeB(flag[B] = true)->readB(flag[A]==false)->CSB(2)
readA(flag[B]==false)->writeB(flag[B]=true)(3)
当flag[B]被设置为true,将保持不变。公式(3)必须成立,否则线程A不可能读到flag[B]到值为false。
由公式(1)-(2)和先于关系的传递性可导出公式:
writeA(flag[A] = true)->readA(flag[B]==flase)->writeB(flag[B] = true)->readB(flag[A]==false)(4)
由此可以看到,从writeA(flag[A] = true)->readB(flag[A]==false)整个过程没有对flag[]进行写操作,也就是说B线程不可能读到flag[A]==false,得到了矛盾。
**LockOne算法的缺陷:**LockOne算法在交叉执行的时候会出现死锁。若事件writeA(flag[A] = true)与writeB(flag[B] = true)在事件readA(flag[B]==flase)和readB(flag[A]==false)之前发生,那么两个线程都将陷入无穷等待。
2 . LockTwo类
class LockTwo implements Lock{
private volatile int victim;
public void lock(){
int i = ThreadID.get();
victim = i;//let the other go first
while(victim == i){} //wait
}
public void unlock(){}
}
LockTwo算法满足互斥特性。
证明 假设不成立。考虑每个线程在第k次(第j次)进入临界区前最后一次调用lock() 方法的执行情形。
通过观察代码可以看出
writeA(victim = A)->readA(victim==B)->CSA(1)
writeB(victim = B)->readB(victim==A)->CSB(2)
线程B必须在事件writeA(victim = A)和事件readA(victim==B)之间将B赋值给victim域,由假设知道这是最后一次赋值,所以有
writeA(victim = A)->writeB(victim = B)->readA(victim==B)(3)
一旦victim域被设置为B,则将保持不变,所以,随后的读操作都是返回B,这将与公式(2)矛盾。
LockTwo类存在的缺陷:当一个线程完全先于另一个线程执行的时候就会出现死锁。但是两个线程并发地执行,却是成功的。由此,我们看到LockOne与LockTwo算法彼此互补,于是将两者结合的算法Peterson算法实现了。
3 . Peterson锁
class Peterson implements Lock{
//线程本地标识,0或1
private volatile boolean[] flag = new boolean[2];
private volatile int victim;
public void lock(){
int i = ThreadID.get();
int j = 1-i;
flag[i] = true; //期望获得CS
victim = i; //谦让,让其他线程先进入CS
while(flag[j] && victim == i){};//wait
}
public void unlock(){
int i = ThreadID.get();
flag[i] = fase; //not interested
}
}
Peterson锁算法满足互斥特性。证明略。
Peterson锁算法是无饥饿的。
证明 假设不成立。假定线程A一直在执行lock()方法,那么它必定在执行while语句,等待flag[B] 被设置为false或者victim被赋值为B。
当A不能继续执行时,一种可能的情况是,B反复地进入临界区又离开临界区。若是这样,线程B一旦重新进入临界区便会将victim设为B。一旦victim被设为B,就不再改变,那么A最终肯定会从lock() 方法返回,矛盾。
因此只可能是另一种情况,线程B也陷入lock() 方法调用,等待flag[A]被设置为false或者victim被赋值为A。但是victim不可能同时被赋值为A和B,再次出现矛盾。
Peterson锁算法是无死锁的。
Peterson锁算法用Java代码实现时会出现问题,原因:编译器不支持顺序一致性(sequential consistent)。
flag[i] = true;
victim = i;
不一定按照这个顺序执行,用硬件实现顺序一致性代价太高!
1.继承Thread类
class MyThread extends Thread
{
public void run()
{
// thread body of execution
}
}
创建线程
MyThread thr1 = new MyThread();
开始执行
thr1.start();
2.实现Runnable接口
class ClassName implements Runnable
{
.....
public void run()
{
// thread body of execution
}
}
创建对象
ClassName myObject = new ClassName();
创建线程对象
Thread thr1 = new Thread( myObject );
开始执行
thr1.start();
(1)操作当前线程
// CurrentThreadDemo.java
class CurrentThreadDemo {
public static void main(String arg[]) {
Thread ct = Thread.currentThread();
ct.setName( "My Thread" );
System.out.println("Current Thread : "+ct);
try {
for(int i=5; i>0; i--) {
System.out.println(" " + i);
Thread.sleep(1000);
}
}
catch(InterruptedException e) {
System.out.println("Interrupted."); }
}
}
Run:
Current Thread : Thread[My Thread,5,main]
5
4
3
2
1
(2)创建新线程
// ThreadDemo.java
class ThreadDemo implements Runnable
{
ThreadDemo()
{
Thread ct = Thread.currentThread();
System.out.println("Current Thread : "+ct);
Thread t = new Thread(this,"Demo Thread");
t.start();
try
{
Thread.sleep(3000);
}
catch(InterruptedException e)
{
System.out.println("Interrupted.");
}
System.out.println("Exiting main thread.");
}
public void run() {
try {
for(int i=5; i>0; i--) {
System.out.println(" " + i);
Thread.sleep(1000);
} }
catch(InterruptedException e) {
System.out.println("Child interrupted.");
}
System.out.println("Exiting child thread.");
}
public static void main(String args[]) {
new ThreadDemo();
}
}
Run:
Current Thread : Thread[main,5,main]
5
4
3
Exiting main thread.
2
1
Exiting child thread.
(3)在main方法中创建两个线程,实现不同线程子类的实例。
class MyThreadA extends Thread {
public void run() { // entry point for thread
for (;;) {
System.out.println("hello world1");
}
}
}
class MyThreadB extends Thread {
public void run() { // entry point for thread
for (;;) {
System.out.println("hello world2");
}
}
}
public class Main1 {
public static void main(String [] args) {
MyThreadA t1 = new MyThreadA();
MyThreadB t2 = new MyThreadB();
t1.start();
t2.start();
// main terminates, but in Java the other threads keep running
// and hence Java program continues running
}
}
(4)java.lang.Thread
yield()
public static void yield();
-Method of java.lang.Thread
-Thread gives up CPU for other threads ready to run
class MyThread extends Thread {
private String name;
public MyThread(String name) {
this.name = name;
}
public void run() {
for (;;) {
System.out.println(name + ": hello world");
yield();
}
}
}
public class Main3 {
public static void main(String [] args) {
MyThread t1 = new MyThread("thread1");
MyThread t2 = new MyThread("thread2");
t1.start(); t2.start();
}
}
join()
public final void join();
MyThread t1 = new MyThread("thread1");
t1.start();
t1.join();
- Wait until the thread is “not alive”
- Threads that have completed are “not alive” as are threads that have not yet been started
sleep()
public static void sleep (long millis) throws InterruptedException;
- Makes the currently running thread sleep (block) for a period of time
- The thread does not lose ownership of any monitors.
- InterruptedException - if another thread has interrupted the current thread.
class MyThead extends Thread{
public void run(){
for(int i=0;i<1000;i++){
System.out.println("hello world");
}
}
}
public class Main4{
public static void main(String[] args){
MyThread t1 = new MyThread();
t1.start();
try{
t1.join();//wait for the thread to terminate
}catch(IntertuptedExcception e){
System.out.println("ERROR:Thread was interrupted");
}
System.out.println("Thread is done!");
}
}
(5)Java多线程之间数据的共享
父线程想要共享数据给子线程,子线程可以改写数据,并且父线程能够获取改写后的数据;
通过将对象实例传递给子线程的构造函数,并将对象实例保存在数据成员上。
class SharedData {
public int a = 0;
public String s = null;
public SharedData() {
a = 10;
s = "Test";
}
}
class MyThread extends Thread {
private SharedData m_data = null;
public MyThread(SharedData data) {
m_data = data;
}
public void run() {
for (;;) {
m_data.a++;
}
}
}
public class Main5 {
public static void main(String [] args) {
SharedData data = new SharedData();
MyThread t1 = new MyThread(data);
t1.start();
for (;;) {
data.a--;
}
}
}
当多个线程访问共享数据的时候,接下来的问题是如何确保数据的一致性以及多线程的同步。
并发和并行从宏观上来讲都是同时处理多路请求的概念。但并发和并行又有区别,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。
在操作系统中,并发是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
由于处理器的处理速度已经达到了极限,所以处理器开始向多核方向发展,而提高程序性能的一个最简单的方式之一就是充分利用多核处理器的计算资源。这就是并行程序的设计。