深入理解Linux多线程

深入理解Linux多线程

目录

  • Linux线程概念
    • 什么是线程
    • 二级页表
    • 线程的优点
    • 线程的缺点
    • 线程异常
    • 线程用途
  • Linux进程VS线程
  • Linux线程控制
    • POSIX线程库
      • 创建线程
      • 线程等待
      • 线程终止与分离
      • 线程ID的本质
  • Linux线程互斥
    • 进程线程间的互斥相关背景概念
    • 互斥量mutex
    • 互斥量实现原理探究
    • 可重入VS线程安全
  • 死锁
    • 死锁四个必要条件
    • 避免死锁
  • Linux线程同步
    • 条件变量
  • 生产者消费者模型
    • 基于BlockingQueue的生产者消费者模型
  • POSIX信号量
    • 基于环形队列的生产消费模型

Linux线程概念

什么是线程

1、在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”

2、一切进程至少都有一个执行线程

3、线程在进程内部运行,本质是在进程地址空间内运行

4、在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化

5、透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

6、Linux下,并不存在真正的多线程,而是用进程模拟的,但windows中存在真正的多线程。如果Linux下支持真正的多线程,当线程足够多的时候,OS要管理线程,如果支持真的线程,OS要创建线程,终止线程,调度线程,切换线程,给线程分配资源,释放资源,回收资源,所有的这一套相比较进程都会另起炉灶,再搭一套在进程内部,或与进程平行的另一条线程管理模块,这样的话一定会提高设计OS的复杂程度,所以Linux中线程的 设计直接复用了进程的数据结构,所以Linux下并不存在真正的多线程,是用进程模拟的。
深入理解Linux多线程_第1张图片

二级页表

深入理解Linux多线程_第2张图片
二级页表:32个比特位,前10个比特位查页目录对应的二级页表,中10个比特位查页表中的某一位置确定要访问的是哪一页,中间10个比特位能够让我们确定映射到物理内存是哪一个页框,再拿最后12个比特位,能够确定某个起始页内的偏移地址,可以找到具体的哪一个字节。

线程的优点

1、创建一个新线程的代价要比创建一个新进程小得多

2、与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

3、线程占用的资源要比进程少很多

4、能充分利用多处理器的可并行数量

5、在等待慢速I/O操作结束的同时,程序可执行其他的计算任务

6、计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。(计算密集型:执行流的大部分任务,主要以计算为主,例:加密解密,排序查找)

7、I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。(执行流执行的大部分任务,是以IO为主的,刷磁盘、访问数据库、访问网络)

线程的缺点

1、性能损失:
一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

2、健壮性降低:
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

3、缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

4、编程难度提高
编写与调试一个多线程程序比单线程程序困难得多

线程异常

1、单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃

2、线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

线程用途

1、合理的使用多线程,能提高CPU密集型程序的执行效率

2、合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

Linux进程VS线程

、进程是资源分配的基本单位
、线程是调度的基本单位
、线程共享进程数据,但也拥有自己的一部分数据:

线程ID
一组寄存器(线程是调度的基本单位,只要调度就要对各自的数据进行上下文保存)
(线程运行时都会产生自己的临时数据,需要将数据进行压栈)
errno
信号屏蔽字
调度优先级

、进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

文件描述符表
每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
当前工作目录
用户id和组id

进程和线程的关系如下图:
深入理解Linux多线程_第3张图片

Linux线程控制

POSIX线程库

与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的要使用这些函数库,要通过引入头文链接这些线程函数库时要使用编译器命令的“-lpthread”选项

创建线程

深入理解Linux多线程_第4张图片

创建线程

