高性能科学计算理论和方法课件:第四章PPT.ppt
- 【下载声明】
1. 本站全部试题类文档,若标题没写含答案,则无答案;标题注明含答案的文档,主观题也可能无答案。请谨慎下单,一旦售出,不予退换。
2. 本站全部PPT文档均不含视频和音频,PPT中出现的音频或视频标识(或文字)仅表示流程,实际无音频或视频文件。请谨慎下单,一旦售出,不予退换。
3. 本页资料《高性能科学计算理论和方法课件:第四章PPT.ppt》由用户(罗嗣辉)主动上传,其收益全归该用户。163文库仅提供信息存储空间,仅对该用户上传内容的表现方式做保护处理,对上传内容本身不做任何修改或编辑。 若此文所含内容侵犯了您的版权或隐私,请立即通知163文库(点击联系客服),我们立即给予删除!
4. 请根据预览情况,自愿下载本文。本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
5. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007及以上版本和PDF阅读器,压缩文件请下载最新的WinRAR软件解压。
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 性能 科学 计算 理论 方法 课件 第四 PPT
- 资源描述:
-
1、用Pthreads进行共享式内存编程共享内存系统进程n一个进程就是运行在处理器上的一个程序实例n在大多数系统中,在默认状态下,一个进程的内存块是私有的:其他进程无法直接访问,除非操作系统进行干涉。n例如:如果正在使用文档编辑器写程序(一个运行文档编辑器的进程),肯定不希望浏览器(另一个进程)覆盖文档编辑器正使用的内存数据。n这一设计在多用户环境下更为重要,一个用户的进程是绝对不允许访问其他用户进程拥有的内存的。线程n我们在运行共享内存程序时,希望有些数据对多个进程都是可用的。因此,典型的共享内存“进程”允许了进程间互相访问各自的内存区域。n它们也经常共享一些区域,例如共享对stdout的访问。
2、n事实上,除了它们各自拥有独立的栈和程序计数器外,它们基本上可以共享所有其他区域。n为了方便管理,一般采用的方法是:启动一个进程,然后由这个进程生成这些“轻量级”进程。轻量级进程由此得名。n更通用的术语是线程。线程是类似于一个“轻量级”的进程。进程和线程n一个进程就是运行在处理器上的一个程序实例n线程是类似于一个“轻量级”的进程POSIX Threadsn也经常称为Pthreads线程库n一个类似Unix操作系统(如Linux、Mac OS X)上的标准库nPthreads不是编程语言 (如C或Java),而是与MPI一样,它拥有一个可以链接到C程序中的库n它定义了一套指定用于多线程编程的应用
3、程序编程接口( API)注意nPthreads API只有在支持POSIX的系统(Linux、Mac OS X,Solaris、HPUX等)上才有效“Hello,World程序n先来看一个Pthreads程序。在程序4-1中,主函数启动了多个线程,每个线程打印一条消息,然后退出。Hello World! (1)declares the various Pthreadsfunctions, constants, types, etc.Hello World! (2)Hello World! (3)运行一个Pthreads程序. / pth_hello . / pth_hello 1Hello f
4、rom the main threadHello from thread 0 of 1. / pth_hello 4Hello from the main threadHello from thread 0 of 4Hello from thread 1 of 4Hello from thread 2 of 4Hello from thread 3 of 4准备工作n 我们仔细研究程序4-1的代码。首先,我们注意到,这个简单的c程序只有main函数和另外一个函数。n程序中包含了我们熟悉的stdio.h和stdlib.h两个头文件,也有一些新增的不同之处。n程序的第3行包含了pthread.h头
5、文件,这是Pthreads线程库的头文件,用来声明Pthreads的函数、常量和类型等。准备工作n第6行定义了一个全局变量thread_count。在Pthreads程序中,全局变量被所有线程所共享,而在函数中声明的局部变量则(通常)由执行该函数的线程所私有。n如果多个线程都要运行同一个函数,则每个线程都拥有自己的私有局部变量和函数参数的副本。全局变量n全局变量可能会在程序中引发令人困惑的错误。n一个引发错误的例子全局变量例子n例如,我们写一个有全局变量int x的程序,函数f内也有一个名为x的局部变量,但我们却忘了在函数中声明这个局部变量x。n编译时是不会有错误或者警告出现的,因为函数f有权
6、访问全局变量x。n然而在运行时,程序却输出了奇怪的结果,这是由全局变量x引起的。过了几天,我们才发现导致全局变量x有奇怪值的原因出自函数f。全局变量n根据经验法则,应该限制使用全局变量,除了确实需要用到的情况外,比如线程之间共享变量。线程数目的生成n程序中的第15行表示从命令行中读取需要生成的线程数目。n不同于MPI,Ptheads程序和普通串行程序一样,是编译完再运行。将命令行参数作为输入值传入程序,由此来确定线程的数目,不失为一种简单方便的方法,但也并不仅仅局限于这一种方法。strtol函数nstrtol函数的功能是将字符串转化为long int(长整型),它在stdlib.h中声明n它的
7、语法形式为:n它返回由number_p所指向的字符串转换得到的长整型数,参数base是表达这个整数值所用的基(进位计数制)。n如果end_p不是NULL,它就指向number_p字符串中第一个无效字符(非数值字符)。启动线程n正如我们已经提到的,Pthreads不同于MPI程序,它不是由脚本来启动的,而是直接由可执行程序启动。n这样会增加一些复杂度,因为我们需要在程序中添加相应的代码来显式地启动线程,并构造能够存储线程信息的数据结构。pthread_t对象n代码第17行为每个线程的pthread_t对象分配内存,pthread_t数据结构用来存储线程的专有信息,它由pthread.h声明。n
8、pthread_t对象是一个不透明对象。对象中存储的数据都是系统绑定的,用户级代码无法直接访问到里面的数据。pthread_t对象nPthreads标淮保证pthread_t对象中必须存有足够多的信息,足以让pthread_t对象对它所从属的线程进行唯一标识。n例如:Pthreads有一个库函数,利用这个函数可以让线程取得它的专有pthread_t对象;还有一个pthreads函数,通过检查两个线程的pthread_t对象来确定它们是否为同一个线程。启动线程n在第1921行的代码中,调用pthread_create函数来生成线程。它与大多数的Pthreads库函数一样,有一个前缀pthread
9、,它的语法为:int pthread_create (pthread_t* thread_p /* out */ ,const pthread_attr_t* attr_p /* in */ ,void* (*start_routine ) ( void ) /* in */ ,void* arg_p /* in */ ) ;pthread_create函数n第一个参数是一个指针,指向对应的pthread_t对象。注意,pthread_t对象不是由pthread_create函数分配的,必须在调用pthread_create函数前就为pthread_t对象分配内存空间。n第二个参数不用,所以只
10、是在函数调用时把NULL传递给参数。n第三个参数表示该线程将要运行的函数;最后一个参数也是一个指针,指向传给函数start_routine的参数。Pthreads函数的返回值n大多数Pthreads函数的返回值用于表示线程调用过程中是否有错误。为了减少复杂性,在本章(以及本书后面的内容)中,我们一般忽略Pthreads函数的返回值。由pthread_create生成并运行的函数n原型: void* thread_function ( void* args_p ) ;nvoid* 可以转换成C语言中任意指针类型nargs_p可以指向一个列表,该列表包含一个或者多个thread_function函
11、数需要的数值。n类似地,thread_function返回的值也可以是一个包含一个或者多个值的列表。由pthread_create生成并运行的函数n在我们的代码中,调用pthread_create函数时,传入最后一个参数采用了一个常用的技巧:为每一个线程赋予了唯一的int型参数rank,表示线程的编号。n首先,我们先解释一下这么做的理由,然后再具体探讨如何做。线程编号n考虑以下问题:运行一个生成了两个线程的Pthreads程序,当其中一个线程遇到了错误时,我们或者用户如何才能知道是哪个线程出了问题呢?我们不能简单地输出pthread_t对象,因为它是不透明的。n如果我们启动线程时赋予第一个线程
12、编号为0,第二个线程编号为1,那么通过错误信息中线程的编号就能非常容易地判断是哪个线程出错了。线程函数n既然线程函数可以接收void*类型的参数,我们就可以在main函数中为每个线程分配一个int类型的整数,并为这些整数赋予不同的数值。n当启动线程时,把指向该int型参数的指针传递给pthread_create函数。然而,程序员会用类型转换来处理此问题:不是在main函数中生成int型的进程号,而是把循环变量thread转化为void*类型,然后在线程函数hello中,把这个参数的类型转换为long型(第33行)。关于类型转换n类型转换的结果是“系统定义”的,但大多数C编译器允许这么做。n不过
13、,如果指针类型的大小和表示进程编号的整数类型不同,在编译时就会收到警告。在我们使用的机器上,指针类型是64位,而int型是32位,为了避免警告,我们用long型替代了int型。关于线程编号和线程函数n需要注意的是,我们为每一个线程分配不同的编号只是为了方便使用。事实上,pthread_create创建线程时没有要求必须传递线程号,也没有要求必须要分配线程号给一个线程。n还需要注意的是,并非规定每个线程都要运行同样的函数。一个线程运行hello函数的同时,另一个线程可以运行goodbye函数。n与编写MPI程序的方法类似,Pthreads程序也采用“单程序,多数据”的并行模式,即每个线程都执行同
14、样的线程函数,但可以在线程内用条件转移来获得不同线程有不同功能的效果。运行线程n运行main函数的线程一般称为主线程。所以,在线程启动后,会打印一句: Hello from the main threadn同时,调用pthread_create所生成的线程也在运行。这些线程通过第33行的类型转换代码获得各自的编号,然后打印各自的消息。n注意,当线程结束时,由于它的函数的类型有一个返回值,那么线程就应该返回一个值。在本例中,线程没有需要特别返回的值,所以只返回NULL。运行线程n在Pthreads中,程序员不直接控制线程在哪个核上运行。在pthread_create函数中,没有参数用于指定在哪个
15、核上运行线程。n线程的调度是由操作系统来控制的。在负载很重的系统上,所有线程可能都运行在同一个核上。事实上,如果线程个数大于核的个数,就会出现多个线程运行在一个核上。当然,如果某个核处于空闲状态,操作系统就会将一个新线程分配给这个核。停止线程n代码的第25行和第26行为每个线程调用一次pthread_join函数。调用一次pthread_join将等待pthread_t对象所关联的那个线程结束。npthread_join的语法为:n第二个参数可以接收任意由pthread_t对象所关联的那个线程产生的返回值。在我们的例子中,每个线程执行return,最后,主线程调用pthread_join等待这
16、些线程完成并最终结束。停止线程n将这个函数命名为pthread_join的原因是,这个名字常常用于多线程的图解描述。n如图4-2所示,假设主线程在图中是一条直线,调pthread_create后就创建了主函数的一条分支或派生,多次调用pthread_create就会出现多条线程分支或派生。n当pthread_create创建的线程结束时,从图4-2中可以清楚地看到,这些分支最后又合并(join)到主线程的直线中。错误检查n为了使程序紧凑易读,我们省略了许多在“真正”程序中常见而重要的细节。n例子中的程序(以及很多其他程序)发生错误的很大一部分原因是用户的输入出错或者缺少输入。因此,检查一个程序
17、时,最好一开始先检查命令行参数;允许的话,还可以检查输入的实际线程数目是否合理。n另外,检查由Pthreads函数返回的错误代码也是一个好办法,尤其是在你刚开始使用这个库,对具体的函数功能不怎么熟悉的时候。启动线程的其他方法n在我们的例子中,用户通过在命令行键入参数来决定生成多少个线程,然后由主线程来生成这些“辅助”线程。当线程运行时,主线程会打印消息,然后等待其他线程结束。这种多线程的编程方法与MPI的编程方法类似,在MPI系统中,也会启动一组进程,然后等待它们的完成。n然而,还有一种完全不同的多线程程序设计方法。在这种方法中,辅助线程根据需要而启动。举个例子,一台专门处理旧金山湾区高速公路
18、交通信息的Web服务器,假设主线程接收请求,辅助线程完成请求。平常周二的凌晨1点,可能只有很少的网络请求,但到了晚上5点,却会出现数千个请求。因此,设计Web服务器的一个很自然的做法是,请求来到之后,主线程启动辅助线程进行请求处理。Pthreads矩阵-向量乘法Matrix-Vector Multiplication in pthreads矩阵-向量乘法n让我们用Pthreads写一个矩阵-向量乘法程序。n如果A=(aij)是一个m*n的矩阵,x是一个n维列向量,矩阵-向量的乘积Ax=y是个m维的列向量。串行的伪代码并行化代码n通过把工作分配给各个线程将程序并行化。n一种分配方法是将线程外层的
19、循环分块,每个线程计算y的一部分。例如,假设m=n=6,线thread_count(或t)为3,则计算可以按下列情况分配:并行化代码n为了计算y0,线程0将执行代码:n因此,线程0需要访问矩阵A的第0行以及向量x中的每一个元素。n更一般地,被分配给yi的线程将执行代码:使用3个线程thread 0general case并行化代码n我们发现,每个线程除了访问各自分配到的矩阵A的第i行以及y分量外,还要访问X中的每个元素。这意味着最低限度下,要共享向量x。n如果矩阵A和y是全局变量,主函数就可以简单地通过读取标准输入stdin来初始化矩阵A,乘积向量y也可以很容易被主线程打印输出。并行化代码n接
20、下来,我们只要编写每一个线程的代码,确定每个线程计算哪一部分的y。n假设,m与n都能被t整除,在例子中,m=6,t=3,每个线程能分配到m/t=3行的运算数据,而且,线程0处理第一部分的m/t行,线程1处理第二部分的m/t 行,以此类推。n所以线程q处理的矩阵行是: 第一行: 最后一行:Pthreads的矩阵向量乘法与MPI对比n用MPI编写一个矩阵-向量乘法的程序工作量比较大,因为它的数据结构必须是分布式的,即每个MPI的进程只能直接访问自己的局部内存。所以,在MPI代码中,需要显式地将x分配到每一个进程的内存中。n从这个例子看,编写一个共享内存的并行程序比编写分布式内存的程序容易,但我们接
21、下来就会看到:共享内存程序也有更复杂的情况。练习n编写完整的Pthreads矩阵-向量程序n提示:临界区(Critical sections)共享变量被改写?n因为共享内存区域是较理想的存储访问方式,所以矩阵-向量乘法的代码很容易编写。n在程序初始化后,线程只读取除了y以外的所有变量。即在主函数创建线程后,除了y以外,没有任何共享变量被改写。即使是y,也是每个线程各自改变属于自己运算的那一部分,没有两个或两个以上线程共同处理同一部分y的情况。n如果情况变了会怎样?如果多个线程需要更新同一内存单元的数据会怎样?估计值估计值的并行化n我们尝试用并行化矩阵-向量乘法的方法来并行化这个程序:将for循
22、环分块后交给各个线程处理,并将sum设为全局变量。n为了简化计算,假设线程数thread_count,简称t能够整除项目总数n。一般地,对于线程q,循环变量的范围是:n注意第一项的符号。使用线程函数来计算使用双核处理器n可以看到,随着n的增加,单线程的估算结果也越来越准确。n事实上,n每增大十倍就能获得更加精确的结果。n两个线程的运算结果与n= 105时一样,然而,对于n值较大的情况,双线程的计算结果反而变糟。事实上,多次运行这个双线程程序,尽管n未变,但每次的结果都不同。n结论:当多个线程尝试更新同一个共享变量时,会出现问题。可能的竞争条件n先看一个例子,用一条C语句将存储单元y的内容加到存
23、储单元x中去: x=x+yn机器的处理过程一般比这个式子更加复杂。n因为x和y中的值都存储在计算机的主存中,无法直接进行加法运算,需要先将它们从主存中加载到CPU的寄存器中后,才能进行加法运算。当运算完成后,必须将结果再从寄存器重新存储到主存中。可能的竞争条件n假设有2个线程,每个线程对并存储在自己私有变量y中的值进行计算。还假设将这些私有变量加到共享变量x中,主线程将x的初始值置为0。n每个线程执行以下代码: y= Compute(my_rank); x=x+y;n假设线程0计算出的结果为y=1,线程1计算出的结果为y=2,则“正确”结果就应该是x=3。n但是,事情总有意外。可能的竞争条件可
24、能的竞争条件n如果在线程0存储它的结果前,线程1就将x的值从内存复制到寄存器,那么线程0计算出的结果就会被线程1的值重写。n问题也可能反过来:如果线程1先处理数据,则最后x的结果会被线程0的值重写。n事实上,除非一个线程在其他线程从内存读取x前就把它要改写的值写回内存,否则“先到者”的结果肯定会被“后来者”重写。临界区概念n这个例子反映了共享内存编程的一个基本问题:当多个线程尝试更新一个共享资源(共享变量)时,结果可能是无法预测的。n更一般地,当多个线程都要访问共享变量或共享文件这样的共享资源时,如果至少其中一个访问是更新操作,那么这些访问就可能会导致某种错误,我们称之为竞争条件( race
25、condition)。n在我们的例子中,为了使代码产生正确的结果,需要保证一旦某个线程开始执行x=x+y,其他线程在它未完成前不能执行此操作n因此,代码x=x+y就是一个临界区。临界区就是一个更新共享资源的代码段,一次允许只一个线程执行该代码段。忙等待(Busy-Waiting)n当线程0要执行x=x+y时,它需要先确认线程1此时没有在执行同样的语句.n一旦线程0确认后,它需要通过某些办法,告知线程1它目前正在执行该语句,以防止线程1在线程0的操作完成前,执行该语句而导致出错。n最后,当线程0完成操作后,也需要通过某些办法,告知线程1它已经结束了这个语句的执行,线程1此时可以安全地执行这个语句
展开阅读全文