进程和线程

1 fork基本概念

一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
    一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

我们来看一个例子:

[cpp]  view plain  copy
  1. /*  
  2.  * @filename: fork_test.c  
  3.  * @version: version 1  
  4.  * @date: 2010-5-29  
  5.  * @author: aaron shen  
  6.  */  
  7. #include <unistd.h>  
  8. #include <stdio.h>  
  9.   
  10. int main ()  
  11. {  
  12.         pid_t fpid; /* fpid表示fork函数返回的值 */  
  13.         int count=0;  
  14.   
  15.         fpid=fork();  
  16.         if (fpid < 0)  
  17.             printf("error in fork!");  
  18.         else if (fpid == 0) {  
  19.             printf("i am the child process, my process id is %d\n",getpid());  
  20.             count++;  
  21.         }  
  22.         else {  
  23.             printf("i am the parent process, my process id is %d\n",getpid());     
  24.             count++;  
  25.         }  
  26.         printf("统计结果是: %d\n",count);  
  27.         return 0;  
  28. }  

运行结果:


[cpp]  view plain  copy
  1. i am the parent process, my process id is 8499  
  2. 统计结果是: 1  
  3. i am the child process, my process id is 8500  
  4. 统计结果是: 1  

在语句fpid=fork()之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的几乎完全相同,将要执行的下一条语句都是if(fpid<0)……
     为什么两个进程的fpid不同呢,这与fork函数的特性有关。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
     1)在父进程中,fork返回新创建子进程的进程ID
     2)在子进程中,fork返回0
     3)如果出现错误,fork返回一个负值;

fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。

fork出错可能有两种原因:
1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN
2)系统内存不足,这时errno的值被设置为ENOMEM
创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
    每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。
fork执行完毕后,出现两个进程,


有人说两个进程的内容完全一样啊,怎么打印的结果不一样啊,那是因为判断条件的原因,上面列举的只是进程的代码和指令,还有变量啊。
     执行完fork后,进程1的变量为count=0fpid=0(父进程)。进程2的变量为count=0fpid=0(子进程),这两个进程的变量 都是独立的,存在不同的地址中,不是共用的,这点要注意。可以说,我们就是通过fpid来识别和操作父子进程的。
     还有人可能疑惑为什么不是从#include处开始复制代码的,这是因为fork是把进程当前的情况拷贝一份,执行fork时,进程已经执行完了int count=0;fork只拷贝下一个要执行的代码到新的进程。

2 fork进阶知识

先看代码:

[cpp]  view plain  copy
  1. /*  
  2.  * @filename: fork_test.c  
  3.  * @version: version 1  
  4.  * @date: 2010-5-29  
  5.  * @author: aaron shen  
  6.  */  
  7. #include <unistd.h>  
  8. #include <stdio.h>  
  9.   
  10. int main ()  
  11. {  
  12. int i=0;  
  13.   
  14.     printf("i son/pa ppid pid  fpid\n");  
  15.     //ppid指当前进程的父进程pid    
  16.     //pid指当前进程的pid,    
  17.     //fpid指fork返回给当前进程的值    
  18.     for(i=0;i<2;i++)  
  19.     {  
  20.         pid_t fpid=fork();  
  21.         if(fpid==0)  
  22.            printf("%d child  %4d %4d %4d\n",i,getppid(),getpid(),fpid);  
  23.         else  
  24.            printf("%d parent %4d %4d %4d\n",i,getppid(),getpid(),fpid);  
  25.     }  
  26.     return 0;  
  27. }  

代码执行结果:

[cpp]  view plain  copy
  1. i son/pa ppid  pid   fpid  
  2. 0 parent 10633 11493 11494  
  3. 0 child  11493 11494    0  
  4. 1 parent 10633 11493 11495  
  5. 1 parent 11493 11494 11496  
  6. 1 child     1  11495    0  
  7. 1 child     1  11496    0  

现在,我们来分析该段代码。

第一步:在父进程中,指令执行到for循环中,i=0,接着执行forkfork执行完后,系统中出现两个进程,分别是p11493和p11494(后面我都用 pxxxxx表示进程idxxxxx的进程)。可以看到父进程p11493的父进程是p11494,子进程p11494的父进程正好是p11493。

