使用pthread进行编程

进程和线程

进程是一个运行程序的实例;线程像一个轻量级的进程;在一个共享内存系统中,一个进程可以有多个线程

POSIX® Threads:

即 Pthreads,是一个 Unix 系统标准;一个可以用于 C 语言的库;是多线程编程的一个 API 接口。

第一个 pthreads "hello, world"程序:

#include <stdio.h>
#include <stdlib.h>
//pthread 线程库的头文件
#include <pthread.h>

//定义线程数量
int thread_count=4;
void* Hello(void* rank);//负载函数
int main(int argc, char* argv[]) {
  pthread_t* thread_handles;
  thread_handles=(pthread_t*)malloc(thread_count*sizeof(pthread_t));

  for (int i=0; i< thread_count; i++){
           pthread_create(&thread_handles[i], NULL, Hello, (void*)i);
  }

  printf("Hello from the main thread\n");
  for (int i=0; i < thread_count; i++)
    pthread_join(thread_handles[i], NULL);
  free(thread_handles);
  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 是由程序来启动线程的,这样就需要在程序中添加相应的代码来显式启动线程,并构造能够储存信息的数据结构。

thread_handles=(pthread_t*)malloc(thread_count*sizeof(pthread_t));
//为每个线程的 pthread_t 分配内存,pthread_t 数据结构用来存储线程的专有信息,它由 pthread.h 声明

pthread_t 对象是一个不透明对象。对象存储的数据都是由系统决定的,用户级代码无法直接访问;Pthreads 标准保证 pthread_t 能够存储足够信息来标识唯一线程。

int pthread_create (pthread_t*  thread_p ,
const pthread_attr_t*  attr_p ,
void*  (*start_routine ) ( void ) ,
void*  arg_p ) ;
//第一个参数是一个指针,指向对应的 pthread_t 对象。
//第二个参数一般用 NULL 就行
//第三个参数表示该线程将要运行的函数。
//最后一个参数也是一个指针,指向传给函数 start_routine 的参数列表。
  1. pthread_t 对象不是由 pthread_create 函数分配的,必须在调用 pthread_create 函数前就为 pthread_create 函数前就为 pthread_t 对象分配内存空间。
  2. pthread_create 创建的函数:
void*  thread_function ( void*  args_p ) ;//原型

void* 可以转为任意 C 类型;args_p 可以指向任何参数;函数返回值可以是任何内容。
需要注意的是:我们为每一个线程分配不同的编号只是为了方便使用。事实上,pthread_create 创建线程并没有要求必须传递线程号,也没有要求必须要分配线程号给一个线程。

运行线程

运行 main 函数的线程一般称为主线程。所以在线程启动后有一句这样的打印:

printf("Hello from the main thread\n");

图片

同时,调用 pthread_create 所生成的线程也在运行。所以这一句的打印出现在中间。

在 pthread 中,程序员不直接控制线程在哪个核上运行。在 pthread_create 函数中,没有参数用于指定在哪个核上运行线程。线程的调度是由操作系统来做的。

停止线程

依次为每个线程调用一次 pthread_join 函数。调用一次 pthread_join 将等待 pthread_t 对象所关联的那个线程结束。

int pthread_join(pthread_t thread, void**);

第二个参数可以接受任意由 pthread_t 对象所关联的线程的那个线程产生的返回值。

矩阵向量乘法

图片

串行程序伪代码

for (i = 0; i < m; i++){
  y[i] = 0.0;
  for (j = 0; j < n; j++) 
    y[i] += A[i][j]*x[j];
}

通过把工作分配给各个线程将程序并行化。一种分配方法是将线程外层的循环分块,每个线程计算 y 的一部分。

//被分配给 y[i]的线程将执行代码
y[i] = 0.0;
for (j = 0; j < n; j++) 
  y[i] += A[i][j]∗ x[j];

并行代码

假设 A, x, y, m, n 都是全局共享变量:

void  Pth_mat_vect(void* rank){ 
  long my_rank = (long) rank; 
  int i, j;
  int local_m = m/thread_count;
  int my_first_row = my_rank∗local_m;
  int my_last_row = (my_rank+1)∗local_m − 1;

  for (i = my_first_row; i <= my_last_row; i++){ 
    y[i] = 0.0;
    for (j = 0; j < n; j++) 
      y[i] += A[i][j]∗x[j];
  }
  return NULL;
}    

临界区

估算 pi 值的例子

图片

串行运行代码

double factor = 1.0;
double sum = 0.0;
for (i = 0; i < n; i++, factor = −factor) {
  sum += factor/(2∗i+1);  
} 
pi = 4.0∗sum;

计算 pi 的线程函数

将 for 循环方块后交给各个线程处理,并将 sum 设为全局变量

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_first_i = my_n*my_rank;
  long long my_last_i = my_first_i + my_n;

  if (my first i % 2 == 0) 
    factor = 1.0;
  else 
    factor = −1.0;
  for (i = my first i; i < my last i; i++, factor = −factor){
    sum += factor/(2*i+1);
   }
   return NULL;
}

