• 使用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
    ");
      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
    ", 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
    ");
    
    图片

    同时,调用 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, "	
    ");    
     		while ( my_string != NULL ){      
     			count++;      
     			printf("Thread %ld > string %d = %s n", my_rank, count,my_string);      
     			my_string = strtok(NULL, "	
    ");      
     		}    
     		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 标准会提供要给线程安全的方案:

    图片
  • 相关阅读:
    c#Socket通讯
    LeetCode 836. 矩形重叠
    AOP之SpringAOP、AspectJ、CGlib
    Springboot启动流程,跟着源码看看启动的时候都做了什么
    Mybatis/Mybatis plus/Hibernate如何忽略指定的字段不与数据库映射
    LeetCode 206. 反转链表
    LeetCode 1071. 字符串的最大公因子
    LeetCode 994. 腐烂的橘子
    Java生鲜电商平台-监控模块的设计与架构
    Java生鲜电商平台-售后模块的设计与架构
  • 原文地址:https://www.cnblogs.com/yiyefuyou/p/12783639.html
Copyright © 2020-2023  润新知