#include                                                                  
  2 #include<pthread.h>
  3 #include<unistd.h>
  4 #include<sys/types.h>
  5 #include<stdlib.h>
  6 
  7 void *Routinue(void *arg)
  8 {
  9   char *msg=(char*)arg;
 10   while(1)
 11   {
 12     printf("%s: pid: %d,ppid: %d\n",msg,getpid(),getppid());
 13     sleep(1);
 14   }
 15 }
 16 int main()
 17 {
 18   pthread_t tid;
 19 
 20  // &tid:线程id;NULL:为默认属性;Routinue,回调函数,即想让线程执行什么逻辑
 21  // "thread 1",给回调函数指定的参数
 22   pthread_create(&tid,NULL,Routinue,(void*)"thread 1"); //创建一个线程
 23   while(1)
 24   {
 25     printf("main thread: pid: %d,ppid: %d\n",getpid(),getppid());
 26     sleep(2);
 27   }
 28   return 0;
 29 }                                                                                  
 30 

创建多线程及获取线程自身id

1 #include<stdio.h>                                                                          
  2 #include<pthread.h>
  3 #include<unistd.h>
  4 #include<sys/types.h>
  5 #include<stdlib.h>
  6 
  7 void *Routinue(void *arg)
  8 {
  9   char *msg=(char*)arg;
 10   while(1)
 11   {
 12     //pthread_self(),获取线程自身id
 13     printf("%s: pid: %d,ppid: %d,tid: %lu\n",msg,getpid(),getppid(),pthread_self());
 14     sleep(1);
 15   }
 16 }
 17 int main()
 18 {
 19   pthread_t tid[5];  //创建多个线程
 20   for(int i=0;i<5;i++)
 21   {
 22     char buffer[64];
 23     sprintf(buffer,"thread %d\n",i);
  24     // &tid:线程id;NULL:为默认属性;Routinue,回调函数,即想让线程执行什么逻辑
 25     // "thread 1",给回调函数指定的参数
 26     pthread_create(&tid[i],NULL,Routinue,(void*)buffer); //创建线程                        
 27     printf("%s tid is: %lu\n",buffer,tid[i]);
 28   }
 29   while(1)
 30   {
 31     printf("main thread: pid: %d,ppid: %d, tid: %lu\n",getpid(),getppid(),pthread_self());
 32     sleep(2);
 33   }
 34   return 0;
 35 }


线程等待

深入理解Linux多线程_第5张图片

深入理解Linux多线程_第6张图片

1 #include<stdio.h>                                                                        
    2 #include<pthread.h>
    3 #include<unistd.h>
    4 #include<sys/types.h>
    5 #include<stdlib.h>
    6 
    7 void *Routinue(void *arg)
    8 {
    9   char *msg=(char*)arg;
   10   int count=0;
   11   while(count<5)
   12   {
   13     //pthread_self(),获取线程自身id
   14     printf("%s: pid: %d,ppid: %d,tid: %lu\n",msg,getpid(),getppid(),pthread_self());
   15     sleep(1);
   16     count++;
   17   }
   18   return (void*)10;
   19 }
   20 int main()
   21 {
   22   pthread_t tid[5];  //创建多个线程
   23   for(int i=0;i<5;i++)
   24   {
E> 25     char* buffer=(void*)malloc(64);
   26     sprintf(buffer,"thread %d\n",i);
   27     // &tid:线程id;NULL:为默认属性;Routinue,回调函数,即想让线程执行什么逻辑
   28     // "thread 1",给回调函数指定的参数
   29     pthread_create(&tid[i],NULL,Routinue,(void*)buffer); //创建线程
   30     printf("%s tid is: %lu\n",buffer,tid[i]);
   31   }
   32   printf("main thread: pid: %d,ppid: %d, tid: %lu\n",getpid(),getppid(),pthread_self());
   33   for(int i=0;i<5;i++)
   34   {
   35     void* ret=NULL;
   36     // pthread_join  线程等待
   37     pthread_join(tid[i],&ret);  //&ret是退出码
E> 38     printf("thread %d[%lu] ...quit!,code: %d\n",i,tid[i],(int)ret);                      
   39   }
   40   
   41   return 0;
   42 }