当多个线程都要访问共享变量或者共享文件这样的共享资源时,如果至少其中一个访问是更新操作,那么这些访问就可能会导致某种错误,我们称为竞争条件。因此,更新共享资源的代码段一次只能允许一个线程执行,称为临界区

忙等待

线程循环测试条件, 直到满足条件 (注意编译器可能会进行优化,使忙等待失效,最简单的措施就是关闭编译器优化选项)

y=Compute(my rank);while (flag != my rank);x = x + y;flag++;忙等待可能造成 cpu 资源的浪费,关闭编译器优化选项同样也可能降低性能。

简单的对 flag 值进行加 1 存在隐患,对 flag++的语句进行改造后的程序:

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_first_i = my_n*my_rank; long long my_last_i = my_first_i + my_n;

if (my_first_i % 2 == 0) factor = 1.0; else factor = −1.0;

for (i = my_first_i; i < my_last i; i++, factor = −factor){ while (flag != my rank); sum += factor/(2*i+1);//临界区 flag = (flag+1) % thread count; //在线程 t-1 离开临界区时,应该将 flag 值重置为 0 }

return NULL;}   循环后用临界区求全局和的函数:

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_first_i = my_n*my_rank; long long my_last_i = my_first_i + my_n;

if (my_first_i % 2 == 0) factor = 1.0; else factor = −1.0;

for (i = my_first_i; i < my_last i; i++, factor = −factor){ my_sum+=factor/(2*i+1) while (flag != my rank); sum += my_sum; flag = (flag+1) % thread count; //在线程 t-1 离开临界区时,应该将 flag 值重置为 0

return NULL;}互斥量
线程使用忙等待会持续消耗 CPU 计算资源;

互斥量是一种特殊的变量,使得同一时间只有一个线程可以访问临界区。

当一个线程在使用临界区时,保证其它线程无法访问;

Pthreads 的互斥量: pthread_mutex_t.

使用 pthread_mutex_t 前,必须由系统

int pthread_mutex_init( pthread_mutex_t∗ mutex_p, const pthread mutexattr t∗    attr_p);当一个线程使用完互斥量后,应该调用:

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 使线程等待,直到没有其他线程进入临界区。;调用~unlock 则通知系统该线程已经完成了临界区中代码的执行。

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_first_i = my_n*my_rank; long long my_last_i = my_first_i + my_n; double my_sum = 0.0;

if (my_first_i % 2 == 0) factor = 1.0; else factor = −1.0;

for (i = my first 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; }   比较忙等待和互斥量的程序性能,当线程个数少于核的个数时,两者的执行时间并没有很大差别。当线程数超过核的个数,互斥量程序的性能依旧维持不变,但是忙等待的性能就会下降。

图片

生产者-消费者同步和信号量

遇到的问题

忙等待方法可以保证线程对临界区访问的顺序,但效率不高;互斥量效率更高,但无法保证顺序;

信号量方法

信号量可以认为是一种特殊类型的 unsigned int 无符号整型变量,可以赋值为 0,1,2,3 等,一般只赋 0(对应上锁的互斥量)/1(未上锁的互斥量)。要把一个二元互斥量用作互斥量时候=,需要把信号量的值初始化为 1,即开锁状态。在要保护的临界区前调用函数 sem_wait,线程执行到 sem_wait 函数时,如果信号量为 0,线程就会被阻塞,否则减 1 后进去临界区。执行完临界区的操作后,再调用 sem_post 对信号量的值加 1,使得在 sem_wait 中阻塞的其他线程能够继续运行。

void* Send_msg(void* rank){ long my_rank = (long) rank; long dest = (my_rank + 1) % thread_count; char∗  my_msg = malloc(MSG_MAX∗sizeof(char)); sprintf(my_msg, "Hello to %ld from %ld", dest, my_rank); messages[dest] = my_msg; sem_post(&semaphores[dest]); sem_wait(&semaphores[my_rank]); printf("Thread %ld > %s n", my_rank, messages[my_rank]); return NULL;}   不同信号量的语法为:

