临界区

advertisement
Lecture 9
共享存储器程序设计
Programming with Shared Memory
Part 1 Heavyweight processes
重量级进程
Pthreads标准线程
Threads(线程)
Accessing shared data(访问共享数据)
Critical sections(临界区)
Shared memory multiprocessor
system( 共享存储器多处理机)
任一处理器可访问任何存储单元。
存在单一编址空间( single address space ),
即每个存储单元都由一个地址范围内的特定地址
所指定。
共享存储多处理器系统编程利用共享存储来保存
数据,所有的处理器都可访问这些数据,而不需
要把数据通过消息传递发送到目的地。
Shared Memory Programming
通常,共享存储编程比较方便,但由于需要多处
理器访问共享的数据,程序员需要小心控制这些
处理器。
随着共享存储系统,特别是多核系统multi-core
systems的出现,为它们编写程序变得越来越重
要。
Methods for Programming Shared
Memory Multiprocessors
• 使用重量级进程Using heavyweight processes.
• 使用线程Using threads. Example Pthreads, Java threads
• 用完全新的程序语言实现并行- 不流行. Example: Ada.
• 修改现有的顺序程序设计语言以构成新的并行语言
Example: UPC (http://upc.lbl.gov/ unified parallel C)
• 使用并行性编译器命令和库例compiler directives and libraries
的顺序程序设计语言 Example: OpenMP
We will look mostly at threads and OpenMP
Using Heavyweight Processes
使用重量级进程
操作系统像UNIX大都是基于进程的概念设计的.
处理器的时间被多个进程分享,处理器的使用从一个
进程切换到另一个进程. 可以在固定的时间间隔切换,
也可以是当活动的进程的执行发生停滞时切换.
Fork-join结构语句
FORK-JOIN construct
UNIX System Calls
当fork被成功调用,向子进程返回0,向父进程返回子进程ID
若fork调用失败,向父进程返回-1
UNIX System Calls
SPMD model with different code for master process and
forked slave process.
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(){
pid_t pid;
int status = -10;
pid=fork();
if(pid==0) {
printf("i am in son proc\n");
printf("pid = %d\n", getpid());//取得当前进程的PID
int t = wait(&status);
//wait会导致父进程阻塞,直到它的1个子进程结束或者父根本没有子进程。
//这个调用会返回该子进程的PID。如果有错误,返回-1
printf("t = %d\n", t);
printf("status = %d\n", status);
//子进程下面没有子进程,wait返回-1,status也未变化
exit(0);
}else {
sleep(3);// 执行挂起一段时间(毫秒)
printf("i am in father proc\n");
int t = wait(&status);
//wait(&status) 返回子进程的pid
// wait()会等待子进程执行结束,然后回过来的执行父进程
printf("t = %d\n", t);
printf("status = %d\n", status);
}
}
进程与线程的差别
进程:完全独立的程序,
有自己的变量,堆栈和存储分配。
线程:共享进程的同一的存储
空间和进程的全局变量
Pthreads线程
IEEE Portable Operating System Interface, POSIX standard.
pthread_t thread1
Detached Threads(分离线程)
一个被创建的线程(子线程)终止,不用通知它的创
建线程(父线程),父线程就不需要join
没有join的线程叫 detached threads 分离线程.
当分离线程终止时,会撤销并释放它们所占的资源
Pthreads Detached Threads
Hello World!
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define NUM_THREADS 5
void *PrintHello(void *threadid){
long tid;
tid = (long)threadid;
printf("Hello World! It's me, thread #%ld!\n", tid);
pthread_exit(NULL);
}
int main(int argc, char *argv[]){
pthread_t threads[NUM_THREADS];
int rc;
long t;
for(t=0;t<NUM_THREADS;t++){
printf("In main: creating thread %ld\n", t);
rc = pthread_create(&threads[t], NULL, PrintHello, (void *)t);
if (rc){//创建成功就返回0,否则不为0
printf("ERROR; return code from pthread_create() is %d\n", rc);
exit(-1);
}
}
pthread_exit(NULL);
}
gcc –pthread program.c –o program.exe
语句执行顺序
Single processor: Processes/threads 执行直到被阻塞.
Multiprocessor: processes/threads的指令在时间上可能交叉.
Example
Process 1
Instruction 1.1
Instruction 1.2
Instruction 1.3
Process 2
Instruction 2.1
Instruction 2.2
Instruction 2.3
Many possible orderings, e.g.:
Instruction 1.1
Instruction 1.2
Instruction 2.1
Instruction 1.3
Instruction 2.2
Instruction 2.3
assuming instructions cannot be divided into smaller steps.
Thread-Safe Routines线程安全例程
若例程同时被多个线程调用,仍都能得到正确的结果,
则例程是安全的。
那些访问共享数据的例程可能需要加以特殊的处理以
达到线程安全。
POSIX threads的例程被定义成是线程安全的。
被编译器和处理器有意重新安排代码顺序:
• Compilers to optimize performance
• Processors internally, again to optimize
performance
Compilers will re-order code prior to execution while
processors will re-order the code during
execution.
In both cases the objective is to best utilize the
available computer resources and minimize any
waiting time.
Compiler/Processor Optimizations
Compiler and processor reorder instructions to improve
performance (execution speed).
Example
Suppose one had the code:
.
.
a = b + 5;
x = y * 4;
p = x + 9;
.
.
and the processor can perform, as is usual, multiple arithmetic
operations at the same time.
Compiler/Processor Optimizations
Can reorder to:
.
.
x = y * 4;
a = b + 5;
p = x + 9;
.
.
and still be logically correct. This gives the multiply operation
longer time to complete before the result (x) is needed in the last
statement.
Very common for processors to execute machines instructions out
of order for increased speed.
Accessing Shared Data访问共享数据
访问共享数据需要小心控制。
考虑两个进程/线程,每个进程/线程都要对一个共享的数据项x
加1
读取 x, 计算x + 1, 然后把结果写入新的x
Conflict in accessing shared variable
Critical Section(临界区)
确保一次(同时)只有一个进程访问特定的共享资源的
机制,即建立临界区critical section – 一段涉及这个
共享资源的代码,并使一次只能有一个临界区的代
码被执行。
这种机制就是 “互斥” mutual exclusion.
Locks 锁机制
确保临界区最简单的互斥机制就是:“锁机制”
一个锁 :一个0-1变量, 1 表示一个进程已经进入临
界区, 0 表示临界区没有进程.
一个进程进入“没上锁”的临界区,马上上锁(防止
别的进程也同时进入没上锁的临界区,检测锁的开关
与上锁为不可中断过程),进程结束后,开锁,离开
临界区。
Control of critical sections through
busy waiting忙等待
不停地检查锁位
26
Pthreads锁例程
Lock routines
Locks implemented in Pthreads with mutually exclusive lock
variables(互斥锁变量), or “mutex” variables:
.
pthread_mutex_lock(&mutex1);
临界区;
pthread_mutex_unlock(&mutex1);
.
如果一个线程到达互斥锁,并发现互斥锁关闭,则它将等待直
到互斥锁打开;如果多个线程等待互斥锁打开,则等锁打开时,
系统会选择一个线程进入临界区。只有加互斥锁的线程才能解
锁。
Pthread mutex 初始化
pthread_mutex_t mutex1;
pthread_mutex_init(&mutex1,NULL);
Condition Variables条件变量
有些临界区需要特定的全局条件满足后才可以执行,
例如,某个变量达到特定值时。
若使用锁机制,则需要在临界区内频繁检查这个全局
变量的值是否已经达到特定值。
很耗时且无效。
引人条件变量 condition variables机制克服这个问题。
Pthread 条件变量
在主线程定义和初始化一个条件变量:
pthread_cond_t cond1;
pthread_cond_init(&cond1,NULL);
pthread_mutex_t mutex1;
pthread_mutex_init(&mutex1,NULL);
pthread_cond_wait(cond1,mutex1);
//负责将调用它的线程挂起,等待条件变量cond1的信号和互斥锁mutex1解锁
pthread_cond_signal(cond1);
//从一个线程向另一个线程发送信号来释放一个因为等待条件变量cond1而被阻塞的线程
Pthread 条件变量
Pthreads的信号和等待如下安排
Critical Sections Serializing Code
临界区序列化代码
高性能的程序应尽少使用临界区,因为它们的使用
将使代码顺序化.
假设 所有的进程刚好一起达到它们的临界区。
这些临界区将依次执行. 在这种情况下,执行时间
会变得如同在单处理器的执行时间。
图例
Deadlock死锁
当一个进程/线程P1需要由另一个进程/线程P2拥有的资源R2,而
P2反过来也需要P1拥有的资源R1时会发生死锁。
Deadlock (deadly embrace死亡拥抱)
Deadlock can also occur in a circular fashion with several
processes having a resource wanted by another.
Pthreads
trylock routine
不阻塞例程来测试一个锁是否真的锁上了:
pthread_mutex_trylock()
可以将一个未加锁的互斥锁 mutex加锁并 返回 0 ,若这个互斥
锁已经加锁了就返回 EBUSY
Program example
To sum the elements of an array, a[1000]:
int sum, a[1000];
sum = 0;
for (i = 0; i < 1000; i++)
sum = sum + a[i];
Pthreads program example
我们将创建no_thread个线程,每个线程都从list中获取数并加到
它们的本地部分和中. 当所有的数都被读取后, 这no_thread个线
程就将各自的部分和加到共享单元sum中.
各个线程通过共享单元 global_index 获取数组a[ ] 的下一个未
加元素.在这个 global_index 所指的数组元素被读取后,
global_index就会加1, 为下一个元素的获取做好准备。
结果存在单元 sum, 它将同样被共享,且用锁机制来保护对其的
访问。
#include <stdio.h>
#include <stdlib.h>
a[1000]
#include <pthread.h>
#define array_size 1000
#define no_threads 10
int a[array_size];
long sum =0;
pthread_t thread[no_threads];
pthread_mutex_t mutex1;
void *slave(void *threadid)
{
long partial_sum=0;
long tid;
tid = (long)threadid;
long i;
for(i=tid*(array_size/no_threads);i<(tid+1)*(array_size/no_threads);i++)
partial_sum += *(a+i);
printf("Partial sum of Thread #%ld is %ld\n", tid, partial_sum);
pthread_mutex_lock (&mutex1);
sum+=partial_sum;
pthread_mutex_lock (&mutex1);
pthread_exit((void*) 0);
}
int main(int argc, char *argv[])
{
long i;
void *status;
pthread_attr_t attr;
pthread_mutex_init(&mutex1,NULL);
for(i=0;i<array_size;i++)
a[i]=i+1;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
for(i=0;i<no_threads;i++)
pthread_create(&thread[i],&attr,slave, (void *)i);
pthread_attr_destroy(&attr);
for(i=0;i<no_threads;i++)
pthread_join(thread[i],&status);
printf("The sum of 1 to %i is %ld",array_size, sum);
pthread_mutex_destroy(&mutex1);
pthread_exit(NULL);
}
Download