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); }