第一次fork后,p11493(父进程)的变量为i=0fpid=11494(fork函数在父进程中返向子进程id;p11494(子进程)的变量为i=0fpid=0fork函数在子进程中返回0。所以,执行结果就是前两行:

[plain]  view plain  copy
  1. 0 parent 10633 11493 11494  
  2. 0 child  11493 11494    0  

 第二步:假设父进程p11493先执行,当进入下一个循环时,i=1,接着执行fork,系统中又新增一个进程p11495

对于子进程p11494,执行完第一次循环后,i=1,接着执行fork,系统中新增一个进程p11496从输出可以看到p11494原来是p11493的子进程,现在变成p11496的父进程。父子是相对的,这个大家应该容易理解。只要当前进程执行了fork,该进程就变成了父进程了,就打印出了parent
     所以打印出结果是:

[cpp]  view plain  copy
  1. 1 parent 10633 11493 11495  
  2. 1 parent 11493 11494 11496  

     第三步:第二步创建了两个进程p11495,p11496,这两个进程执行完printf函数后就结束了,因为这两个进程无法进入第三次循环,无法fork,该执行return 0;了,其他进程也是如此。
     以下是p11495,p11496打印出的结果:

[plain]  view plain  copy
  1. 1 child     1   11495    0  
  2. 1 child     1   11496    0  

细心的读者可能注意到p11495,p11496的父进程难道不该是p11493,p11494吗,怎么会是1呢?这里得讲到进程的创建和死亡的过程,在 p11493和p11494执行完第2个循环后,main函数就该退出了,也即进程该死亡了,因为它已经做完所有事情了。p11493和p11494死亡 后,p11495,p11496就没有父进程了,这在操作系统是不被允许的,所以p11495,p11496的父进程就被置为p1了,p1就是init进程,它负责系统初始化操作,init是所有进程的根
     下面,再来一份代码:

[plain]  view plain  copy
  1. /*   
  2.  * @filename: fork_test.c   
  3.  * @version: version 1   
  4.  * @date: 2010-5-29   
  5.  * @author: aaron shen   
  6.  */  
  7. #include <unistd.h>  
  8. #include <stdio.h>  
  9.   
  10. int main (int argc, char *argv[])  
  11. {  
  12.         int i=0;  
  13.   
  14.         if(argc != 2)  
  15.         {  
  16.                 printf("Usage: exec n\n");  
  17.                 return 0;  
  18.         }  
  19.   
  20.         for(i=0;i<atoi(argv[1]);i++)  
  21.         {  
  22.                 pid_t fpid=fork();  
  23.                 if(fpid==0)  
  24.                         printf("son\n");  
  25.                 else  
  26.                         printf("father\n");  
  27.         }  
  28.         return 0;  
  29. }  

总结一下规律,对于这种N次循环的情况,执行printf函数的次数为2*1+2+4+…+2N-1)次,创建的子进程数为1+2+4+……+2N-1个。网上有人说N次循环产生2*1+2+4+……+2N)个进程,这个说法是不对的,希望大家需要注意。

线程编程 

3.1 概念

3.1.1 进程:

1系统中程序执行和资源分配的基本单位;

2每个进程有自己的数据段、代码段和堆栈段;

3在进行切换时需要有比较复杂的上下文切换

 进程间通信主要包括管道系统IPC(包括消息队列,信号量,共享存储), SOCKET.

3.1.2 线程:

1减少处理机的空转时间,支持多处理器以及减少上下文切换开销比创建进程小很多

2进程内独立的一条运行路线;

3处理器调度的最小单元,也称为轻量级进程;

4)可以对进程的内存空间和资源进行访问,并与同一进程中的其他线程共享;

5线程相关的执行状态和存储变量放在线程控制表内;

6一个进程可以有多个线程,有多个线程控制表及堆栈寄存器,共享一个用户地址空间

3.1.3 多线程同步问题:

1线程共享进程的资源和地址空间;

2任何线程对系统资源的操作都会给其他线程带来影响;

3.1.4 线程技术发展:

1Linux 2.2内核,不存在真正意义上的线程;

2Linux 2.4内核,消除线程个数的限制,允许动态地调整进程数上限;在Linux 内核2.6之前,进程是最主要的处理调度单元,并没支持内核线程机制

3Linux 2.6内核,实现共享地址空间的进程机制1996年第一次获得线程的支持

