我的并行计算之路(五)Pthreads共享内存编程

来源:互联网 发布:java wait notify 源碼 编辑:程序博客网 时间:2024/05/21 11:01

 共享内存,可以理解为多个CPU连接在一个内存上,内存上的数据供所有CPU访问和修改。与之对应的概念是分布式内存编程,详见我之前的博客《我的并行计算之路(一)Ubuntu 16.04下的MPI安装》。共享内存相对于分布式内存编程哟个很大的好处是数据是共享的,即所有的CPU都可以访问的到。而不是像分布式内存编程那样,当需要数据交互时,得通过进程通信来完成《我的并行计算之路(二)MPI点对点通信》、《我的并行计算之路(三)MPI集合通信》和《我的并行计算之路(四)MPI集合通信》。此外共享内存和分布式内存编程还有一个很大的不同,就是分布式内存运行的是进程,而共享内存运行的是线程。

线程可以理解为“轻量级的进程”。进程是一个可拥有资源的独立单位又是一个可以独立调度和分派的单位,因此其才成为一个能独立运行的基本单位。然而,成也萧何,败也萧何,也正是进程又拥有资源又可以被独立调度和分派,因此操作系统在进程切换的时候需要花费大量的时间和空间开销,不仅需要保存和设置寄存器内容,还需要涉及存储器管理方面的工作。因此,若能将进程的两个属性分开,由操作系统分开处理,程序的并发性会更好。亦即,对于作为调度和分派的基本单位,其不作为拥有资源的单位,以做到“轻装上阵”,减少时空开销;而作为拥有资源的基本单位,不对其做频繁的切换。这样的思想便形成了线程的概念。也就是说,线程作为调度和分派的基本单位,在切换时,只需保存和设置少量寄存器,而进程作为拥有资源的单位,保存了绝大多数的资源数据。在引入了线程的操作系统中,通常一个进程拥有若干个线程,至少也有一个线程。这些线程只拥有一点必不可少的资源,主要资源依靠访问其隶属的进程获得。此外,引入线程的另一个好处是一个进程中的多个线程具有相同的地址空间,在同步和通信的实现方面线程也比进程容易。当然,在同一个进程中的线程切换不会引起进程的切换,但从一个进程中的线程切换到另一个进程的线程仍会引起进程的切换。

我使用的是POSIX线程库,也常称为Pthreads线程库,其是类Unix系统(Linux, Mac OS等)上的标准库(C 语言),定义了一套多线程的编程的应用程序接口(API)。广泛使用的多线程库还有Java threads、Windows threads和Solaris threads。首先我们来看一下一个简单的多线程程序,该程序在各个线程中打印线程号和线程总数:

#include<bits/stdc++.h>#include<pthread.h>using namespace std;int thread_count = 0;void* hello(void* rank){long my_rank = (long)rank;printf("Hello from thread %ld of %d\n", my_rank, thread_count);return NULL;}int main(int argc, char* argv[]){long thread;pthread_t* thread_handles = NULL;thread_count = strtol(argv[1], NULL,10);thread_handles = (pthread_t*)malloc(thread_count*sizeof(pthread_t));for(thread = 0; thread < thread_count; thread++){pthread_create(&thread_handles[thread], NULL, hello, (void*)thread);}for(thread = 0; thread < thread_count; thread++){pthread_join(thread_handles[thread], NULL);}free(thread_handles);thread_handles = NULL;return 0;}
下面,详细解释一下这个程序。程序包含了pthread.h文件,是Pthreads线程库的头文件。首先定义了一个全局变量thread_count,用来记录线程数量。在Pthreads程序中,全局变量被所有线程共享,函数中的局部变量由执行该函数的线程所私有,为了避免混淆以及出现奇怪的结果,建议不要将全局变量和局部变量起一样的名字。主函数中的long thread是用来给线程编号的。函数strtol是用来将字符串转换为长整形的:

long strtol(const char*number_p,char**end_p,intbase)
返回由number_p所指向的字符串转换得到的长整形数,参数base是表达这个整数值所用的基(进位计数制),如果end_p不是NULL,它就指向number_p字符串的第一个无效字符(非数值字符)。得到了线程数量后,就要用malloc为每个线程的Pthread_t对象分配内存,Pthread_t是一个声明好的数据结构,用来存储线程的专有信息。这些线程的Pthread_t对象组成一个数组,thread_handles指向首地址。接下里就要创建线程了,使用的是pthread_create函数:

int pthread_create(Pthread_t*thread_p,const pthread_attr_t*attr_p,void*(*start_routine)(void*),void*arg_p,)
其中,第一个参数是一个指针,指向对应的Pthread_t对象(注意,Pthread_t对象在pthread_create函数调用前就要给其分配内存),第二个参数一般为NULL,第三个参数表示将要运行的函数,第四个参数表示指向第三个参数中运行函数的参数。在创建了线程后,线程就交由操作系统来进行调度和分派了,我们无法决定某个线程在某个核上运行。线程运行完成后,需要将各个线程结束,我们队每个线程调用pthread_join函数,该函数将等待对应的线程结束。

int pthread_join(Pthread_tthread,void**ret_val_p)
其中,第一个参数为一个Pthread_t对象,第二个参数可以接受对应线程的返回值。大家可以想象一下整个过程,pthread_create创建出多个线程,然后pthread_join函数将各个线程合并在一起(join)




阅读全文
0 0
原创粉丝点击