Linux C :线程操作和线程同步的多线程并发编程

      在这之前可以先看看这边文章了解线程概念,信号量,条件变量,死锁、管程等概念

https://blog.csdn.net/superSmart_Dong/article/details/116668370

      与进程相比,线程的创建和上下文切换更快。一个进程可以有多个线程,而这些线程都可以访问自身进程的所有资源。而进程的切换,则涉及到用户态转内核态的过程,原先的内存资源可能也要从外存中重新换页换回内存里。进程的创建也需要重新分配内存和构建页表等相关数据结构,而线程于进程公用同一个内存空间,除了重新初始化栈之外没有其他复杂的操作。因此,线程的相应速度也比会比进程快。多个线程则对应有多个执行路径,当一个线程被阻塞时,同进程下的其他线程仍然会在后台进行计算。进程之间必须通过进程间通信(IPC)来交换数据或者把共享资源纳入到进程的地址空间中,而同进程下的线程之间可以直接相互访问。

     当然线程操作需要考虑线程的同步问题。通常许多库函数可能是线程不安全的,通常情况下,依赖于全局变量或者静态变量的函数,如果没有特殊处理都存在线程不安全问题。

目录

一、线程操作

二、利用多线程进行快速排序

三、线程同步——互斥量、锁

四、线程同步-信号量

 五、线程同步-屏障

六、线程同步-条件变量

七、用条件变量解决生产者和消费者问题


一、线程操作

     几乎所有的操作系统都支持 POSIX Pthread 标准应用程序编程接口,在Linux中需要引入的头文件是

1)创建线程

int pthread_create(pthread_t *pthread_id ,pthread_attr_t *attr,void*(*func)(void*) , void * arg)

  • pthread_id 是线程id,数据类型是pthread_t,在POSIX是一种不透明类型,它的id是操作系统中唯一的。可以通过pthread_self()函数来获取当前线程的id。在Linux中,pthread_t被定义为无符号长整型,打印id的为  %lu
  •     attr 是pthread_attr_t的数据类型,在POSIX是一种不透明类型。它用来指定需要创建线程的属性。通常如果选择NULL则系统会给线程一个默认的推荐配置。该参数的初始化要稍微繁琐些,主要的使用步骤为。
  1. 定义一个pthread_attr_t 的变量 并用 pthread_attr_init() 初始化。
  2. 设置attr的属性并传入pthread_craete 中
  3. 必要时通过pthread_attr_destory(&attr) 释放 pthread_attr_t资源
  4. 例如要求创建的线程不能与其他线程建立关联(join) 
pthread_attr_t attr;
pthread_attr_init (&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); //取消关联
pthread_attr_setstacksize(&attr,0x10000);         //设置栈大小16KB。
pthread_create(&thread_id , &attr,func,NULL);
pthread_attr_destory(&attr);
  •     func  是线程的执行函数体地址
  •     arg   是线程的执行函数体的函数参数。

2)线程id的比较

    int pthread_equal( pthread_t  t1, pthread_t  t2);

   比较两个线程id是否相等,相等则返回0,否则为非0。

3)线程终止

   int pthread_exit(void *status);

   当线程函数执行结束后线程即终止。或者可以调用上述函数显式地终止当前线程。其中,status时线程的退出状态。操作系统规范通常是0表示正常终止,非0表示异常终止

4)线程关联 join

  int pthread_join(pthread_t thread_id,void **status_ptr);

  当前进程等待其他线程的终止,其中线程的终止状态值通过status_ptr引用入参返回。

二、利用多线程进行快速排序

       快排原理就不细说了,大致就是每次排序把其中一个元素通过头尾比大小交换到正确的位置上,之后分成左右两边进行递归再排序。 因此,当左右两边的排序并不是相互依赖的,因此可以通过线程的方式并行排序从而提高排序效率。