线程终止与分离

深入理解Linux多线程_第7张图片

线程ID的本质

pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
深入理解Linux多线程_第8张图片
所谓的线程id(pthread_t)本质是一个地址,本质是动态库的地址Linux不提供真正的线程,只提供LWP,意味着OS只需要对LWP内核执行流进行管理,那么,供用户使用的接口等其它数据应该由谁来管理呢?-》pthread线程库来管理,在库里面,先描述,在组织。CPU在进行调度的时候,要上下文保存的时候,执行的代码,并不会跑到内核代码完成切换,而是到动态库里进行线程切换,线程切换是把当前线程在CPU的所有临时数据保存到线程的局部存储区,把形成的临时变量保存在栈里面,线程控制块被切走,切到下一个线程。此时,把下一个线程的数据恢复到CPU上。

Linux线程互斥

进程线程间的互斥相关背景概念

临界资源:多线程执行流共享的资源就叫做临界资源

临界区:每个线程内部,访问临界资源的代码,就叫做临界区

互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用

原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

对原子性理解:在汇编层面上,只有一行代码,可以称之为原子性,那么对一个全局变量进行++,或- -是原子的吗? -》不是

互斥量mutex

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。

但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。

多个线程并发的操作共享变量,会带来一些问题。

// 操作共享变量会有问题的售票系统代码

    1 #include<stdio.h>                                                                        
    2 #include<pthread.h>
    3 #include<unistd.h>
    4 #include<sys/types.h>
    5 #include<stdlib.h>
    int tickets=2000;
   55 
   56 void *TicketGrabbing(void *arg)
   57 {
   58   const char *name=(char*)arg;
   59   while(1)
   60   {
   61     //票大于0,才可以抢
   62     if(tickets>0)
   63     {
   64       usleep(1000);  //usleep()微秒级别 
   65       printf("[%s] get a ticket : %d\n",name,tickets--); // 抢到票打印出来
   66     }
   67     else 
   68     {
   69       break;                                                                             
   70     }
   71   }
   72   printf("%s quit!\n",name);
   73   pthread_exit((void*)0);
   74 }

 int main()
   76 {
   77   pthread_t t1,t2,t3,t4;
   78   // &t1:线程id;NULL:为默认属性;TicketGrabbing,回调函数,即想让线程执行什么逻辑
   79   // "thread 1",给回调函数指定的参i数
   80   pthread_create(&t1,NULL,TicketGrabbing, "thread 1");  
   81   pthread_create(&t2,NULL,TicketGrabbing, "thread 2");
   82   pthread_create(&t3,NULL,TicketGrabbing, "thread 3");
   83   pthread_create(&t4,NULL,TicketGrabbing, "thread 4");
   84  
   85   pthread_join(t1,NULL);
   86 
   87   pthread_join(t2,NULL);                                                                 
   88   pthread_join(t3,NULL);
   89   pthread_join(t4,NULL);
   90 }

深入理解Linux多线程_第9张图片
为什么还会有负数呢?票还有负的,因为tickets–,不是原子性的,可能当票还有1张的时候,有几个线程同时抢票,导致减了多次,成为负数
深入理解Linux多线程_第10张图片
要解决以上问题,需要做到三点:
1、代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。

2、如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。