4为了改善Linux Thread问题,根据新内核机制重新编写线程库改善Linux对线程的支持。由IBM主导的新一代POSIX线程库(Next Generation POSIX Threads,简称为NGPT,NGPT项目在2002年启动,为了避免出现有多个Linux线程标准,在2003年停止该项目。由Red Hat主导的本地化POSIX线程库 (Native POSIX Thread Library,简称为NTPL),最早在Red Hat Linux9中被支持,现在已经成为GNU C函数库的一部分,同时也成为Linux线程的标准

3.1.5 线程标识:

使用线程ID标示。进程ID在整个系统中是唯一的;而,线程ID只在它所属的进程环境中有效。获取线程ID的函数为:

#include <pthread.h>

pthread_t pthread_self(void); /* 返回值,调用线程的线程ID */

pthread_t类型通常用结构来表示;不能把它作为整数处理,在Linux中使用无符号长整数表示;为了移植,使用函数来比较线程ID

#include <pthread.h>

int pthread_equal(pthread_t tid_1, pthread_t tid_2); /* 返回值,调用线程的线程ID */

[cpp]  view plain  copy
  1. #include <stdio.h>  
  2. //#include <stdlib.h>  
  3. #include <pthread.h>  
  4.    
  5. int main()  
  6. {  
  7.     pthread_t thread_id;  
  8.    
  9.     thread_id=pthread_self(); // 返回调用线程的线程ID  
  10.     printf("Thread ID: %lu.\n",thread_id);  
  11.    
  12.     if (pthread_equal(thread_id,pthread_self()))   
  13.     {     
  14.         printf("Equal!\n");  
  15.     } else   
  16.     {     
  17.         printf("Not equal!\n");  
  18.     }     
  19.     return 0;  
  20. }  

编译命令:

[cpp]  view plain  copy
  1. gcc -o pthread_test pthread_test.c -lpthread  

运行结果:

[cpp]  view plain  copy
  1. Thread ID: 140099063064320.  
  2. Equal!  

3.1.5 线程创建

#include<pthread.h>

int pthread_create(pthread_t *tidp, const pthread_attr_t *attr,

(void*)(*start_rtn)(void*),void *arg);

若线程创建成功,则返回0。若线程创建失败,则返回出错编号,并且*thread中的内容是未定义的。

返回成功时,由tidp指向的内存单元被设置为新创建线程的线程IDattr参数用于指定各种不同的线程属性。新创建的线程从start_rtn函数的地址开始运行,该函数只有一个万能指针参数arg,如果需要向start_rtn函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg的参数传入。

由 restrict 修饰的指针是最初唯一对指针所指向的对象进行存取的方法,仅当第二个指针基于第一个时,才能对对象进行存取。对对象的存取都限定于基于由 restrict 修饰的指针表达式中。 由 restrict 修饰的指针主要用于函数形参,或指向由 malloc() 分配的内存空间。restrict 数据类型不改变程序的语义。 编译器能通过作出 restrict 修饰的指针是存取对象的唯一方法的假设,更好地优化某些类型的例程。

参数

第一个参数为指向线程标识符指针

第二个参数用来设置线程属性。

第三个参数是线程运行函数的起始地址。

最后一个参数是运行函数的参数。 

注意事项

因为pthread并非Linux系统的默认库,而是POSIX线程库。 在Linux中将其作为一个库来使用,因此加上 -lpthread(或-pthread)以显式链接该库。函数在执行错误时的错误信息将作为返回值返回,并不修改系统全局变量errno,当然也无法使 用perror()打印错误信息。

查看代码:

[cpp]  view plain  copy
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <pthread.h>  
  4.    
  5. void *thrd_func(void *arg);  
  6.   
  7. pthread_t tid;  
  8.    
  9. int main()  
  10. {  
  11.     /* 创建线程tid,且线程函数由thrd_func指向,是thrd_func的入口点,即马上执行此线程函数 */  
  12.     if (pthread_create(&tid,NULL,thrd_func,NULL)!=0) {  
  13.         printf("Create thread error!\n");  
  14.         exit(1);  
  15.     }     
  16.     printf("TID in pthread_create function: %lu.\n",tid);  
  17.     printf("Main process: PID: %d,TID: %lu.\n",getpid(),pthread_self());   
  18.       
  19.     sleep(1); //race  
  20.     return 0;  
  21. }  
  22.    
  23. void *thrd_func(void *arg)  
  24. {  
  25.     printf("New process:  PID: %d,TID: %lu.\n", getpid(), pthread_self()); //why pthread_self  
  26.     printf("New process: PID: %d,TID: %lu.\n",getpid(),tid); //why pthread_self  
  27.    
  28.     pthread_exit(NULL); //退出线程  
  29. }  

打印结果:

[plain]  view plain  copy
  1. TID in pthread_create function: 139895877572352.  
  2. Main process: PID: 32051,TID: 139895885829888.  
  3. New process:  PID: 32051,TID: 139895877572352.  
  4. New process:  PID: 32051,TID: 139895877572352.  

3.1.6 退出线程

线程通过调用pthread_exit函数终止执行,就如同进程在结束时调用exit函数一样。这个函数的作用是,终止调用它的线程并返回一个指向某个对象的指针

void pthread_exit(void* retval);

3.1.7 等待线程

函数pthread_join用来等待一个线程的结束。

头文件 : #include <pthread.h>

函数定义: int pthread_join(pthread_t thread, void **retval);

描述 :pthread_join()函数,以阻塞的方式等待thread指定的线程结束。当函数返回时,被等待线程的资源被收回。如果线程已经结束,那么该函数会立即返回。并且thread指定的线程必须是joinable的。

参数 :thread: 线程标识符,即线程ID,标识唯一线程。retval: 用户定义的指针,用来存储被等待线程的返回值。

返回值 : 0代表成功。 失败,返回的则是错误号。

查看代码:

[cpp]  view plain  copy
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <pthread.h>  
  4.   
  5. void *thrd_func1(void *arg);  
  6. void *thrd_func2(void *arg);  
  7.   
  8. int main(){  
  9.     pthread_t tid1,tid2;  
  10.     void *tret;  
  11.     // 创建线程tid1,线程函数thrd_func1  
  12.     if (pthread_create(&tid1,NULL,thrd_func1,NULL)!=0) {  
  13.         printf("Create thread 1 error!\n");  
  14.         exit(1);  
  15.     }  
  16.     // 创建线程tid2,线程函数thrd_func2  
  17.     if (pthread_create(&tid2,NULL,thrd_func2,NULL)!=0) {  
  18.         printf("Create thread 2 error!\n");  
  19.         exit(1);  
  20.     }  
  21.     // 等待线程tid1结束,线程函数返回值放在tret中  
  22.     if (pthread_join(tid1,&tret)!=0){  
  23.         printf("Join thread 1 error!\n");  
  24.         exit(1);  
  25.     }  
  26.   
  27.     printf("Thread 1 exit code: %d.\n",(int *)tret);  
  28.     // 等待tid2结束,线程函数返回值放在tret中  
  29.     if (pthread_join(tid2,&tret)!=0){  
  30.         printf("Join thread 2 error!\n");  
  31.         exit(1);  
  32.     }  
  33.   
  34.     printf("Thread 2 exit code: %d.\n",tret);  
  35.   
  36.     return 0;  
  37. }  
  38.   
  39. void *thrd_func1(void *arg){  
  40.     printf("Thread 1 returning!\n");  
  41. //    sleep(3);  
  42.     //   return ((void *)1); // 自动退出线程  
  43.     pthread_exit((void *)1);  
  44. }  
  45.   
  46. void *thrd_func2(void *arg){  
  47.     printf("Thread 2 exiting!\n");  
  48.     pthread_exit((void *)2);  // 线程主动退出,返回(void *)2  
  49. }  
打印结果:


[cpp]  view plain  copy
  1. Thread 1 returning!  
  2. Thread 2 exiting!  
  3. Thread 1 exit code: 1.  
  4. Thread 2 exit code: 2.  

3.2 线程同步与互斥

线程共享进程的资源和地址空间,对这些资源进行操作时,必须考虑线程间同步与互斥问题。

三种线程同步机制:

1互斥锁

2信号量

3条件变量

使用场合:

(1)互斥锁更适合同时可用的资源是惟一的情况;

(2)信号量更适合同时可用的资源为多个的情况;

3.2.1 互斥锁

用简单的加锁方法控制对共享资源的原子操作,只有两种状态上锁、解锁。可把互斥锁看作某种意义上的全局变量;在同一时刻只能有一个线程掌握某个互斥锁,拥有上锁状态的线程能够对共享资源进行操作;若其他线程希望上锁一个已经被上锁的互斥锁,则该线程就会挂起,直到上锁的线程释放掉互斥锁为止;互斥锁保证让每个线程对共享资源按顺序进行原子操作。

互斥锁分类,区别在于其他未占有互斥锁的线程在希望得到互斥锁时是否需要阻塞等待。

1快速互斥锁:调用线程会阻塞直至拥有互斥锁的线程解锁为止;默认为快速互斥锁;

2检错互斥锁:为快速互斥锁的非阻塞版本,它会立即返回并返回一个错误信息;

互斥锁主要包括下面的基本函数:

1互斥锁初始化:pthread_mutex_init()

2互斥锁上锁:pthread_mutex_lock()

3互斥锁判断上锁:pthread_mutex_trylock()

4互斥锁解锁:pthread_mutex_unlock()

(5)消除互斥锁:pthread_mutex_destroy()

互斥锁理解:

说明:

(1) 假设有三个线程,分别为线程1,线程2,和线程3;

(2) 处于标圆形框之上的线段表示相关的线程没有拥有互斥量;

(3) 处于圆形框中心线之上的线段表示相关的线程等待互斥量;

(4) 处于圆形框中心线之下的线段表示相关的线程拥有互斥量;

过程描述:

1最初,互斥量没有被加锁;

2当线程1试图加锁该互斥量时,因为没有竞争,线程1立即加锁成功,对应线段也移到中心线之下;

3然后线程2试图加锁互斥量,由于互斥量已经被加锁,所以线程2被阻塞,对应线段在中心线之上;

4接着,线程1解锁互斥量,于是线程2解除阻塞,并对互斥量加锁成功;

5然后,线程3试图加锁互斥量,同样被阻塞;

6此时,线程1调用函数pthread_mutext_trylock试图加锁互斥量,而立即返回EBUSY;

7然后,线程2解锁互斥量,解除线程3的阻塞,线程3加锁成功;

8最后,线程3完成工作,解锁互斥量

查看代码:

[cpp]  view plain  copy
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <pthread.h>  
  4.   
  5. #define THREAD_NUM 3  
  6. #define REPEAT_TIMES 5  
  7. #define DELAY 4  
  8.   
  9. pthread_mutex_t mutex;  
  10.   
  11. void *thrd_func(void *arg);  
  12. int main(){  
  13.     pthread_t thread[THREAD_NUM];  
  14.     int no;  
  15.     void *tret;  
  16.   
  17.     srand((int)time(0));  
  18.     // 创建快速互斥锁(默认),锁的编号返回给mutex      
  19.     pthread_mutex_init(&mutex,NULL);  
  20.   
  21.     // 创建THREAD_NUM个线程,每个线程号返回给&thread[no],每个线程的入口函数均为thrd_func,参数为  
  22.     for(no=0;no<THREAD_NUM;no++){  
  23.         if (pthread_create(&thread[no],NULL,thrd_func,(void*)no)!=0) {  
  24.             printf("Create thread %d error!\n",no);  
  25.             exit(1);  
  26.         } else  
  27.             printf("Create thread %d success!\n",no);  
  28.     }  
  29.   
  30.     // 对每个线程进行join,返回值给tret  
  31.     for(no=0;no<THREAD_NUM;no++){  
  32.         if (pthread_join(thread[no],&tret)!=0){  
  33.             printf("Join thread %d error!\n",no);  
  34.             exit(1);  
  35.         }else  
  36.             printf("Join thread %d success!\n",no);  
  37.     }  
  38.     // 消除互斥锁  
  39.     pthread_mutex_destroy(&mutex);  
  40.     return 0;  
  41. }  
  42.   
  43. void *thrd_func(void *arg){  
  44.     int thrd_num=(void*)arg; // 传入的参数,互斥锁的编号  
  45.     int delay_time,count;  
  46.   
  47.     // 对互斥锁上锁  
  48.     if(pthread_mutex_lock(&mutex)!=0) {  
  49.         printf("Thread %d lock failed!\n",thrd_num);  
  50.         pthread_exit(NULL);  
  51.     }  
  52.   
  53.     printf("Thread %d is starting.\n",thrd_num);  
  54.     for(count=0;count<REPEAT_TIMES;count++) {  
  55.         delay_time=(int)(DELAY*(rand()/(double)RAND_MAX))+1;  
  56.         sleep(delay_time);  
  57.         printf("\tThread %d:job %d delay =%d.\n",thrd_num,count,delay_time);  
  58.     }  
  59.   
  60.     printf("Thread %d is exiting.\n",thrd_num);  
  61.     // 解锁  
  62.     pthread_mutex_unlock(&mutex);  
  63.   
  64.     pthread_exit(NULL);  
  65. }  

打印结果:

[cpp]  view plain  copy
  1. Create thread 0 success!  
  2. Create thread 1 success!  
  3. Create thread 2 success!  
  4. Thread 2 is starting.  
  5.     Thread 2:job 0 delay =2.  
  6.     Thread 2:job 1 delay =2.  
  7.     Thread 2:job 2 delay =4.  
  8.     Thread 2:job 3 delay =4.  
  9.     Thread 2:job 4 delay =1.  
  10. Thread 2 is exiting.  
  11. Thread 1 is starting.  
  12.     Thread 1:job 0 delay =3.  
  13.     Thread 1:job 1 delay =4.  
  14.     Thread 1:job 2 delay =4.  
  15.     Thread 1:job 3 delay =1.  
  16.     Thread 1:job 4 delay =3.  
  17. Thread 1 is exiting.  
  18. Thread 0 is starting.  
  19.     Thread 0:job 0 delay =1.  
  20.     Thread 0:job 1 delay =2.  
  21.     Thread 0:job 2 delay =2.  
  22.     Thread 0:job 3 delay =1.  
  23.     Thread 0:job 4 delay =3.  
  24. Thread 0 is exiting.  
  25. Join thread 0 success!  
  26. Join thread 1 success!  
  27. Join thread 2 success!  

和上一版本的程序差异在于有没有锁,有锁的情况下,必须等"thread x is exiting."之后其他线程才能继续。

3.2.2 信号量

操作系统中所用到的PV原子操作,广泛用于进程或线程间的同步与互斥;本质上是一个非负的整数计数器,被用来控制对公共资源的访问。PV原子操作:对整数计数器信号量sem的操作,一次P操作使sem1,而一次V操作使sem1进程(或线程)根据信号量的值来判断是否对公共资源具有访问权限:

1当信号量sem的值大于等于零时,该进程(或线程)具有公共资源的访问权限

2当信号量sem的值小于零时,该进程(或线程)就将阻塞直到信号量sem的值大于等于0为止

 PV操作主要用于线程间的同步和互斥。互斥,几个线程只设置一个信号量sem;同步,会设置多个信号量,安排不同初值来实现它们之间的顺序执行。

3.2.3 信号量函数

sem_init():创建一个信号量,并初始化它;

sem_wait()sem_trywait(): P操作,在信号量大于零时将信号量的值减1

区别若信号量小于零时,sem_wait()将会阻塞线程,sem_trywait()则会立即返回

sem_post(): V操作,将信号量的值加一同时发出信号来唤醒等待的线程

sem_getvalue(): 得到信号量的值

sem_destroy(): 删除信号量

所需头文件:#include <semaphore.h>

查看代码:

[cpp]  view plain  copy
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <pthread.h>  
  4. #include <semaphore.h>  
  5.   
  6. #define THREAD_NUM    3  
  7. #define REPEAT_TIMES  5  
  8. #define DELAY         4  
  9.   
  10. /* 
  11.  * 保存信号量ID 
  12.  */  
  13. sem_t sem[THREAD_NUM];  
  14.   
  15. void *thrd_func(void *arg);  
  16.   
  17. int main()  
  18. {  
  19.   pthread_t thread[THREAD_NUM];  
  20.   int i;  
  21.   void *tret;  
  22.   
  23.   srand((int)time(0));  
  24.   
  25.   // 初始化THREAD_NUM-1个信号量,均初始化为0  
  26.   for(i=0; i < THREAD_NUM-1; i++)  
  27.   {  
  28.     sem_init(&sem[i],0,0);  /* int sem_init(sem_t *sem, int pshared, unsigned int value) */  
  29.   }  
  30.   
  31.   // sem[2]信号量初始化为1,即sem数组中最后一个信号量  
  32.   sem_init(&sem[2],0,1);  
  33.   
  34.   // 创建THREAD_NUM个线程,入口函数均为thrd_func,参数为(void*)i  
  35.   for(i = 0; i < THREAD_NUM; i++)  
  36.   {  
  37.     if (pthread_create(&thread[i],NULL,thrd_func,(void*)i)!=0)   
  38.     {     
  39.       printf("Create thread %d error!\n",i);  
  40.       exit(1);  
  41.     }     
  42.     else  
  43.       printf("Create thread %d success!\n",i);  
  44.   }  
  45.       
  46.   // 逐个join掉THREAD_NUM个线程  
  47.   for(i=0 ; i<THREAD_NUM ; i++)  
  48.   {  
  49.     if (pthread_join(thread[i],&tret)!=0)  
  50.     {     
  51.       printf("Join thread %d error!\n",i);  
  52.       exit(1);  
  53.     }else  
  54.       printf("Join thread %d success!\n",i);  
  55.   }  
  56.       
  57.   // 逐个取消信号量  
  58.   for(i=0;i<THREAD_NUM;i++)  
  59.   {  
  60.     sem_destroy(&sem[i]);  
  61.   }  
  62.   
  63.   return 0;  
  64. }  
  65.   
  66. void *thrd_func(void *arg)  
  67. {  
  68.   int thrd_num=(void*)arg; // 参数i  
  69.   int delay_time,count;  
  70.   
  71.   // 带有阻塞的p操作  
  72.   sem_wait(&sem[thrd_num]);  
  73.   
  74.   
  75.   printf("Thread %d is starting.\n",thrd_num);  
  76.   for(count=0;  count<REPEAT_TIMES; count++)  
  77.   {  
  78.     delay_time=(int)(DELAY*(rand()/(double)RAND_MAX))+1;  
  79.     sleep(delay_time);  
  80.     printf("\tThread %d:job %d delay =%d.\n",thrd_num,count,delay_time);  
  81.   }  
  82.   
  83.   printf("Thread %d is exiting.\n",thrd_num);  
  84.   
  85.   // 对前一个信号量进行V操作  
  86.   // 由于只有最后一个信号量初始化为1,其余均为0  
  87.   // 故线程执行的顺序将为逆序  
  88.   sem_post(&sem[(thrd_num+THREAD_NUM-1)%THREAD_NUM]);  
  89.   
  90.   pthread_exit(NULL); // 线程主动结束  
  91. }  
运行结果:

[cpp]  view plain  copy
  1. Create thread 0 success!  
  2. Create thread 1 success!  
  3. Create thread 2 success!  
  4. Thread 2 is starting.  
  5.     Thread 2:job 0 delay =3.  
  6.     Thread 2:job 1 delay =2.  
  7.     Thread 2:job 2 delay =4.  
  8.     Thread 2:job 3 delay =4.  
  9.     Thread 2:job 4 delay =1.  
  10. Thread 2 is exiting.  
  11. Thread 1 is starting.  
  12.     Thread 1:job 0 delay =2.  
  13.     Thread 1:job 1 delay =1.  
  14.     Thread 1:job 2 delay =1.  
  15.     Thread 1:job 3 delay =3.  
  16.     Thread 1:job 4 delay =3.  
  17. Thread 1 is exiting.  
  18. Thread 0 is starting.  
  19.     Thread 0:job 0 delay =4.  
  20.     Thread 0:job 1 delay =1.  
  21.     Thread 0:job 2 delay =3.  
  22.     Thread 0:job 3 delay =4.  
  23.     Thread 0:job 4 delay =3.  
  24. Thread 0 is exiting.  
  25. Join thread 0 success!  
  26. Join thread 1 success!  
  27. Join thread 2 success!  

3.2.4 读写锁

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。

代码实现功能:使用读写锁实现四个线程读写一段程序,共建了四个线程,其中两个线程用来读数据,另外两个线程用来写数据,在任意时刻,如果有一个线程在写数据,将阻塞其他线程的任何操作。

[cpp]  view plain  copy
  1. /* 
  2.  * @Author:     Aaron Shen 
  3.  * @Date:       2016.4.26 
  4.  * 
  5.  */  
  6. #include <errno.h>  
  7. #include <pthread.h>  
  8. #include <stdio.h>  
  9. #include <stdlib.h>  
  10. #include <bits/pthreadtypes.h>  
  11.   
  12. /* 
  13.  * Macro definition 
  14.  */  
  15.   
  16. #define WORK_SIZE 1024  
  17.   
  18. /* 
  19.  * variable definition 
  20.  */  
  21. static pthread_rwlock_t rwlock;  
  22. char work_area[WORK_SIZE];  
  23. int time_to_exit;  
  24.   
  25. /* 
  26.  * functions declaration 
  27.  */  
  28. void *thread_function_read_o(void *arg);        //read  
  29. void *thread_function_read_t(void *arg);  
  30. void *thread_function_write_o(void *arg);       //write  
  31. void *thread_function_write_t(void *arg);  
  32.      
  33. /* 
  34.  * @func name:      main() function 
  35.  */  
  36. int main(int argc,char *argv[])  
  37. {  
  38.     int res;  
  39.     pthread_t a_thread, b_thread, c_thread, d_thread;  
  40.     void *thread_result;  
  41.   
  42.     res=pthread_rwlock_init(&rwlock,NULL);  
  43.     if (res != 0){   
  44.         perror("rwlock initialization failed");  
  45.         exit(EXIT_FAILURE);  
  46.     }     
  47.       
  48.     res = pthread_create(&a_thread, NULL, thread_function_read_o, NULL);  
  49.     if (res != 0){   
  50.         perror("Thread creation failed");  
  51.         exit(EXIT_FAILURE);  
  52.     }     
  53.   
  54.     res = pthread_create(&b_thread, NULL, thread_function_read_t, NULL);//create new thread  
  55.     if (res != 0){   
  56.         perror("Thread creation failed");  
  57.         exit(EXIT_FAILURE);  
  58.     }  
  59.   
  60.     res = pthread_create(&c_thread, NULL, thread_function_write_o, NULL);//create new thread  
  61.     if (res != 0){  
  62.         perror("Thread creation failed");  
  63.         exit(EXIT_FAILURE);  
  64.     }  
  65.   
  66.     res = pthread_create(&d_thread, NULL, thread_function_write_t, NULL);//create new thread  
  67.     if (res != 0){  
  68.         perror("Thread creation failed");  
  69.         exit(EXIT_FAILURE);  
  70.     }  
  71.   
  72.     res = pthread_join(a_thread, &thread_result);//等待a_thread线程结束             
  73.     if (res != 0){  
  74.         perror("Thread join failed");  
  75.         exit(EXIT_FAILURE);  
  76.     }  
  77.   
  78.     res = pthread_join(b_thread, &thread_result);  
  79.     if (res != 0){  
  80.         perror("Thread join failed");  
  81.         exit(EXIT_FAILURE);  
  82.     }  
  83.   
  84.     res = pthread_join(c_thread, &thread_result);  
  85.     if (res != 0){  
  86.         perror("Thread join failed");  
  87.         exit(EXIT_FAILURE);  
  88.     }  
  89.   
  90.     res = pthread_join(d_thread, &thread_result);  
  91.     if (res != 0){  
  92.       perror("Thread join failed");  
  93.       exit(EXIT_FAILURE);  
  94.    }  
  95.   
  96.    pthread_rwlock_destroy(&rwlock);//销毁读写锁                 
  97.    exit(EXIT_SUCCESS);  
  98. }  
  99. /* 
  100.  * read function 1 
  101.  */  
  102. void *thread_function_read_o(void *arg)  
  103. {  
  104.     printf("thread read one try to get lock\n");  
  105.     pthread_rwlock_rdlock(&rwlock);//获取读取锁  
  106.     while(strncmp("end", work_area, 3) != 0)  
  107.     {  
  108.         printf("this is thread read one.");  
  109.         printf("the characters is %s",work_area);  
  110.   
  111.         pthread_rwlock_unlock(&rwlock);  
  112.         sleep(2);  
  113.         pthread_rwlock_rdlock(&rwlock);  
  114.         while (work_area[0] == '\0' )  
  115.         {  
  116.             pthread_rwlock_unlock(&rwlock);  
  117.             sleep(2);  
  118.             pthread_rwlock_rdlock(&rwlock);  
  119.         }  
  120.   
  121.     }  
  122.     pthread_rwlock_unlock(&rwlock);  
  123.     time_to_exit=1;  
  124.     pthread_exit(0);  
  125. }  
  126. /* 
  127.  * read function 2 
  128.  */  
  129.  void *thread_function_read_t(void *arg)  
  130. {  
  131.     printf("thread read one try to get lock\n");  
  132.     pthread_rwlock_rdlock(&rwlock);  
  133.     while(strncmp("end", work_area, 3) != 0)  
  134.     {  
  135.         printf("this is thread read two.");  
  136.         printf("the characters is %s",work_area);  
  137.   
  138.         pthread_rwlock_unlock(&rwlock);  
  139.         sleep(3);  
  140.         pthread_rwlock_rdlock(&rwlock);  
  141.         while (work_area[0] == '\0' )  
  142.         {  
  143.             pthread_rwlock_unlock(&rwlock);  
  144.             sleep(3);  
  145.             pthread_rwlock_rdlock(&rwlock);  
  146.         }  
  147.     }  
  148.     pthread_rwlock_unlock(&rwlock);  
  149.     time_to_exit=1;  
  150.     pthread_exit(0);  
  151. }  
  152. /* 
  153.  * write function 1 
  154.  */  
  155. void *thread_function_write_o(void *arg)  
  156. {  
  157.     printf("this is write thread one try to get lock\n\n");  
  158.     while(!time_to_exit) //because every Read have changed the Time_to_Exit=1, so if Time_to_Exit!=1,this is Write    
  159.     {  
  160.         pthread_rwlock_wrlock(&rwlock);  
  161.         printf("this is write thread one.\nInput some text. Enter 'end' to finish...\n");  
  162.         fgets(work_area, WORK_SIZE, stdin);  
  163.         pthread_rwlock_unlock(&rwlock);  
  164.         sleep(7);  
  165.     }  
  166.     pthread_rwlock_unlock(&rwlock);  
  167.     pthread_exit(0);  
  168. }  
  169. /* 
  170.  * write function 2 
  171.  */  
  172. void *thread_function_write_t(void *arg)  
  173. {  
  174.     sleep(4);  
  175.     while(!time_to_exit)  
  176.     {  
  177.         pthread_rwlock_wrlock(&rwlock);//获取写入锁  
  178.         printf("this is write thread two.\nInput some text. Enter 'end' to finish...\n");  
  179.         fgets(work_area, WORK_SIZE, stdin);//写入  
  180.         pthread_rwlock_unlock(&rwlock);//解锁  
  181.         sleep(10);  
  182.     }  
  183.     pthread_rwlock_unlock(&rwlock);//解锁  
  184.     pthread_exit(0);  
  185. }  

3.2.5 条件变量

类型声明:pthread_cond_t cond; 

对条件变量的初始化:程序在使用pthread_cond_t变量之前必须对其进行初始化。对于静态的pthread_cond_t变量来说,是要将PTHREAD_COND_INITIALIZER赋给变量;对于动态分配的或没有默认属性的变量来说,就要调用pthread_cond_init函数来执行初始化。

对条件变量的操作:

int pthread_cond_wait(pthread_cond_* cond, pthread_mutex_t* mutex);//使线程阻塞于某个条件。 

int pthread_cond_signal(pthread_cond_t* cond);//唤醒某个阻塞在cond上的线程。 

int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒阻塞在cond上的所有线程。 

对条件变量的销毁: int pthread_cond_destroy(pthread_cond_t* cond); 

条件变量的使用细节研究:

互斥量和读写锁解决了多线程访问共享变量产生的竞争问题,那么条件变量的作用何在呢。

条件变量的作用在于他给多个线程提供了一个汇合的场所。什么意思呢?
举个最简单的例子,比如运动会赛跑中,所有选手都会等到发令枪响后才会跑,把选手比作其他的子线程。发令员比作主线程。那么就是说,所有的子线程现在都在等待主线程给予一个可以运行的信号(发令枪响)。这就是这些子线程的汇合点。如果主线程没给信号,那么子线程就会阻塞下去。
大概明白了条件变量的作用,现在我们来考虑第一个使用细节上的问题:

考虑一个情况:b、c、d,3个线程都期望在一个条件变量等待主线程发送信号,如果此时条件测试为假,那么三个线程下一步应该是阻塞休眠。
但是在判断条件不正确和休眠这之间有个时间窗口,假如在b,c,d,3个线程检查条件为假后,CPU切换到另一个线程a,在线程a中却使条件变为真了。那么当CPU切换回b,cd线程中时线程还是会休眠。也就是说在线程检查条件变量和进入休眠等待条件改变这两个操作之间存在一个时间窗口,这里存在着竞争。
我们知道互斥量是可以用来解决上面的竞争问题的,所以条件变量本身是由互斥量来保护的。也就是他们必须搭配使用。
既然判断和休眠是由互斥量来保护从而成为一个原子操作,那么其他改变条件的线程就应该以一致的方式修改条件,也就是说其它线程在改变条件状态前也必须首先锁住互斥量。(如果修改操作不是用互斥量来保护的话,那么判断和休眠使用互斥量来保护也就没有意义。因为其他线程还是可以在两个操作的空隙中改变条件。但是如果修改操作也使用互斥量。因为判断和休眠之前先加锁了。那么修改操作就只能等到判断失败和休眠两个操作完成才能进行而不会在这之间修改)。

代码实现功能:一个int型全局变量g_flag初始值为0,在主线程中启动线程1,打印“this is thread1”,g_flag设置为1,在主线程中启动线程2,打印““this is thread2”,g_flag设置为2,线程1在线程2退出后退出,主线程在检测到g_Flag1变成2,或者从2变成1退出。

[cpp]  view plain  copy
  1. #include<stdio.h>  
  2. #include<stdlib.h>  
  3. #include<pthread.h>  
  4. #include<errno.h>  
  5. #include<unistd.h>   
  6.   
  7. typedef void* (*fun)(void*);   
  8. int g_flag=0;   
  9. static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;   
  10. static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;   
  11. void* thread1(void*);   
  12. void* thread2(void*);   
  13.   
  14. /* 
  15.  * @func:   main() 
  16.  * @descripe:   First, two threads that have been created, 
  17.  */  
  18. int main(int argc, char** argv)  
  19. {   
  20.     printf("enter main\n");  
  21.       
  22.     pthread_t tid1, tid2;   
  23.     int rc1=0, rc2=0;  
  24.       
  25.     rc2 = pthread_create(&tid2, NULL, thread2, NULL);   
  26.     if(rc2 != 0)   
  27.         printf("%s: %d\n",__func__, strerror(rc2));  
  28.   
  29.     rc1 = pthread_create(&tid1, NULL, thread1, &tid2);   
  30.     if(rc1 != 0)   
  31.       printf("%s: %d\n",__func__, strerror(rc1));  
  32.   
  33.     pthread_cond_wait(&cond, &mutex);   
  34.     printf("leave main\n");   
  35.       
  36.     exit(0);      
  37. }  
  38. /* 
  39.  * @func:       thread1() 
  40.  * @des:        Implementation function for the thread one, to 
  41.  */  
  42. void* thread1(void* arg)  
  43. {   
  44.     printf("enter thread1\n");   
  45.     printf("this is thread1, g_flag: %d, thread id is %u\n",g_flag, (unsigned int)pthread_self());  
  46.     pthread_mutex_lock(&mutex);   
  47.       
  48.     if(g_flag == 2)  
  49.       pthread_cond_signal(&cond);  
  50.     g_flag = 1;   
  51.       
  52.     printf("this is thread1, g_flag: %d, thread id is %u\n",g_flag, (unsigned int)pthread_self());  
  53.       
  54.     pthread_mutex_unlock(&mutex);  
  55.     pthread_join(*(pthread_t*)arg, NULL);   
  56.     printf("leave thread1\n");  
  57.     pthread_exit(0);  
  58. }  
  59.   
  60. void* thread2(void* arg)  
  61. {   
  62.     printf("enter thread2\n");   
  63.     printf("this is thread2, g_flag: %d, thread id is %u\n",g_flag, (unsigned int)pthread_self());  
  64.     pthread_mutex_lock(&mutex);   
  65.   
  66.     if(g_flag == 1)  
  67.         pthread_cond_signal(&cond);  
  68.     g_flag = 2;   
  69.       
  70.     printf("this is thread2, g_flag: %d, thread id is %u\n",g_flag, (unsigned int)pthread_self());  
  71.     pthread_mutex_unlock(&mutex);   
  72.     printf("leave thread2\n");  
  73.     pthread_exit(0);  
  74. }  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值