#include 
#include 
#include 
//快排函数需传入的参数,数组上下限
typedef struct {int upperbound;int lowerbound;
}PARM;#define N 11
int A[N] = {5,1,6,4,7,2,9,9,8,0,3};int print(){int i ;printf("[ ");for (int i=0 ; i< N ; i++){printf("%d ",A[i]);}printf(" ]\n");return 0;
}void *qsort(void *aptr){// ap 用来接收传入的参数,aleft 和aright 用来初始化新建线程的入参PARM *ap ,aleft,aright;//排序元素,排序元素位置, 排序时用的2个索引变量,和一个临时变量int pivot,pivotIndex,left,right,temp;// 接收入参数组的上下限int upperbound, lowerbound;pthread_t me,leftThread,rightThread;me = pthread_self();ap  = (PARM *) aptr;upperbound = ap->upperbound;lowerbound = ap->lowerbound;pivot   =  A[upperbound];left = lowerbound  -1 ;right = upperbound ;if ( lowerbound >= upperbound){pthread_exit(NULL);}//单次快速排序 while(left pivot);if( left < right){temp = A[left];A[left] = A[right];A[right]  = temp ;}}print();pivotIndex =  left ;temp  =  A [pivotIndex];A[pivotIndex] = pivot;A[upperbound]    =temp ;//单次快排结束 .....// 启动线程递归快排aleft.upperbound =   pivotIndex -1 ;aleft.lowerbound = lowerbound ;aright.lowerbound =  pivotIndex +1 ;aright.upperbound =  upperbound ;printf("%lu : create threads \n ",me );pthread_create(&leftThread,NULL,qsort,(void*)&aleft);pthread_create(&rightThread,NULL,qsort,(void*)&aright);// 等待左右两个线程执行完再继续执行pthread_join(leftThread,NULL);pthread_join(rightThread,NULL);printf("%lu: join with left & right threads \n",me);
}int main(int argc , char*argv[]){PARM arg;int i, *array ;pthread_t me ,thread;me  =  pthread_self();printf("main %lu : unsorted array = ",me );print();arg.upperbound = N -1 ;arg.lowerbound = 0 ;printf("%lu : create threads for qsort \n ",me );pthread_create(&thread,NULL,qsort,(void*)&arg);pthread_join(thread,NULL);printf("%lu :finish \n ",me );print();return 0;
}

命令   g++  pthreadqsort.c -pthread  进行编译:输出结果如下图:

三、线程同步——互斥量、锁

     互斥量、锁结构体实际上由 int型成员,所有者和阻塞队列组成。 int型成员当值为0则代表这块资源没被占用,允许被访问,值为1时代表资源被占用,不能再被访问。

    临界区概念:临界区是指要访问共享资源的那段代码块。这里的共享资源指系统资源、全局变量等全局资源。落到机器层面上,代码块指的时机器语言上的代码块。对于临界区的保护访问,通常的方式就是在临界区外围加上互斥机制。

    互斥变量用 pthread_mutex_t 声明,初始化的方法有两种..基本操作函数有4钟

/*******有两种方式初始化********************/
pthread_mutex_t  m  =PTHREAD_MUTEX_INITIALIZER;   //静态方法初始化
pthread_mutex_init(ptherad_mutex_t *m , pthread_mutexattr_t *attr); //动态方法
/*********锁的操作函数**********************/
int  pthread_mutex_lock(ptherad_mutex_t *m );     //上锁,如果已被上锁则阻塞线程
int  pthread_mutex_unlock(ptherad_mutex_t *m );   //解锁,上锁的变量只能由当前线程解锁
int  pthread_mutex_trylock(ptherad_mutex_t *m );  //尝试上锁,如果已被上锁则继续执行
int  pthread_mutex_destory(ptherad_mutex_t *m );  //销毁锁,如果是动态分配的互斥量,所有线程都完成后可能会被自动销毁。

   看看如下代码

#include 
#include 
#include 
int i;
pthread_mutex_t *m;
void* func (void *arg){pthread_mutex_lock(m);/*****临界区↓************/int w = i%2;i += w+1;/*****临界区↑************/pthread_mutex_unlock(m);
}
int main(int argc,char*argv[]){pthread_t  thread_1 , thread_2;i = 0;void * status;m = (pthread_mutex_t * )malloc(sizeof(pthread_mutex_t));pthread_mutex_init(m,NULL);pthread_create(&thread_1 ,NULL,func,NULL);pthread_create(&thread_2 ,NULL,func,NULL);pthread_join(thread_1,&status);pthread_join(thread_2,&status);printf("i= %d",i);
}

在临界区之外加了同步之后,输出的结果就是i=3,如果没给临界区上锁,那么线程可能执行到一一半就到另外的线程继续执行。输出的结果除了i=3之外,还有可能会 i=2.

四、线程同步-信号量

       信号量是一种数据结构,一个用int 型数据用来记录剩余资源数,另外一个成员用来记录当资源不足时,用链表表示的一个阻塞线程的队列。最经典的信号量操作就是P(),和V()用来实现线程的同步和互斥。P 和 V 操作都是原子操作,其大致伪代码定义如下.

