使用pthread进行编程
使用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 的参数列表。
- pthread_t 对象不是由 pthread_create 函数分配的,必须在调用 pthread_create 函数前就为 pthread_create 函数前就为 pthread_t 对象分配内存空间。
 - 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 标准会提供要给线程安全的方案:
![]()
使用pthread进行编程的更多相关文章
- pthread多线程编程的学习小结
		
pthread多线程编程的学习小结 pthread 同步3种方法: 1 mutex 2 条件变量 3 读写锁:支持多个线程同时读,或者一个线程写 程序员必上的开发者服务平台 —— DevSt ...
 - VC++6.0 下配置 pthread库2010年12月12日 星期日 13:14VC下的pthread多线程编程   转载
		
VC++6.0 下配置 pthread库2010年12月12日 星期日 13:14VC下的pthread多线程编程 转载 #include <stdio.h>#include &l ...
 - clone的fork与pthread_create创建线程有何不同&pthread多线程编程的学习小结(转)
		
进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合,这些资源在Linux中被抽 象成各种数据对象:进程控制块.虚存空间.文件系统,文件I/O.信号处理函数.所以创建一个进程的 过程就是这 ...
 - Pthread 并发编程(一)——深入剖析线程基本元素和状态
		
Pthread 并发编程(一)--深入剖析线程基本元素和状态 前言 在本篇文章当中讲主要给大家介绍 pthread 并发编程当中关于线程的基础概念,并且深入剖析进程的相关属性和设置,以及线程在内存当中 ...
 - Pthread 并发编程(二)——自底向上深入理解线程
		
Pthread 并发编程(二)--自底向上深入理解线程 前言 在本篇文章当中主要给大家介绍线程最基本的组成元素,以及在 pthread 当中给我们提供的一些线程的基本机制,因为很多语言的线程机制就是建 ...
 - C语言使用pthread多线程编程(windows系统)二
		
我们进行多线程编程,可以有多种选择,可以使用WindowsAPI,如果你在使用GTK,也可以使用GTK实现了的线程库,如果你想让你的程序有更多的移植性你最好是选择POSIX中的Pthread函数库,我 ...
 - C语言使用pthread多线程编程(windows系统)一
		
运行之前需要做一些配置: 1.下载PTHREAD的WINDOWS开发包 pthreads-w32-2-4-0-release.exe(任何一个版本均可) http://sourceware.or ...
 - linux pthread多线程编程模板
		
pthread_create() 创建线程,pthread_join()让线程一直运行下去. 链接时要加上-lpthread选项. pthread_create中, 第三个参数为线程函数,定义如下: ...
 - pthread多线程编程
		
http://blog.csdn.net/onlyou930/article/details/6755593 http://blog.csdn.net/ithomer/article/details/ ...
 
随机推荐
- Python python 五种数据类型--元组
			
# 定义一个元组 var1 = ('Hello','Python') var2 = tuple() print(type(var1)) #<class 'tuple'> print(typ ...
 - 前端学习(3)-CSS
			
一 CSS CSS是Cascading Style Sheets的缩写,层叠样式表,用来控制网页数据的显示,可以使网页的显示与数据内容分离. 二 引入方式 (1)行内式:在标记的style属性中设置C ...
 - 解决Python pip安装第三方包慢的问题
			
解决Python pip安装第三方包慢的问题 主要是修改源,国内的源有几个 阿里云 http://mirrors.aliyun.com/pypi/simple/ 中国科技大学 https://pypi ...
 - Java 程序该怎么优化?(实战篇)
			
面试官:出现了性能问题,该怎么去排查呢? 程序猿:接口响应那么慢,时间都花到哪里去了? 运维喵:为什么你的应用跑着跑着,CPU 就接近 100%? 分享一些真实生产问题排查故事,看看能否涨姿势,能否 ...
 - vue 刮刮乐功能实现
			
<template> <!--游玩区域--> <div class="panel"> <canvas id="canvas&qu ...
 - udev规则,部署Multipath
			
部署Multipath多路径环境 配置iSCSI服务 编写udev规则 配置并访问NFS共享 部署Multipath多路径环境 1 配置iSCSI服务 1.1 问题 本案例要求先搭建好一台iSCSI服 ...
 - Linux基础;Day07
			
dns服务 dns的作用:地址解析 IP -> 域名(反向) 域名 -> IP(正向) 类型 主域名服务器 负责维护一个区域的所有域名信息,是特定的所有信息的权威信息源,数据可以修改. ...
 - 使用tap、Fragment等相关相关知识点。实现类似微信的界面
			
实验结果,可以实现通过左右活动来切换不同的界面.也可以通过点击不同的下方按钮来实现切换不同的界面. 自己也添加了相关的自己编写的小页面来展示相关的效果.主要的是对于碎片Fragment对于tap的相关 ...
 - go 切片重组
			
我们已经知道切片创建的时候通常比相关数组小,例如: slice1 := make([]type, start_length, capacity) 其中 start_length 作为切片初始长度而 c ...
 - Java第十天,多态
			
多态 一.多态的定义: 一个对象拥有多种形态,这就是对象的多态性.也就是说多态针对的是对象.多态的前提是接口和继承(C++中实行多继承,不存在接口). 二.多态在代码中的形式: 父类 对象名 = ne ...