使用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 标准会提供要给线程安全的方案:

使用pthread进行编程的更多相关文章

  1. pthread多线程编程的学习小结

    pthread多线程编程的学习小结  pthread 同步3种方法: 1 mutex 2 条件变量 3 读写锁:支持多个线程同时读,或者一个线程写     程序员必上的开发者服务平台 —— DevSt ...

  2. 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 ...

  3. clone的fork与pthread_create创建线程有何不同&pthread多线程编程的学习小结(转)

    进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合,这些资源在Linux中被抽 象成各种数据对象:进程控制块.虚存空间.文件系统,文件I/O.信号处理函数.所以创建一个进程的 过程就是这 ...

  4. Pthread 并发编程(一)——深入剖析线程基本元素和状态

    Pthread 并发编程(一)--深入剖析线程基本元素和状态 前言 在本篇文章当中讲主要给大家介绍 pthread 并发编程当中关于线程的基础概念,并且深入剖析进程的相关属性和设置,以及线程在内存当中 ...

  5. Pthread 并发编程(二)——自底向上深入理解线程

    Pthread 并发编程(二)--自底向上深入理解线程 前言 在本篇文章当中主要给大家介绍线程最基本的组成元素,以及在 pthread 当中给我们提供的一些线程的基本机制,因为很多语言的线程机制就是建 ...

  6. C语言使用pthread多线程编程(windows系统)二

    我们进行多线程编程,可以有多种选择,可以使用WindowsAPI,如果你在使用GTK,也可以使用GTK实现了的线程库,如果你想让你的程序有更多的移植性你最好是选择POSIX中的Pthread函数库,我 ...

  7. C语言使用pthread多线程编程(windows系统)一

    运行之前需要做一些配置: 1.下载PTHREAD的WINDOWS开发包 pthreads-w32-2-4-0-release.exe(任何一个版本均可)    http://sourceware.or ...

  8. linux pthread多线程编程模板

    pthread_create() 创建线程,pthread_join()让线程一直运行下去. 链接时要加上-lpthread选项. pthread_create中, 第三个参数为线程函数,定义如下: ...

  9. pthread多线程编程

    http://blog.csdn.net/onlyou930/article/details/6755593 http://blog.csdn.net/ithomer/article/details/ ...

随机推荐

  1. String是否相等、new的时候创建了几个对象等问题详解

    问题一 这段代码创建了几个对象? String str1 = new String("aa"); 答案是两个 "aa"对象和String对象 Java代码在编译 ...

  2. [vijos1782]借教室<线段树>

      题目链接:https://vijos.org/p/1782 题意:一个区间1,n.m次操作,每次操作让l,r区间值减去d,当有任何一个值小于0就输出当前是第几个操作 这道题其实是没有什么难度的,是 ...

  3. 使用golang理解mysql的两阶段提交

    使用golang理解mysql的两阶段提交 文章源于一个问题:如果我们现在有两个mysql实例,在我们要尽量简单地完成分布式事务,怎么处理? 场景重现 比如我们现在有两个数据库,mysql3306和m ...

  4. 使用 nodejs 对文件进行批量重命名

    0. 前言 从B站下载了一点视频,硕鼠自动将标题添加到了每个文件名的前面,导致文件名过长,不方面查看文件的具体内容. 虽然只有二十几个文件,但是手动删除前缀还是个不小的工作量,还有可能删除错误.考虑到 ...

  5. 老技术新谈,Java应用监控利器JMX(1)

    先聊聊最近比较流行的梗,来一次灵魂八问. 配钥匙师傅: 你配吗? 食堂阿姨: 你要饭吗? 算命先生: 你算什么东西? 快递小哥: 你是什么东西? 上海垃圾分拣阿姨: 你是什么垃圾? 滴滴司机: 你搞清 ...

  6. Docker的MySQL镜像, 实行数据,配置信息,日志持久化

    Docker的MySQL8镜像, 实行数据持久化 使用Docker的MySQL8.0.17实例化一个容器之后需要对其进行数据持久化操作, 使用 docker docker run -p 7797:33 ...

  7. ASP.NET Core WEB API 使用element-ui文件上传组件el-upload执行手动文件文件,并在文件上传后清空文件

    前言: 从开始学习Vue到使用element-ui-admin已经有将近快两年的时间了,在之前的开发中使用element-ui上传组件el-upload都是直接使用文件选取后立即选择上传,今天刚好做了 ...

  8. MySQL从库实用技能(一)--巧用slave_exec_mode参数

    想必从库异常中断的情况不在少数,其中报错信息中1032及1062的错误占了不少的比重 错误1032指的是从库中找不到对应行的记录 错误1062指的是主键冲突 遇到此报错时,大多DBA会使用如下方法进行 ...

  9. 1044 Shopping in Mars (25分)(二分查找)

    Shopping in Mars is quite a different experience. The Mars people pay by chained diamonds. Each diam ...

  10. 从零搭建一个SpringCloud项目之Zuul(四)

    整合Zuul 为什么要使用Zuul? 易于监控 易于认证 减少客户端与各个微服务之间的交互次数 引入依赖 <dependency> <groupId>org.springfra ...