struct sempahore{int value;struct process *queue; //阻塞队列
}s;P(sempahore *s){s->value--;if(s->value <0){BLOCK(s);  //阻塞当前线程并加入到信号量的阻塞队列中}
}V(sempahore *s){s->value++;if(s->value <=0){SIGNAL(s);  //从线程中阻塞队列中出队并唤醒出队的线程}
}

由于信号量不是POSIX的标准部分,但是Linux 还是支持信号量的一些操作

#include 
//初始化信号量,并指定信号量的类型pshared=0表示局部信号量否则可以在多线程间共享,设置可使用资源值value
int sem_init(sem_t *sem,int pshared,unsigned int value); 
int sem_destory(sem_t *sem)    //销毁信号量
int sem_post(sem_t *sem);  //等同于 V()函数
int sem_wait(sem_t *sem);  //等同于 P()函数

 五、线程同步-屏障

      在pthread_join()中,允许某个线程等待其他线程终止再继续执行.假如只是为了线程同步到达某个指定代码块的开始区域,而不希望终止线程,就可以用屏障。例如,希望每个线程都按顺序执行A,B模块代码。假如希望线程一同执行完A模块再同时开始执行B模块,不希望创建线程序列专门执行单独的模块来实现同步,那么屏障就是一个很好的选择。他可以节省创建过多的线程造成较大的系统开销,还可以再保持线程活跃的情况下同步其他线程的执行情况。

#include pthread_barrier_t barrier;   //声明//屏障初始化, 第一个是pthread_barrier_t的地址,第二个是设置属性,第3个参数是等待nthreads个线程到达,则把屏障所有阻塞的线程变成就绪态。
pthread_barrier_init(&barrier ,  NULL , nthreads);//让屏障的计数减1并阻塞当前线程,如果共wait了nthreads次,则线程继续执行
pthread_barrier_wait(&barrier)

六、线程同步-条件变量

      条件变量通常和互斥量配合使用。条件变量的成员用来记录阻塞线程数(初始值为0)和另一个成员是阻塞线程队列。类似于管程原理,最多只能有1个线程同时访问条件变量,因此访问条件变量时,需要在访问条件变量的外围加上互斥量锁。即

#include pthread_mutex_t  con_mutex;   //互斥锁
pthread_cond_t   con ;    //条件变量func(){pthread_mutex_lock(&mutex);
/*****************临界区 
******************/
pthread_cond_wait(&con,&mutex);
pthread_mutex_unlock(&mutex);}

在Linux中,条件变量的操作方法

pthread_cond_init(&con,NULL); //初始化条件变量
//阻塞当前线程到条件变量的阻塞队列,之后释放锁并sechedule(),使阻塞线程让出CPU使用权,使将来的唤醒的线程再获取锁,继续执行接下来的代码
pthread_cond_wait(&con,&mutex); //如果当前有阻塞线程,把阻塞线程唤醒,阻塞线程数减1 
pthread_cond_signal(&con); //唤醒所有阻塞线程
pthread_cond_broadcast(&con); 

条件变量通常需要多个不同功能的线程进行。因为自身线程如果执行了pthread_cond_wait,那么只能够由其他线程执行pthread_cond_signal,才能把自己唤醒,否则将陷入无限等待中。

七、用条件变量解决生产者和消费者问题

#include 
#include 
#include #define NBUF  5
#define N       10int buf [NBUF];
int head,tail;
int data;
pthread_mutex_t mutex;
pthread_cond_t empty ,full;int init(){head = tail =data = 0;pthread_mutex_init(&mutex,NULL);pthread_cond_init(&full,NULL);pthread_cond_init(&empty , NULL);
}void *producer(void *){int i  ;pthread_t me =  pthread_self();for( i = 0 ;  i< N ;i++){pthread_mutex_lock(&mutex);      if (data  == NBUF  ){printf("producer %lu :all buf FULL ,waiting\n",me);pthread_cond_wait(&empty , &mutex);}buf [head ++]   = i+1;head %= NBUF;data ++ ; printf("producer %lu :data =%d value=%d \n",me, data ,i+1);pthread_mutex_unlock(&mutex);pthread_cond_signal (&full);}printf("producer %lu : exit \n",me);
}void * consumer(void *){int i,c ;pthread_t me  = pthread_self();for(  i=0 ;  i

g++  condconsum.c  -pthread   命令进行编译

执行 a.out 的结果之一如下


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部