3、如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
深入理解Linux多线程_第11张图片
加入互斥量改进抢票系统

    1#include<stdio.h>                                                                                                 
    2 #include<pthread.h>
    3 #include<unistd.h>
    4 #include<stdlib.h>
    5 #define NUM 2000
    6 
    7 int tickets=NUM;
    8 //定义互斥锁
    9 pthread_mutex_t lock;
   10 
   11 void *GetTicket(void *arg)
   12 {
   13   int number=(int)arg;
   14   while(1)
   15   {
   16       pthread_mutex_lock(&lock); //加锁
   17     if(tickets>0)
   18     {
   19       //****** 保护起来的临界区  ***********
   20       usleep(100);
   21       printf("thread [ %d ] 抢票: %d\n",number,tickets--);
   22 
   23       pthread_mutex_unlock(&lock); //解锁
   24     }
   25     else 
   26     {
   27         pthread_mutex_unlock(&lock); //解锁
   28         break;
   29     }
   30   }
   31 
   32 }
   33 
   34 int main()
   35 {
   36   pthread_t thds[5];
   37   pthread_mutex_init(&lock,NULL);//锁初始化
   38 
   39   for(int i=0;i<5;i++)
   40   {                                                                                                               
   41     pthread_create(&thds[i], NULL, GetTicket, (void*)i);
   42   }
   43 
   44   for(int i=0;i<5;i++)
   24     }
   25     else 
   26     {
   27         pthread_mutex_unlock(&lock); //解锁
   28         break;
   29     }
   30   }
   31 
   32 }
   33 
   34 int main()
   35 {
   36   pthread_t thds[5];
   37   pthread_mutex_init(&lock,NULL);//锁初始化
   38 
   39   for(int i=0;i<5;i++)
   40   {                                                                                                               
   41     pthread_create(&thds[i], NULL, GetTicket, (void*)i);
   42   }
   43 
   44   for(int i=0;i<5;i++)
   45   {
   46     pthread_join(thds[i],NULL);
   47   }
   48 
   49   pthread_mutex_destroy(&lock); //释放锁
   50   return 0;
   51 }                


深入理解Linux多线程_第12张图片

深入理解Linux多线程_第13张图片

互斥量实现原理探究

可重入VS线程安全

概念
1、线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

2、重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

常见的线程不安全的情况

不保护共享变量的函数

函数状态随着被调用,状态发生变化的函数

返回指向静态变量指针的函数

调用线程不安全函数的函数

常见的线程安全的情况

每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的

类或者接口对于线程来说都是原子操作

多个线程之间的切换不会导致该接口的执行结果存在二义性

常见不可重入的情况

调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的

调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构

可重入函数体内使用了静态的数据结构

常见可重入的情况

不使用全局变量或静态变量

不使用malloc或者new开辟出的空间

不调用不可重入函数

不返回静态或全局数据,所有数据都有函数的调用者提供

使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全联系

函数是可重入的,那就是线程安全的

函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题

如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全区别

可重入函数是线程安全函数的一种

线程安全不一定是可重入的,而可重入函数则一定是线程安全的。

如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。

死锁四个必要条件

互斥条件:一个资源每次只能被一个执行流使用

请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放

不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺

循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
深入理解Linux多线程_第14张图片

避免死锁

如和避免死锁呢?破坏死锁四个必要条件中的一个就可以

一般不会破环互斥条件:因为互斥锁就是用来保护临界资源的

破坏循环等待条件:加锁顺序一致,申请锁要按顺序来,先申请A再申请B

破坏请求与保持条件:我可以要求你的,但我也可以把我的给你,避免锁未释放的场景

资源一次性分配

Linux线程同步

条件变量

条件变量:用来描述某种临界资源是否就绪的一种数据化描述,条件变量通常需要配合mutex互斥锁一起使用。

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量

同步概念

同步:在特定的情况下,保证彼此之间能够按照特定的顺序进行资源访问,这就叫做同步,同步的本质就是让多线程协同起来,然后能够有效的避免竞争力比较弱的进程,因为长时间得不到锁资源,而引起的饥饿问题,就叫同步

互斥:互斥一定不会出错,但是可能导致竞争力比较弱的进程,因为长时间得不到锁资源,而引起的饥饿问题

深入理解Linux多线程_第15张图片

例,用主线程控制其它线程
深入理解Linux多线程_第16张图片

Makefile文件

  1 demo:demo.cc
  2   g++ -o $@ $^ -std=c++11 -lpthread                                                                                 
  3 .PHONY:clean
  4 clean:
  5   rm -f demo
