使用Pthread进行共享内存编程
1.简介
使用POSIX线程库来实线共享内存的访问,线程大体上是轻量级的进程,进程是正在运行(或挂起)的程序的一个实例,除了可执行代码外,它还包括 栈段,堆段,系统为进程分配的资源描述符(如文件描述符),安全信息(如进程允许访问的硬件和软件资源),描述进程状态的信息(程序计数器的数值等)
典型的共享内存“进程”允许了进程间互相访问各自内存区域,事实上,除了他们各自拥有独立的栈和程序计数器外,为了方便,它们基本上可以共享所有其他区域,为了方便管理,一般的方法是启动一个进程,然后由这个进程生成这些“轻量级”进程。
“轻量级”进程更通用的术语是线程。POSIX线程库是一个类UNIX操作系统上的标准库,定义了一套多线程编程应用程序的编程接口。Pthreads的API 只有在支持POSIX的系统(Linux, Max OS X, Solaris等)上才有效。
2一个简单的Pthread程序
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
int thread_count;
void* Hello(void* rank);
int main(int argc,char* argv[])
{
long thread;
thread_count=strtol(argv[1],NULL,10);
pthread_t thread_handles[thread_count];
for(thread=0;thread<thread_count;thread++)
pthread_create(&thread_handles[thread],NULL,Hello,(void*)thread);
printf("Hello from the main thread\n");
for(thread=0;thread<thread_count;thread++)
pthread_join(thread_handles[thread],NULL);
return 0;
}
void* Hello(void* rank)
{
long my_rank=(long) rank;
printf("Hello from thread %ld of %d\n",my_rank,thread_count);
return NULL;
}
编译时链接到pthread 库即可, gcc -o hello hello.c -lpthread
运行:
./hello 5
输出类似于:输出是并不确定的,因为每个线程先后都不一定
Hello from thread 3 of 5
Hello from the main thread
Hello from thread 1 of 5
Hello from thread 2 of 5
Hello from thread 0 of 5
Hello from thread 4 of 5
程序分析:
thread_count是一个全局变量,在Pthread程序中,全局变量被所有线程所共享,全局变量可能会在程序中引发令人困惑的错误,应该限制使用全局变量的使用,
除了确实需要用到的情况外,比如线程之间共享变量。
首先要定义个pthread_t 对象,来左右线程的调用对象,然后在创建线程之前要为这个pthread_t对象分配内存空间,pthread_t 数据结构用来存储线程
专有信息,由pthread.h声明。
调用pthread_create函数来生成线程,语法为,其中in和out只是为了说明此变量为输入变量还是输出变量
int pthread_create(
pthread_t* thread_p, //out
const pthread_attr_t* attr_p, //in
void* (*start_routine)(void* arg_p) //in
void* arg_p //in
)
第一个参数是一个指针,指向对应的pthread_t 对象,必须在调用pthrad_create前就为pthread_t对象
分配内存空间;
第二个参数可以用NULL(缺省值)
第三个参数表示该线程将要运行的函数,此函数的形式通常为 void* thread_function(void* args_p)
最后一个参数是一个指针,只想传给start_routine的参数
给线程标注编号是由好处的,因为pthread_t对象是不透明的,所以不能用来输出,通过赋予线程编号,可以帮助我们了解在程序出错时是哪个线程发生了错误
运行main函数的线程一般称为主线程,没有参数用于指定线程在哪个核上运行,线程的调度是由操作系统来控制的。
调用pthread_join将等待pthread_t对象所关联的那个线程结束。
int pthread_join(
pthread_t thread , //in
void** rat_val_p //out
)
调用一次pthread_join 将等待pthread_t 对象所关联的那个线程结束
第二个参数可以接收任意由pthread_t 对象所关联的那个线程结束
关于线程启动的一些认识:
在上面的例程中是通过键入参数来决定生成多少个线程,然后由主线程来生成这些“辅助”线程。
还有一种做法是,请求到来后,主线程启动辅助线程来进行请求处理,例如WEB服务器
需要知道的是,线程的启动开销是比较大的,所以“按需启动线程”也许并不是使应用程序性能最优化的理想方法
二.临界区
当多个线程需要更新同一内存单元时,如果至少其中一个访问是更新操作,那么这些访问就可能会导致某种错误,我们称为竞争条件。
三,忙等待
使用一个共享的标识量flag,在下面的程序中,当线程数为2时,运行时间为18秒左右,当线程数目为1时,运行时间仅需要2秒,这主要是忙等待中:while(flag!=my_rank)语句,flag初始化的值为0,所以在线程0完成临界区运算并将flag加1之前,线程1必须等待,同理,当线程1进入临界区后,线程0必须等待线程1完成运算。所以,线程不停的在等待和运行之间切换,所以是非常耗时的!
int flag=0;
int n=100000000;
int thread_count=2;
void* Thread_sum(void* rank);
double sum;
int main()
{
long thread;
pthread_t thread_handles[thread_count];
clock_t start_time=clock();
for(thread=0;thread<thread_count;thread++)
pthread_create(&thread_handles[thread],NULL,Thread_sum,(void*)thread);
for(thread=0;thread<thread_count;thread++)
pthread_join(thread_handles[thread],NULL);
printf("%lf\n",sum*4);
clock_t end_time=clock();
double runtime=(double)(end_time-start_time)/CLOCKS_PER_SEC;
printf("Running time is %f S:\n",runtime);
return 0;
}
void* Thread_sum(void* rank)
{
long my_rank=(long)rank;
double factor;
long long i;
long long my_n=n/thread_count;
long long my_fisrt_i=my_n*my_rank;
long long my_last_i=my_fisrt_i+my_n;
if(my_fisrt_i%2==0)
factor=1.0;
else
factor=-1.0;
for(i=my_fisrt_i;i<my_last_i;i++,factor=-factor)
{
while(flag!=my_rank);
sum+=factor/(2*i+1);
flag=(flag+1)%thread_count;
}
return NULL;
}
忙等待不是保护临界区的唯一方法,事实上,还有很多更好的方法,然而,因为临界区中的代码一次只能由一个线程运行,所以无论如何限制访问临界区,都必须串行地执行其中的代码。如果可能的话,我们应该 执行临界区的次数。能够大幅度提高性能的一个方法是:给每个线程配置私有变量来存储各个部分的和,然后for循环一次性将所有部分和加在一起算出总和。
void* Thread_sum(void* rank)
{
long my_rank=(long)rank;
double factor,my_sum=0.0;
long long i;
long long my_n=n/thread_count;
long long my_fisrt_i=my_n*my_rank;
long long my_last_i=my_fisrt_i+my_n;
if(my_fisrt_i%2==0)
factor=1.0;
else
factor=-1.0;
for(i=my_fisrt_i;i<my_last_i;i++,factor=-factor)
{
my_sum+=factor/(2*i+1);
}
while(flag!=my_rank);
printf("this is thread %ld\n",my_rank);
sum+=my_sum;
flag=(flag+1)%thread_count;
return NULL;
}
四.互斥量
互斥量是互斥锁的简称,它是一个特殊类型变量的简称,通过某些特殊类型的函数,互斥量可以用来限制每次只有一个线程能够进入临界区。
Pthreads标准为互斥量提供了一个特殊类型:pthread_mutex_t,在使用pthread_mutex_t类型之前,必须由系统对其进行初始化,初始化函数为:
int pthread_mutex_init(
pthread_mutex_t* mutex_p,
const pthread_mutexattr_t* attr_p
)
我们不使用第二个参数,给这个参数赋值NULL即可,当一个pthread程序使用完互斥量,它应该调用
int pthread_mutex_destroy(
pthread_mutex_t* mutex_p
)
要获得临界区的访问权,线程需要调用
int pthread_mutex_lock(
pthread_mutex_t * mutex_p
)
当线程退出临界区后,线程应该调用:
int pthread_mutex_unlock(
pthread_mutex_t* mutex_p
)
通过声明一个全局的互斥量,可以在全局求和的程序中用互斥量来代替忙等待,主线程对互斥量进行初始化,当线程进入临界区前调用pthread_mutex_lock,在执行完临界区中的所有操作后调用pthread_mutex_unlock.。第一个调用pthrad_mutex_lock的线程
会为临界区上锁,其他线程如果想要进入临界区,也需要调用pthread_mutex_lock,这些调用了pthread_mutex_lock的线程都会阻塞并等待,直到第一个线程调用了pthread_mutex_unlock离开临界区
long n=500000000;
int thread_count=10;
void* Thread_sum(void* rank);
double sum;
pthread_mutex_t mutex;
int main()
{
long thread;
pthread_mutex_init(&mutex,NULL);
pthread_t thread_handles[thread_count];
clock_t start_time=clock();
for(thread=0;thread<thread_count;thread++)
pthread_create(&thread_handles[thread],NULL,Thread_sum,(void*)thread);
for(thread=0;thread<thread_count;thread++)
pthread_join(thread_handles[thread],NULL);
printf("%lf\n",sum*4);
clock_t end_time=clock();
double runtime=(double)(end_time-start_time)/CLOCKS_PER_SEC;
printf("Running time is %f S:\n",runtime);
return 0;
}
void* Thread_sum(void* rank)
{
long my_rank=(long)rank;
double factor,my_sum=0.0;
long long i;
long long my_n=n/thread_count;
long long my_fisrt_i=my_n*my_rank;
long long my_last_i=my_fisrt_i+my_n;
if(my_fisrt_i%2==0)
factor=1.0;
else
factor=-1.0;
for(i=my_fisrt_i;i<my_last_i;i++,factor=-factor)
my_sum+=factor/(2*i+1);
pthread_mutex_lock(&mutex);
sum+=my_sum;
pthread_mutex_unlock(&mutex);
return NULL;
}
在使用互斥量的多线程程序中,多个线程进入临界区的顺序是随机的,线程顺序由系统负责分配