int sem_init( sem_t∗ semaphore_p , int shared , unsigned initial_val );int sem_destroy(sem t∗ semaphore p  ); int sem post(sem t∗ semaphore p  ); int sem wait(sem t∗ semaphore p );注意:信号量不是 Pthreads 线程库的一部分,所以在使用信号量的程序开头加头文件

include <semaphore.h>以上这种一个线程需要等待另一个线程执行某种操作的同步方式,有时候称为生产者-消费者模型。

路障和条件变量

作用

使线程之间同步,并保证它们运行到了同一个位置。

没有线程可以越过设置的路障,直到所有线程都抵达这里。

使用路障来计时

图片

使用路障来调试

图片

忙等待和互斥量

使用互斥量和忙等待来实现路障的方法;

使用一个通过互斥量保护的计数器;

当计数器表明,所有线程都进入过临界区, 线程就可以离开了。

实现

图片

问题:依旧使用了忙等待,浪费 cpu 周期。

使用信号量实现路障

图片

count_sem 由于保护计数器,barrier_sem 用于阻塞已经进入路障的线程。

条件变量

一个条件变量允许停止一个线程,直到某个事件发生;

当条件被满足时,另一个线程可以激活这个线程;

条件变量总是和互斥量绑在一起。

伪代码

图片

实现

图片

读写锁

控制对一大片共享数据的访问

看一个例子:

假如有一个共享的排序链表, 对链表的操作有 Member, Insert, 和 Delete.

图片

图片

member 函数

图片

支持多线程的链表

如何在 Pthreads 中使用链表?

为了使用这个链表, 我们可以将 head_p 定义为一个全局变量,这样简化了链表的参数传递

两个线程同时访问

图片

解决方法 1:对整个链表上锁

上述操作可以通过一个互斥量来控制访问。

图片

问题

对链表的访问是串行的;

如果是 Member 操作,会浪费大量并行性;

如果是 Insert 和 Delete 操作, 则比较适合

解决方法 2:对局部上锁

这是一种细粒度的方法:

图片

问题

这使得 Member 变得很复杂;

性能会很慢, 因为每次访问一个节点的时候,都需要上锁和解锁;

互斥量也会增加系统的存储负担。

解决方法 3:Pthread 读写锁

上述两个方法都有缺陷:

第一个方案只允许同一时间一个线程访问;第二个方案只允许同一时间只有一个线程访问一个节点。

读写锁有点像互斥量,但提供两个方法;:第 1 个用来对读上锁,而第 2 个用来对写上锁;

很多线程都可以获得读锁,但只有一个线程可以获得写锁。

如果有线程获得了读锁,那么其他线程无法获得写锁。

方法

图片

线程安全性

一个代码块能够同时被多个线程调用而不产生问题,那么它是线程安全的。

eg:假设我们想对一个文件进行分词;文本由空格和字符组成。

简单方法:将文本分为很多行,然后交给不同的线程处理。通过信号量来控制对行的访问;当一个线程获得了一行后, 可以使用 strtok 来进行分词。

图片

在第一次调用时,strtok 会将字符指针缓存, 在接下来的调用中返回分隔出的词。

void  Tokenize(void  rank) long my_rank = (long) rank; int count; int next = (my_rank + 1) % thread_count; char  fg_rv; char my_line[MAX]; char  my_string; sem wait(&sems[my_rank]);//强制线程按顺序输入行 fg_rv = fgets(my_line, MAX, stdin); sem_post(&sems[next]); while (fg_rv != NULL){ printf("Thread %ld > my_line = %s", my_rank, my_line); count = 0; my_string = strtok(my_line, "\t\n"); while ( my_string != NULL ){ count++; printf("Thread %ld > string %d = %s n", my_rank, count,my_string); my_string = strtok(NULL, "\t\n"); } sem_wait(&sems[my_rank]); fg_rv = fgets(my_line, MAX, stdin);//读一行输入 sem_post(&sems[next]); }return NULL; }   正确输入和输出:

图片

单线程没有问题,多线程出错:

图片

strtok 对数据进行了缓存;下次调用,会对缓存数据进行解析;不幸的是,缓存区是共享的,而不是私有的。因此,线程 0 调用 strtok 对输入的第三行进行缓存,覆盖了原来线程 1 调用 strtok 输入输入的第二行的缓存。因此,strtok 是线程不安全的。

图片

在某些情况下, C 标准会提供要给线程安全的方案:

图片

猜你喜欢

转载自www.cnblogs.com/yiyefuyou/p/12783639.html