~

main.cc文件

  1 #include<iostream>                                                                                                  
  2 #include<pthread.h>
  3 #include<cstdio>
  4 pthread_mutex_t lock;
  5 pthread_cond_t cond;
  6 
  7 void* Run(void* arg)
  8 {
  9   pthread_detach(pthread_self());  //将当前线程先分离
 10   std::cout<<(char*)arg<<" run..."<<std::endl;
 11   while(true)
 12   {
 13     pthread_cond_wait(&cond,&lock);  //线程阻塞在这,等条件就绪
 14     std::cout<<"thread: "<<pthread_self()<<" 活动..."<<std::endl;
 15   }
 16 }
 17 int main()
 18 {
 19   pthread_mutex_init(&lock,nullptr);
 20   pthread_cond_init(&cond,nullptr);  //初始化
 21 
 22   pthread_t t1,t2,t3; // ctrl是控制线程,用来控制其它3个线程
 23   pthread_create(&t1,nullptr,Run,(void*)"thread 1");
 pthread_create(&t2,nullptr,Run,(void*)"thread 2");
 25   pthread_create(&t3,nullptr,Run,(void*)"thread 3");
 26  
 27   //********* 主线程控制其它线程**************
 28   while(true)
 29   {
 30 
 31     getchar();
 32     //pthread_cond_signal(&cond);       //唤醒第一个线程
 33     pthread_cond_broadcast(&cond);            //唤醒全部线程
 34   }
 35 
 36 
 37   pthread_mutex_destroy(&lock);   //释放锁
 38   pthread_cond_destroy(&cond);    //释放条件变量
 39 
 40   return 0;
 41 }                                         

生产者消费者模型

生产以及消费对应的需要完成的功能主要是
一:互斥功能
二:条件不满足时,让当前执行流具有等待的功能
三:当我的条件满足时,要唤醒对方等待的线程,此时就需要有两种Linux提供的同步互斥机制,分别叫做互斥锁以及条件变量。

生产者消费者模型321原则
3:3种关系,消费者与消费者(竞争关系/互斥关系),生产者与生产者(竞争关系/互斥关系),
生产者与消费者(互斥关系和同步关系)
2:2种角色,生产者与消费者(特定的线程或进程)
1:1个交易 场所(通常是内存中的一段缓存区,自己通过某种方式组织起来)

为什么要有生产者消费者模型
实质是代码进行解耦的过程,生产者只负责生产数据,消费者只负责消费数据,支持并发,支持忙闲不均
深入理解Linux多线程_第17张图片

基于BlockingQueue的生产者消费者模型

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

深入理解Linux多线程_第18张图片
例,生产者生产任务,消费者消费任务
在这里插入图片描述
BlockQueue.hpp文件

 1 #pragma once                                                                                                        
  2 #include<iostream>
  3 #include<queue>
  4 #include<pthread.h>
  5 #include<cstdlib>
  6 #include<ctime>
  7 #include<unistd.h>
  8 
  9 #define NUM 5
 10 template<typename T>
 11 
 12 class BlockQueue
 13 {
 14   private:
 15     bool IsFull()
 16     {
 17       return q.size()==cap;
 18     }
 19 
 20     bool IsEmpty()
 21     {
 22       return q.empty();
 23     }
24   public:
 25     BlockQueue(int _cap=NUM)
 26       :cap(_cap)
 27     {
 28       pthread_mutex_init(&lock,nullptr);
 29       pthread_cond_init(&full,nullptr);  //条件变量初始化
 30       pthread_cond_init(&empty,nullptr);
 31     }
 32 
 33     void Push(const T& in)  //供生产者调用
 34     {
 35       pthread_mutex_lock(&lock);   //加锁
 36       while(IsFull())            //使用while是为了防止pthread_cond _wait调用失败
 37       {
 38         //满了,不能进行生产,等待q有空间可以容纳新的数据
 39         pthread_cond_wait(&full,&lock);  //满了在该条件变量下等待,在等待时,释放互斥锁
 40       }                                                                                                             
 41       q.push(in);
 42       if(q.size()>=cap/2)
 43       {
 44         //生产空间的一半就通知消费者
 45         pthread_cond_signal(&empty);  //唤醒消费者
 46         std::cout<<"数据已经很多了,消费者快来消费吧"<<std::endl;
 47       }
 48       pthread_mutex_unlock(&lock);  //解锁
 49     }
 50 
 51     void Pop(T& out)       //供消费者调用
 52     {
 53       pthread_mutex_lock(&lock);
 54 
 55       while(IsEmpty())
 56       {
 57         //不能进行消费,需要等待q有新的数据
 58         pthread_cond_wait(&empty,&lock); //等待,等待时,释放锁
 59       }
 60       out=q.front();
 61       q.pop();
 62       if(q.size()<=cap/2)
 63       {                                                                                                             
 64         //消费了一半就唤醒生产者
 65         pthread_cond_signal(&full);  //唤醒生产者
 66         std::cout<<"空间已经很多了,生产者快来生产吧"<<std::endl;
 67       }
 68       pthread_mutex_unlock(&lock);
  69     }
 70 
 71     ~BlockQueue()
 72     {
 73       pthread_mutex_destroy(&lock);
 74       pthread_cond_destroy(&full);
 75       pthread_cond_destroy(&empty);
 76     }
 77 
 78   private:
 79     std::queue<T> q;  //临界资源
 80     int cap;
 81     pthread_mutex_t lock;  //定义锁
 82     pthread_cond_t full;
 83     pthread_cond_t empty;                                                                                           
 84 
 85 };



main.cc文件

1 #include "BlockQueue.hpp"                                                                                         
    2 #include "Task.hpp"
    3 void* Consumer(void* arg)
    4 {
    5   auto bq=(BlockQueue<Task>*)arg;
    6   while(true)
    7   {
    8     sleep(1);
    9     Task t;
   10 
   11     bq->Pop(t);  //消费数据
   12     t.Run();
   13   
   14   }
   15 }
   16 
   17 void* Producter(void* arg)
   18 {
   19   auto bq=(BlockQueue<Task>*)arg;
   20   const char *arr="+-*/";
   21   while(true)
   22   {
   23     int x=rand()%100+1;
    24     int y=rand()%50;
   25     char op=arr[rand()%4];
   26     Task t(x,y,op);
   27     bq->Push(t);   //生产数据
   28     std::cout<<"producter task done"<<std::endl;
   29   }
   30 }
   31 
   32 int main()
   33 {
   34 
   35   srand((unsigned long)time(nullptr));
   36   BlockQueue<Task> *bq=new BlockQueue<Task>();
   37   pthread_t c,p;    //创建两个线程
   38 
   39   pthread_create(&c,nullptr,Consumer,bq);      //消费者
   40   pthread_create(&c,nullptr,Producter,bq);    //生产者
   41                                                                                                                   
   42   pthread_join(c,nullptr);    //等待消费者
   43   pthread_join(p,nullptr);    //等待生产者
   44   return 0;
   45 }


Makefile文件

  1 cp:main.cc
  2   g++ -o $@ $^ -std=c++11 -lpthread
  3 .PHONY:clean
  4 clean:
  5   rm -f cp                                                                                                          

Task.hpp文件

1 #pragma once 
  2 #include<iostream>
  3 class Task
  4 {
  5   private:
  6     int x;
  7     int y;
  8     char op;
  9   public:
 10     Task(int _x,int _y,char _op)
 11       :x(_x)
 12       ,y(_y)
 13       ,op(_op)
 14       {}
 15     Task()
 16     {}                                                                                                              
 17     void  Run()
 18     {
 19       int result=0;
 20       switch(op)
 21       {
 22         case '+':
 23           result=x+y;
 24           break;
 25         case '-':
 26           result=x-y;
 27           break;
 28         case '*':
 29           result=x*y;
 30           break;
 31        case '/':
 32           if(y==0)
 33           {
 34             std::cout<<"Waring : div zero! "<<std::endl;
 35             result=-1;
 36           }
 37           else 
 38           {
 39           result=x/y;
 40           }                                                                                                         
 41           break;
 42         default:
 43           break;
 44       }
 45       std::cout<<x<<op<<y<<"="<<result<<std::endl;
 46     }
 47     ~Task()
 48     {}
 49        
 50 };              

效果实现
深入理解Linux多线程_第19张图片

POSIX信号量

POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。

什么是信号量:信号量本质是一个计数器,描述临界资源中资源数目的计数器

信号量存在的价值:1、同步互斥 2、更细粒度的临界资源的管理

申请到信号量的本质:并不是已经开始使用临界资源中你所申请的那个区域,而是有了使用特定资源的权限。

申请信号量的本质,让计数器–, P操作
释放信号量的本质,让计数器++, V操作

本质上信号量是一种临界资源,信号量的PV操作必须是原子的

信号量是一个计数器,如果是sem的值是1呢? -》基本等价于互斥锁,(二元信号量)

用二元信号量模拟互斥锁实现抢票逻辑
深入理解Linux多线程_第20张图片
Makefile文件

1 Getticket:Getticket.cc
  2   g++ -o $@ $^ -lpthread -std=c++11                                                                                 
  3 .PHONY:clean
  4 clean:
  5   rm -f Getticket
~

main.cc文件

 1 #include<iostream>                                                                                                  
  2 #include<semaphore.h>
  3 #include<pthread.h>
  4 #include<unistd.h>
  5 #include<string>
  6 
  7 // ****************  二元信号量模拟互斥锁抢票   ******************
  8 class Sem
  9 {
 10   private:
 11     sem_t sem;
 12   public:
 13     Sem(int num)
 14     {
 15       sem_init(&sem,0,num);  //0,是指线程共享,num是信号量个数
 16     }
 17     void P()
 18     {
 19       sem_wait(&sem);    //申请信号量
 20     }
 21 
 22     void V()
 23     {
 24       sem_post(&sem);  //释放信号量
 25     }
 26 
 27     ~Sem()
 28     {
 29       sem_destroy(&sem); 
 30     }
 31 };
 32 
 33 Sem sem(1);  //1-》二元信号量
 34 int tickets=2000;
 35 
 36 void* GetTickets(void* arg)
 37 {
 38   std::string name=(char*)arg;
 39   while(true)
 40   {                                                                                                                 
 41     sem.P();    //相当于申请锁
 42     if(tickets>0)
 43     {
 44       usleep(10000);
 45       std::cout<<name<< " get ticket: "<<tickets--<<std::endl;
 46       sem.V();  //相当于释放锁
 47     }
 48     else
 49     {
 50       sem.V();
 51       break;
 52     }
 53   }
 54   std::cout<<name <<" quit"<<std::endl;
 55   pthread_exit((void*)0);
 56 }
 57 int main()
 58 {
 59   pthread_t tid1,tid2,tid3,tid4,tid5,tid6;
 60   pthread_create(&tid1,nullptr,GetTickets, (void*)"thread 1");
 61   pthread_create(&tid2,nullptr,GetTickets, (void*)"thread 2");
 62   pthread_create(&tid3,nullptr,GetTickets, (void*)"thread 3");
 63   pthread_create(&tid4,nullptr,GetTickets, (void*)"thread 4");                                                      
 64   pthread_create(&tid5,nullptr,GetTickets, (void*)"thread 5");
 65   pthread_create(&tid6,nullptr,GetTickets, (void*)"thread 6");
 66 
 67   pthread_join(tid1,nullptr);
 68   pthread_join(tid2,nullptr);
  69   pthread_join(tid3,nullptr);
 70   pthread_join(tid4,nullptr);
 71   pthread_join(tid5,nullptr);
 72   pthread_join(tid6,nullptr);
 73 
 74   return 0;
 75 
 76 }                                  

基于环形队列的生产消费模型

深入理解Linux多线程_第21张图片

代码实现
Makefile文件

  1 Ring:main.cc                                                                                                        
  2   g++ -o $@ $^ -lpthread -std=c++11
  3 .PHONY:clean
  4 clean:
  5   rm -f Ring

main.cc文件

1 #include "Ring.hpp"                                                                                                 
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 void *consum(void *arg)
  5 {
  6   RingQueue<int> *rq=(RingQueue<int>*)arg;
  7   while(true)
  8   {
  9     sleep(1);
 10     int x=0;
 11     rq->Pop(x);
 12     std::cout<< "consume done" <<x<<std::endl;
 13   }
 14 }
 15 
 16 void *product(void *arg)
 17 {
 18   RingQueue<int> *rq=(RingQueue<int>*)arg;
 19   while(true)
 20   {
 21     int x=rand()%100+1;
 22     rq->Push(x);
 23     std::cout<<"product done "<<x<<std::endl;
 24   }
 25 }
 26 int main()
 27 {
 28   srand((unsigned long)time(nullptr));
 29   RingQueue<int> *rq=new RingQueue<int>();
 30   pthread_t c,p;
 31   pthread_create(&c,nullptr,consum,rq);
 32   pthread_create(&p,nullptr,product,rq);
 33 
 34 
 35   pthread_join(c,nullptr);
 36   pthread_join(p,nullptr);
 37   return 0;
 38 }                                             

Ring.hpp文件

1 #pragma  once                                                                                                       
  2 #include<iostream>
  3 #include<vector>
  4 #include<pthread.h>
  5 #include<semaphore.h>
  6 
  7 // ***********  创建环形队列  ******************
  8 #define NUM 5
  9 template<typename T>
 10 class RingQueue
 11 {
 12   private:
 13     std::vector<T> q;
 14     int cap;
 15     int c_pos;   //消费位置
 16     int p_pos;   //生产位置
 17     sem_t blank_sem;
 18     sem_t data_sem;
 19   private:
 20     void P(sem_t &s)
 21     {
 22       sem_wait(&s);
 23     }
 24 
 25     void V(sem_t &s)
 26     {
 27       sem_post(&s);
 28     }
 29   public:
 30   RingQueue(int _cap=NUM):cap(_cap),c_pos(0),p_pos(0)
 31   {
 32     q.resize(cap);
 33     sem_init(&blank_sem,0,cap);//刚开始时空间资源是cap
 34     sem_init(&data_sem,0,0);   //刚开始时,数据资源是0
 35   }
 36   
 37   //生产者调用,生产者关心blank(空间)
 38   void Push(const T& in)
 39   {
 40     P(blank_sem);   //申请blank空间                                                                                 
 41     q[p_pos]=in;
 42     V(data_sem);   //释放空间
 43     p_pos++;
 44     p_pos %=cap;
 45   }
 46   //消费者调用,消费数据,关心data数据
 47   void Pop(T& out)
 48   {
 49     P(data_sem);
 50     out=q[c_pos];
 51     V(blank_sem);
 52     c_pos++;
 53     c_pos %=cap;
 54   }
 55 
 56   ~RingQueue()
 57   {
 58     sem_destroy(&blank_sem);
 59     sem_destroy(&data_sem);
 60   }
 61 };                                         


结果演示
深入理解Linux多线程_第22张图片

你可能感兴趣的:(Linux多线程详解,线程VS进程,可重入与线程安全,互斥量和条件变量,生产者消费者模型)