【Linux从青铜到王者】第十三篇:Linux多线程四万字详解

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第1张图片

系列文章目录


文章目录

  • 系列文章目录
  • 前言
  • 一、Linux线程概念
      • 1.什么是线程
      • 2.线程的优点
      • 3.线程的缺点
      • 4.线程的异常
      • 5.线程的用途
  • 二、进程和线程的对比
      • 1.进程和线程
      • 2.多进程的应用场景有哪些?
  • 三、线程控制
      • 1.POSIX线程库
      • 2.创建线程
      • 3.进程ID和线程ID
      • 4.线程ID及进程地址空间布局
      • 5.线程终止
      • 6.线程等待
      • 7.线程分离
  • 四 、线程互斥
      • 1.进程线程间互斥相关概念
      • 2.线程安全
      • 3.线程不安全
        • 1.线程安全的++操作
        • 2.线程不安全的++操作
      • 3.线程不安全代码实现(黄牛抢票)
      • 4.互斥量mutex
      • 5.互斥锁的原理
        • 1.什么是互斥锁
        • 2.互斥锁逻辑
        • 3.加锁逻辑
      • 6.互斥锁的接口
        • 1.初始化互斥锁
        • 2.销毁互斥锁
        • 3.互斥锁的加锁和解锁
          • 1.阻塞加锁接口
          • 2.非阻塞加锁接口
          • 3.带有超时时间的加锁接口
          • 4.解锁接口
        • 4.互斥锁的使用
          • 1.什么时候使用初始化互斥锁
          • 2.什么时候使用销毁互斥锁
          • 3.什么时候使用加锁
          • 4.什么时候使用解锁
          • 5.加锁之后不解锁
          • 6.gdb调试:哪个工作线程加锁之后不解锁
        • 5.正确互斥锁的使用
      • 7.死锁
        • 1.死锁的定义
        • 2.死锁的模拟实现
        • 3.死锁生成的4个必要条件
        • 4.避免死锁的条件(满足其中一个条件就可以)
        • 5.避免死锁的算法
  • 五 、线程同步
      • 1.模拟加锁的未加条件变量单个人吃面例子
      • 2.条件变量
        • 1.PCB等待队列
      • 3.条件变量函数
        • 1.条件变量初始化
        • 2.条件变量销毁
        • 3.条件变量唤醒
        • 4.条件变量等待
      • 4.模拟加锁的加条件变量单个人吃面例子
      • 5.模拟加锁的加条件变量多个人吃面例子
      • 6.条件变量关于等待接口的几个问题
        • 1.条件变量的等待接口参数为什么需要互斥锁?
        • 2.pthread_cond_wait函数的实现原理
        • 3.线程等待的时候,被唤醒之后,需要做什么事情
  • 六、生产者消费者模型
      • 1.生产者、消费者遵循的原则
      • 2.生产者、消费者遵循的原则
      • 3.生产者消费者模型优点
      • 4.基于BlockingQueue的生产者消费者模型
      • 5.基于BlockingQueue的生产者消费者代码实现
  • 七、POSIX信号量
      • 1.POSIX信号量的概念
      • 2.POSIX信号量初始化
      • 3.POSIX信号量销毁
      • 4.POSIX信号量等待
      • 5.POSIX信号量发布
      • 6.基于环形队列的生产消费模型
      • 7.基于环形队列的生产消费模型代码实现
  • 八、线程池
      • 1.线程池概念
      • 2.线程池作用
      • 3.线程池应用场景
      • 4.线程池示例
      • 5.基于队列的线程池实现代码
      • 6.线程池惊群问题
  • 九、线程安全的单例模式
      • 1.单例模式概念
      • 2.单例模式特点
      • 3.饿汉方式实现单例模式
      • 4.懒汉方式实现单例模式
      • 5.懒汉方式实现单例模式(线程安全版本)
  • 十、STL智能指针和线程安全
      • 1.STL中的容器是否是线程安全
  • 十一、其他常见的各种锁
  • 总结


前言


【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第2张图片

一、Linux线程概念

1.什么是线程

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列。
  • 一切进程至少都有一个执行线程。
  • 线程在进程内部运行,本质是在进程地址空间内运行。
  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第3张图片

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第4张图片

2.线程的优点

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第5张图片

  • 创建一个新线程的代价要比创建一个新进程小得多。
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
  • 线程占用的资源要比进程少很多。
  • 能充分利用多处理器的可并行数量。
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

3.线程的缺点

在这里插入图片描述

  • 性能损失
  • 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  • 健壮性降低
  • 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
  • 缺乏访问控制
  • 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
  • 编程难度提高
  • 编写与调试一个多线程程序比单线程程序困难得多

4.线程的异常

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第6张图片

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
     1	#include<iostream>
     2	#include<pthread.h>
     3	#include<unistd.h>
     4	#include<sys/types.h>
     5	using namespace std;
     6	
     7	void* thread_run(void* arg)
     8	{
     9	    pthread_detach(pthread_self());
    10	    while(1)
    11	    {
    12	        cout<<(char*)arg<<pthread_self()<<" pid:"<<getpid()<<endl;
    13	        sleep(1);
    14	        break;
    15	    }
    16	    int a=10;
    17	    a=a/0;
    18	    return (void*)10;
    19	}
    20	int main()
    21	{
    22	    pthread_t tid;
    23	    int ret=0;
    24	    ret= pthread_create(&tid,NULL,thread_run,(void*)"thread 1");
    25	    if(ret!=0)
    26	    {
    27	        return -1;
    28	    }   
    29	    
    30	    sleep(10);
    31	    pthread_cancel(tid);
    32	    cout<<"new thread "<<tid<<" be cancled!"<<endl;
    33	    void* tmp=NULL;
    34	   pthread_join(tid,&tmp);
    35	   cout<<"thread qiut code:"<<(long long )ret<<endl;
    36	    return 100;
    37	}

在这里插入图片描述
在这里插入图片描述

5.线程的用途

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

二、进程和线程的对比

1.进程和线程

在这里插入图片描述

  • 进程是资源分配的基本单位
  • 线程是调度的基本单位
  • 线程共享进程数据,但也拥有自己的一部分数据
    【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第7张图片
  • 进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境。
    【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第8张图片

2.多进程的应用场景有哪些?

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第9张图片

三、线程控制

1.POSIX线程库

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

2.创建线程

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第10张图片

  • int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
  • pthread_t:线程标识符,本质上是线程在共享区独有空间的首地址。
    -pthread_t:是一个出参,该值由pthread_creat函数赋值的。
    -thread:创建线程的属性,一般情况都指定为NULL,采用默认属性。
  • pthread_attr_t:函数指针,接收一个返回值为void*,参数为void*的函数地址,就是线程入口函数。
  • void *(*start_routine) (void *):给线程入口函数传递的参数;由于参数类型是void*,返回值类型为void*,所以给了程序无限的传递参数的方式(char*,int*,结构体指针,this)
  • 返回值
    失败:< 0

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第11张图片
在主线程中创建一个工作线程,主线程和副线程都不退出。
mythead.cpp

     1	#include<iostream>
     2	#include<pthread.h>
     3	using namespace std;
     4	#include<unistd.h>
     5	
     6	void* thread_run(void* arg)
     7	{
     8	    while(1)
     9	    {
    10	        cout<<"i am "<<(char*)arg<<endl;
    11	        sleep(1);
    12	    }
    13	}
    14	int main()
    15	{
    16	    pthread_t tid;
    17	    int ret=pthread_create(&tid,NULL,thread_run,(void*)"thread 1");
    18	    if(ret!=0)
    19	    {
    20	        return -1;
    21	    }
    22	    while(1)
    23	    {
    24	        cout<<"i am main thread"<<endl;
    25	        sleep(2);
    26	    }
    27	    return 0;
    28	}

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第12张图片
makefile

     1	mythread:mythread.cpp
     2		g++ $^ -o $@ -lpthread
     3	.PHONY:clean
     4	clean:
     5		rm -f mythread

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第13张图片
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第14张图片
在这里插入图片描述
在主线程中创建一个副线程,让主线程退出,副线程不退出。

     1	#include<iostream>
     2	#include<pthread.h>
     3	using namespace std;
     4	#include<unistd.h>
     5	
     6	void* thread_run(void* arg)
     7	{
     8	    while(1)
     9	    {
    10	        cout<<"i am "<<(char*)arg<<endl;
    11	        sleep(1);
    12	    }
    13	}
    14	int main()
    15	{
    16	    pthread_t tid;
    17	    int ret=pthread_create(&tid,NULL,thread_run,(void*)"thread 1");
    18	    if(ret!=0)
    19	    {
    20	        return -1;
    21	    }
    22	    return 0;
    23	}

下图可以看到主线程退出来,进程也退出。
在这里插入图片描述

  • 传参问题验证:
  • 假设要往创建的工作线程中传入一个参数1,首先要将参数强转为(void*)类型,然后将参数的地址传入,而在工作线程中使用是只需将(void*)转换为(int*)即可。
     1	#include<iostream>
     2	#include<unistd.h>
     3	#include<pthread.h>
     4	using namespace std;
     5	
     6	void* MyThreadStrat(void* arg)
     7	{
     8	    int* i=(int*)arg;
     9	    while(1)
    10	    {
    11	        cout<<"MyThreadStrat:"<<*i<<endl;
    12	        sleep(1);
    13	    }
    14	    return NULL;
    15	}
    16	int main()
    17	{
    18	    pthread_t tid;
    19	    int i=1;
    20	        int ret=pthread_create(&tid,NULL,MyThreadStrat,(void*)&i);
    21	        if(ret!=0)
    22	        {
    23	            cout<<"线程创建失败!"<<endl;
    24	            return 0;
    25	        }
    26	    while(1)
    27	    {
    28	        sleep(1);
    29	        cout<<"i am main thread"<<endl;
    30	    }
    31	    return 0;
    32	}

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第15张图片
虽然参数可以正常传入,但实际是存在一定的错误的,因为局部变量 i 传入的时候生命周期未结束,而在传递给工作线程的时候生命周期结束了,那么这块局部变量开辟的区域就会自动释放,而此时工作线程还在访问这块地址,就会出现非法访问。

代码改成循环:

     1	#include<iostream>
     2	#include<unistd.h>
     3	#include<pthread.h>
     4	using namespace std;
     5	
     6	void* MyThreadStrat(void* arg)
     7	{
     8	    int* i=(int*)arg;
     9	    while(1)
    10	    {
    11	        cout<<"MyThreadStrat:"<<*i<<endl;
    12	        sleep(1);
    13	    }
    14	    return NULL;
    15	}
    16	int main()
    17	{
    18	    pthread_t tid;
    19	    int i=0;
    20	    for( i=0;i<4;i++)
    21	    {
    22	        int ret=pthread_create(&tid,NULL,MyThreadStrat,(void*)&i);
    23	        if(ret!=0)
    24	        {
    25	            cout<<"线程创建失败!"<<endl;
    26	            return 0;
    27	        }
    28	    }
    29	    while(1)
    30	    {
    31	        sleep(1);
    32	        cout<<"i am main thread"<<endl;
    33	    }
    34	    return 0;
    35	}

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第16张图片
因为for循环4次最终开辟4个工作线程,开辟线程传递进去的是 i 的地址,而 i 中的值从0加到4,而 i 到5退出,此时 i 已经被加为4,最终 i 的地址中存的值为 4,使用最终会一直输出4。

问题的解决---->动态内存开辟:
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第17张图片
传递this指针:

class MyThread
{
    public:
        MyThread()
        {

        }

        ~MyThread()
        {

        }

        int Start()
        {
            int ret = pthread_create(&tid_, NULL, MyThreadStart, (void*)this);
            if(ret < 0)
            {
                return -1;
            }

            return 0;
        }

        static void* MyThreadStart(void* arg)
        {
            MyThread* mt = (MyThread*)arg;
            printf("%p\n", mt->tid_);
        }
    private:
        pthread_t tid_;
};

int main()
{
    return 0;
}

传递结构体指针:

     1	#include<iostream>
     2	#include<unistd.h>
     3	#include<pthread.h>
     4	using namespace std;
     5	
     6	struct ThreadId
     7	{
     8	    int thread_id;
     9	};
    10	void* MyThreadStrat(void* arg)
    11	{
    12	    struct ThreadId* tid=(struct ThreadId*)arg;
    13	    while(1)
    14	    {
    15	        cout<<"MyThreadStrat:"<<tid->thread_id<<endl;
    16	        sleep(1);
    17	    }
    18	    delete tid;
    19	}
    20	int main()
    21	{
    22	    pthread_t tid;
    23	    int i=0;
    24	    for( i=0;i<4;i++)
    25	    {
    26	        struct ThreadId* id=new ThreadId();
    27	        id->thread_id=i;
    28	        int ret=pthread_create(&tid,NULL,MyThreadStrat,(void*)id);
    29	        if(ret!=0)
    30	        {
    31	            cout<<"线程创建失败!"<<endl;
    32	            return 0;
    33	        }
    34	    }
    35	    while(1)
    36	    {
    37	        sleep(1);
    38	        cout<<"i am main thread"<<endl;
    39	    }
    40	    return 0;
    41	}

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第18张图片
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第19张图片

3.进程ID和线程ID

  • 在Linux中,目前的线程实现是Native POSIX Thread Libaray,简称NPTL。在这种实现下,线程又被称为轻量级进程(Light Weighted Process),每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)。
  • 没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程概念之后,情况发生了变化,一个用户进程下管辖N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核的描述符一下子就变成了1:N关系,POSIX标准又要求进程内的所有线程调用getpid函数时返回相同的进程ID,如何解决上述问题呢?
  • 多线程的进程,又被称为线程组,线程组内的每一个线程在内核之中都存在一个进程描述符(task_struct)与之对应。进程描述符结构体中的pid,表面上看对应的是进程ID,其实不然,它对应的是线程ID;进程描述符中的tgid,含义是Thread Group ID,该值对应的是用户层面的进程ID。
    【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第20张图片
    【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第21张图片
     1	#include<iostream>
     2	#include<pthread.h>
     3	using namespace std;
     4	#include<sys/types.h>
     5	#include<unistd.h>
     6	
     7	void* thread_run(void* arg)
     8	{
     9	    while(1)
    10	    {
    11	        cout<<"i am "<<(char*)arg<<"pid:"<<getpid()<<endl;
    12	        sleep(1);
    13	    }
    14	}
    15	int main()
    16	{
    17	    pthread_t tid;
    18	    int ret=pthread_create(&tid,NULL,thread_run,(void*)"thread 1");
    19	    if(ret!=0)
    20	    {
    21	        return -1;
    22	    }
    23	    while(1)
    24	    {
    25	        cout<<"i am main thread,pid:"<<getpid()<<endl;
    26	        sleep(2);
    27	    }
    28	    return 0;
    29	}

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第22张图片

  • ps命令中的-L选项,会显示如下信息:

  • LWP:线程ID,既gettid()系统调用的返回值。

  • NLWP:线程组内线程的个数。
    【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第23张图片

  • 从上面可以看出,进程的ID为12878,下面有一个线程的ID也是12878,这不是巧合。线程组内的第一个线程,在用户态被称为主线程(main thread),在内核中被称为group leader,内核在创建第一个线程时,会将线程组的ID的值设置成第一个线程的线程ID,group_leader指针则指向自身,既主线程的进程描述符。所以线程组内存在一个线程ID等于进程ID,而该线程即为线程组的主线程。

  • 至于线程组其他线程的ID则有内核负责分配,其线程组ID总是和主线程的线程组ID一致,无论是主线程直接创建线程,还是创建出来的线程再次创建线程,都是这样。

  • 强调一点,线程和进程不一样,进程有父进程的概念,但在线程组里面,所有的线程都是对等关系。

4.线程ID及进程地址空间布局

  • pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
  • 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
  • pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
  • 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID。
    【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第24张图片
     1	#include<iostream>
     2	#include<pthread.h>
     3	using namespace std;
     4	#include<sys/types.h>
     5	#include<unistd.h>
     6	
     7	void* thread_run(void* arg)
     8	{
     9	    while(1)
    10	    {
    11	        cout<<"i am:"<<(char*)arg<<"pid:"<<getpid()<<" "<<"my thread id is:"<<pthread_self()<<endl;
    12	        sleep(1);
    13	    }
    14	}
    15	int main()
    16	{
    17	    pthread_t tid;
    18	    int ret=pthread_create(&tid,NULL,thread_run,(void*)"thread 1");
    19	    if(ret!=0)
    20	    {
    21	        return -1;
    22	    }
    23	    while(1)
    24	    {
    25	        cout<<"i am main thread id:"<<pthread_self()<<" "<<"new thread:"<<tid<<" "<<"pid:"<<getpid()<<endl;
    26	        sleep(2);
    27	    }
    28	    return 0;
    29	}

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第25张图片
pthread_t到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第26张图片

5.线程终止

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第27张图片

  • 如果需要只终止某个线程而不终止整个进程,可以有三种方法:
  • 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
     1	#include<iostream>
     2	#include<pthread.h>
     3	#include<unistd.h>
     4	#include<sys/types.h>
     5	using namespace std;
     6	
     7	void* thread_run(void* arg)
     8	{
     9	  //  while(1)
    10	   // {
    11	        cout<<(char*)arg<<pthread_self()<<" pid:"<<getpid()<<endl;
    12	        sleep(1);
    13	        return (void*)10;
    14	   // }
    15	}
    16	int main()
    17	{
    18	    pthread_t tid;
    19	    int ret=0;
    20	    ret= pthread_create(&tid,NULL,thread_run,(void*)"thread 1");
    21	    if(ret!=0)
    22	    {
    23	        return -1;
    24	    }
    25	
    26	    while(1)
    27	    {
    28	        cout<<"i am main:"<<pthread_self()<<" pid:"<<getpid()<<endl;
    29	        sleep(2);
    30	    }
    31	   void* tmp=NULL;
    32	   pthread_join(tid,&tmp);
    33	   cout<<"thread eixt code:"<<(long long) tmp<<endl;
    34	    return 0;
    35	}

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第28张图片

  • 线程可以调用pthread_ exit终止自己,谁调用谁退出。

功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第29张图片

     1	#include<iostream>
     2	#include<pthread.h>
     3	#include<unistd.h>
     4	#include<sys/types.h>
     5	using namespace std;
     6	
     7	void* thread_run(void* arg)
     8	{
     9	//    while(1)
    10	 //   {
    11	        cout<<(char*)arg<<pthread_self()<<" pid:"<<getpid()<<endl;
    12	        sleep(1);
    13	        pthread_exit((void*)10);
    14	  //  }
    15	}
    16	int main()
    17	{
    18	    pthread_t tid;
    19	    int ret=0;
    20	    ret= pthread_create(&tid,NULL,thread_run,(void*)"thread 1");
    21	    if(ret!=0)
    22	    {
    23	        return -1;
    24	    }
    25	
    26	    while(1)
    27	    {
    28	        cout<<"i am main:"<<pthread_self()<<" pid:"<<getpid()<<endl;
    29	        sleep(2);
    30	    }
    31	   void* tmp=NULL;
    32	   pthread_join(tid,&tmp);
    33	   cout<<"thread quit code:"<<(long long)ret<<endl;
    34	    return 0;
    35	}

在这里插入图片描述

  • 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
    【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第30张图片

功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码

     1	#include<iostream>
     2	#include<pthread.h>
     3	#include<unistd.h>
     4	#include<sys/types.h>
     5	using namespace std;
     6	
     7	void* thread_run(void* arg)
     8	{
     9	    while(1)
    10	    {
    11	        cout<<(char*)arg<<pthread_self()<<" pid:"<<getpid()<<endl;
    12	        sleep(10);
    13	    }
    14	}
    15	int main()
    16	{
    17	    pthread_t tid;
    18	    int ret=0;
    19	    ret= pthread_create(&tid,NULL,thread_run,(void*)"thread 1");
    20	    if(ret!=0)
    21	    {
    22	        return -1;
    23	    }   
    24	    
    25	    sleep(10);
    26	    pthread_cancel(tid);
    27	    cout<<"new thread "<<tid<<" be cancled!"<<endl;
    28	    void* tmp=NULL;
    29	   pthread_join(tid,&tmp);
    30	   cout<<"thread qiut code:"<<(long long )ret<<endl;
    31	    return 0;
    32	}

在这里插入图片描述

6.线程等待

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第31张图片
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第32张图片

  • 为什么需要线程等待?
  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复用刚才退出线程的地址空间。

功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码

     1	#include<iostream>
     2	#include<pthread.h>
     3	#include<unistd.h>
     4	#include<sys/types.h>
     5	using namespace std;
     6	
     7	void* thread_run(void* arg)
     8	{
     9	    while(1)
    10	    {
    11	        cout<<(char*)arg<<pthread_self()<<" pid:"<<getpid()<<endl;
    12	        sleep(10);
    13	    }
    14	}
    15	int main()
    16	{
    17	    pthread_t tid;
    18	    int ret=0;
    19	    ret= pthread_create(&tid,NULL,thread_run,(void*)"thread 1");
    20	    if(ret!=0)
    21	    {
    22	        return -1;
    23	    }
    24	
    25	   // while(1)
    26	   // {
    27	        cout<<"i am main:"<<pthread_self()<<" pid:"<<getpid()<<endl;
    28	        sleep(2);
    29	   // }
    30	   void* tmp=NULL;
    31	   pthread_join(tid,&tmp);
    32	    return 0;
    33	}

在这里插入图片描述

7.线程分离

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第33张图片
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第34张图片

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
     1	#include<iostream>
     2	#include<pthread.h>
     3	#include<unistd.h>
     4	#include<sys/types.h>
     5	using namespace std;
     6	
     7	void* thread_run(void* arg)
     8	{
     9	    pthread_detach(pthread_self());
    10	    while(1)
    11	    {
    12	        cout<<(char*)arg<<pthread_self()<<" pid:"<<getpid()<<endl;
    13	        sleep(1);
    14	        break;
    15	    }
    16	    return (void*)10;
    17	}
    18	int main()
    19	{
    20	    pthread_t tid;
    21	    int ret=0;
    22	    ret= pthread_create(&tid,NULL,thread_run,(void*)"thread 1");
    23	    if(ret!=0)
    24	    {
    25	        return -1;
    26	    }   
    27	    
    28	    sleep(10);
    29	    pthread_cancel(tid);
    30	    cout<<"new thread "<<tid<<" be cancled!"<<endl;
    31	    void* tmp=NULL;
    32	   pthread_join(tid,&tmp);
    33	   cout<<"thread qiut code:"<<(long long )ret<<endl;
    34	    return 100;
    35	}

在这里插入图片描述
分离的线程在同一个进程地址空间,相互的线程不会想回干扰,但是如果分离的线程崩溃,进程也会崩溃。

     1	#include<iostream>
     2	#include<pthread.h>
     3	#include<unistd.h>
     4	#include<sys/types.h>
     5	using namespace std;
     6	
     7	void* thread_run(void* arg)
     8	{
     9	    pthread_detach(pthread_self());
    10	    while(1)
    11	    {
    12	        cout<<(char*)arg<<pthread_self()<<" pid:"<<getpid()<<endl;
    13	        sleep(1);
    14	        break;
    15	    }
    16	    int a=10;
    17	    a=a/0;
    18	    return (void*)10;
    19	}
    20	int main()
    21	{
    22	    pthread_t tid;
    23	    int ret=0;
    24	    ret= pthread_create(&tid,NULL,thread_run,(void*)"thread 1");
    25	    if(ret!=0)
    26	    {
    27	        return -1;
    28	    }   
    29	    
    30	    sleep(10);
    31	    pthread_cancel(tid);
    32	    cout<<"new thread "<<tid<<" be cancled!"<<endl;
    33	    void* tmp=NULL;
    34	   pthread_join(tid,&tmp);
    35	   cout<<"thread qiut code:"<<(long long )ret<<endl;
    36	    return 100;
    37	}

在这里插入图片描述

四 、线程互斥

1.进程线程间互斥相关概念

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第35张图片

  • 临界资源:多线程执行流共享的资源就叫做临界资源。
  • 临界区:每个线程内部,访问临界自娱的代码,就叫做临界区。
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
     1	#include<iostream>
     2	#include<pthread.h>
     3	#include<unistd.h>
     4	#include<sys/types.h>
     5	using namespace std;
     6	
     7	int a=10;
     8	void* thread_run(void* arg)
     9	{
    10	    while(1)
    11	    {
    12	        cout<<(char*)arg<<pthread_self()<<" pid:"<<getpid()<<endl;
    13	        cout<<(char*)arg<<" global-variable:"<<a<<" &a:"<<&a<<endl;
    14	        sleep(1);
    15	    }
    16	    return (void*)10;
    17	}
    18	int main()
    19	{
    20	    pthread_t tid;
    21	    pthread_t tid1;
    22	    pthread_create(&tid,NULL,thread_run,(void*)"thread 1");
    23	    pthread_create(&tid1,NULL,thread_run,(void*)"thread 1");
    24	    cout<<"main:"<<pthread_self()<<" pid:"<<getpid()<<endl;
    25	    
    26	    cout<<"before:"<<main<<" global-variable:"<<a<<" %p:"<<&a<<endl;
    27	    sleep(5);
    28	    a=100;
    29	    cout<<"after:"<<main<<" globa-variable:"<<a<<" %p:"<<&a<<endl;
    30	
    31	    pthread_cancel(tid);
    32	    void* tmp=NULL;
    33	   pthread_join(tid,&tmp);
    34	   cout<<"thread qiut code:"<<(long long )tmp<<endl;
    35	    return 100;
    36	}

这里对临界资源进行了修改。
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第36张图片

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

2.线程安全

多个线程并发同一段代码时,不会出现不同的结果。多个执行流,访问临界资源,不会导致程序产生二义性。

  • 执行流:理解为线程
  • 访问:指的是对临界资源进行操作
  • 临界资源:指的是多个线程都可以访问到的资源
  • eg:全局变量,某个结构体(不能是定义在某个线程入口函数内),某个类的实例化指针
  • 临界区:代码操作临界资源的代码区域称之为临界区
  • 二义性:结果会有多个

3.线程不安全

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第37张图片

1.线程安全的++操作

正常情况,假设我们定义一个变量 i 这个变量 i 一定是保存在内存的栈当中的,我们要对这个变量 i 进行计算的时候,是CPU(两大核心功能:算术运算和逻辑运算)来计算的,假设要对变量 i = 10 进行 +1 操作,首先要将内存栈中的 i 的值为 10 告知给寄存器,此时,寄存器中就有一个值 10,让后让CPU对寄存器中的这个 10 进行 +1 操作,CPU +1 操作完毕后,将结果 11 回写到寄存器当中,此时寄存器中的值被改为 11,然后将寄存器中的值回写到内存当中,此时 i 的值为 11。
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第38张图片

2.线程不安全的++操作

  • 基于 3.1 正常变量操作的原理描述的,分以下几步描述:
  • 假设场景,有两个线程,每个线程进行++操作。

假设有两个线程,线程A和线程B,线程A和线程B都想对全局变量 i 进行++。

  • 在两个线程描述,体现出:线程切换,上下文信息,程序计数器。

假设全局变量 i 的值为 10,线程A从内存中把全局变量 i = 10 读到寄存器当中,此时,线程A的时间片到了,线程A被切换出来了,线程A的上下文信息中保存的是寄存器中的i = 10,程序计数器中保存的是下一条即将要执行的 ++ 指令,若此时线程B获取了CPU资源,也想对全局变量 i 进行 ++ 操作,因为此时线程A并未将运算结果返回到内存当中,所以线程B从内存当中读到的全局变量 i 的值还是10,然后将 i 的值读到寄存器中,然后再在CPU中进行 ++ 操作,然后将 ++ 后的结果 11,回写到寄存器,寄存器再回写到内存,此时内存当中 i 的值已经被线程B机型 ++ 后改为了 11,然后线程B将CPU资源让出来,此时线程A再切换回来的时候,它要执行的下一条指令是程序计数器中保存的对 i 进行 ++ 操作 ,而线程A此时 ++ 的 i 的值是从上下文信息中获取的,上下文信息中此时的 i = 10 ,此时线程A在CPU中完成对 i 的 ++ 操作,然后将结果 11 回写给寄存器,然后由寄存器再回写给内存,此时内存中的 i 被线程B改为了 11,虽然 ,线程A和线程B都对全局变量 i 进行了 ++ ,按理说最终全局变量 i 的值应该为12,而此时全局变量 i 的值却为11

  • 总结

线程A对全局变量 i 加了一次,线程B也对全局变量 i 加了一次,而此时,全局变量的值为 11 而不是 12,由此就产生了多个线程同时操作临界资源的时候有可能产生二义性问题(线程不安全现象)

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第39张图片

3.线程不安全代码实现(黄牛抢票)

     1	#include<iostream>
     2	#include<pthread.h>
     3	#include<unistd.h>
     4	using namespace std;
     5	int ticket=1000;
     6	
     7	void* get_ticket(void* arg)
     8	{
     9	    while(1)
    10	    {
    11	        if(ticket>0)
    12	        {
    13	            cout<<"i am "<<pthread_self()<<" get a ticket,no:"<<ticket<<endl;
    14	            ticket--;
    15	        }
    16	        else 
    17	        {
    18	            break;
    19	        }
    20	    }
    21	    return NULL;
    22	}
    23	int main()
    24	{
    25	    pthread_t tid[4];
    26	    for(int i=0;i<4;i++)
    27	    {
    28	        int ret=pthread_create(&tid[i],NULL,get_ticket,NULL);
    29	        if(ret!=0)
    30	        {
    31	            cout<<"线程创建失败!"<<endl;
    32	        }
    33	
    34	    }
    35	    for(int i=0;i<4;i++)
    36	    {
    37	        pthread_join(tid[i],NULL);
    38	    }
    39	
    40	    cout<<"pthread_join end!"<<endl;
    41	    return 0;
    42	}

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第40张图片
如上图所示,我们可以看到两个线程都拿到了第819张票,这就产生了二义性,即线程不安全现象。

  • 这里存在两个问题:
  • 线程在取票的时候,多个线程可能会拿到同一张票,,若CPU多的话有可能会拿到负数(互斥锁解决此问题)。
  • 线程拿票不合理,可能一个线程A拿了所有的票,而另一个线程B只拿了一张票还与线程A相同。

4.互斥量mutex

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
  • 多个线程并发的操作共享变量,会带来一些问题。
     1	#include<iostream>
     2	#include<pthread.h>
     3	#include<unistd.h>
     4	using namespace std;
     5	int ticket=100;
     6	
     7	void* get_ticket(void* arg)
     8	{
     9	    int* num=(int*)arg;
    10	    while(1)
    11	    {
    12	        if(ticket>0)
    13	        {
    14	            sleep(1); 
    15	            cout<<"thread "<<num<<" get a ticket,no:"<<ticket<<endl;
    16	            ticket--;
    17	        }
    18	        else 
    19	        {
    20	            break;
    21	        }
    22	    }
    23	    return NULL;
    24	}
    25	int main()
    26	{
    27	    pthread_t tid[4];
    28	    for(int i=0;i<4;i++)
    29	    {
    30	        pthread_create(tid+1,NULL,get_ticket,(void*)i);
    31	
    32	    }
    33	    for(int i=0;i<4;i++)
    34	    {
    35	        pthread_join(tid[i],NULL);
    36	    }
    37	    return 0;
    38	}

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第41张图片

  • 为什么可能无法获得争取结果?
  • if 语句判断条件为真以后,代码可以并发的切换到其他线程。
  • sleep这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。
  • –ticket操作本身就不是一个原子操作。
  • 要解决以上问题,需要做到三点
  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第42张图片

5.互斥锁的原理

1.什么是互斥锁

  • 互斥锁的底层是一个互斥量,而互斥量的本质就是一个计数器,计数器的取值只有两种情况,一种是 1 ,一种是 0 ;
  • 表示当前临界资源可以被访问。
  • 0:表示当前临界资源不可以被访问。

2.互斥锁逻辑

  • 调用加锁接口,加锁接口内部计数器的值是否为 1 ,若为 1 ,则能访问;当枷加锁成功之后,就会将计数器的值从 1 变成 0 ;如果为 0 ,则不能访问。
  • -调用解锁逻辑,计数器的值从 0 变成 1 ,表示资源可以使用。

假设有一临界资源,有一个线程A和一个线程B,按之前的黄牛抢票的思路,只要线程拥有时间片就可以去访问这块临界资源,现在我们给线程 A 和线程 B 都加上互斥锁,假设此时线程A要去访问临界资源,它首先得获取互斥锁,而此时互斥锁中的值为1,表示当前可以访问,线程 A 去访问临界资源然后将互斥锁中的 1 改为 0 ,此时如果线程B如果想要访问临界资源之前先要获取互斥锁,而此时互斥锁中的值为0,所以线程 B 此时不能访问临界资源,等线程 A 访问完毕后,就会将锁释放,此时所中的值就会从 0 变为 1 , 此时线程 B 判断互斥锁中的值变为 1 可以访问了,就可以去访问临界资源了;互斥锁保证了当前临界资源在同一时刻只能被一个执行流访问。
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第43张图片
注意:若要多个线程访问临界资源的时候是互斥访问的属性,一定要在多个线程中进行同一把锁的加锁操作,这样每个线程在访问临界资源之前都要获取这把锁,若锁中的值为 1 就能访问,为 0 则不能访问;若只给线程 A 加锁线程 B 不加锁,那么线程 A 判断锁中的值为 1 ,则访问临界资源并将锁中的值改为 0 ,而线程 B 为加这把锁,则不需要获取锁并判断锁中的值是否为 1 就可以直接对临界资源进行访问,会出现线程不安全现象。

3.加锁逻辑

加锁的时候会提前在寄存器的计数器中保存的一个值 0,而不管内存的计数器中保存的值为多少,都会将寄存器中保存到值 0 和内存计数器中保存的值进行交互,然后对寄存器中的值进行判断是否为 1 ,如果为 1 ,则能加锁,如果不为 1 ,则不能加锁。
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第44张图片

  • 经过上面的例子,大家已经意识到单纯的i++或者++i都不是原子的,有可能会有数据一致性问题。
  • 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下。
    【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第45张图片

6.互斥锁的接口

1.初始化互斥锁

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第46张图片

  • 初始化互斥量有两种方法:
  • 方法1,静态分配。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

使用vim /usr/include/pthread.h路径下查看宏定义,这个宏中的PTHREAD_MUTEX_INITIALIZER 是初始化 pthread_mutex_t 这个变量的,如下图所示:
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第47张图片
pthread_mutex_t 实际是一个联合体, vim /usr/include/bits/pthreadtypes.h 路径,静态初始化实际就是用上面那个宏初始化这个联合体,如下图所示
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第48张图片

  • 方法2,动态分配:

int pthread_mutex_init(pthread_mutex_t restrict mutex, const pthread_mutexattr_trestrict attr);
参数:

  • mutex:要初始化的互斥量
  • attr:NULL ,设置为空,代表默认属性

2.销毁互斥锁

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第49张图片

  • 使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁。
  • 不要销毁一个已经加锁的互斥量。
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。

int pthread_mutex_destroy(pthread_mutex_t *mutex);动态初始化互斥锁变量的情况需要动态销毁互斥锁,否则就会造成内存泄漏

3.互斥锁的加锁和解锁

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第50张图片

1.阻塞加锁接口

int pthread_mutex_lock(pthread_mutex_t *mutex);

  • 如果互斥锁变量当中的计数器的值为1,调用该接口,则加锁成功,该接口调用完毕,函数返回。
  • 如果互斥锁变量当中的计数器的值为0,调用该接口,则调用该接口的执行流阻塞在当前接口内部。
2.非阻塞加锁接口

int pthread_mutex_trylock(pthread_mutex_t *mutex);

  • 不管有没有加锁成功,都会返回,所以需要对加锁返回的结果进行判断,判断是否加锁,如果加锁成功,则操作临界资源。反之,则需要循环获取互斥锁,直到拿到互斥锁
3.带有超时时间的加锁接口

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第51张图片

4.解锁接口

int pthread_mutex_unlock(pthread_mutex_t *mutex);

  • 不管是阻塞加锁 / 非阻塞加锁 / 带有超 时时间的加锁,加锁成功的互斥锁,都可以使用该接口进行解锁。

4.互斥锁的使用

1.什么时候使用初始化互斥锁

在这里插入图片描述
先初始化互斥锁,在创建线程。

2.什么时候使用销毁互斥锁

在所有使用互斥锁的线程全部退出之后,就可以销毁互斥锁

3.什么时候使用加锁

线程访问临界资源之前,进行加锁操作

4.什么时候使用解锁

线程所有可能退出的地方进行解锁

5.加锁之后不解锁

在访问临界资源之前进行加锁,然后不解锁,会发生什么现象。

     1	#include<iostream>
     2	#include<pthread.h>
     3	#include<unistd.h>
     4	using namespace std;
     5	int ticket=1000;
     6	
     7	pthread_mutex_t g_lock; //全局变量的互斥锁
     8	
     9	void* get_ticket(void* arg)
    10	{
    11	    while(1)//1位置加锁还是在2位置加锁
    12	    {
    13	        pthread_mutex_lock(&g_lock);
    14	        //pos1
    15	        if(ticket>0)
    16	        {
    17	            cout<<"i am "<<pthread_self()<<" get a ticket,no:"<<ticket<<endl;
    18	            //pos2
    19	            ticket--;
    20	        }
    21	        else 
    22	        {
    23	            break;
    24	        }
    25	    }
    26	    return NULL;
    27	}
    28	int main()
    29	{
    30	    pthread_mutex_init(&g_lock,NULL);//初始化互斥锁
    31	    pthread_t tid[4];
    32	    for(int i=0;i<4;i++)
    33	    {
    34	        int ret=pthread_create(&tid[i],NULL,get_ticket,NULL);
    35	        if(ret!=0)
    36	        {
    37	            cout<<"线程创建失败!"<<endl;
    38	        }
    39	
    40	    }
    41	    for(int i=0;i<4;i++)
    42	    {
    43	        pthread_join(tid[i],NULL);
    44	    }
    45	
    46	    cout<<"pthread_join end!"<<endl;
    47	    pthread_mutex_destroy(&g_lock);
    48	    return 0;
    49	}

观察到只得到一张票就不往下去执行了。
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第52张图片
上图发现4个工作线程都被阻塞了,有一个工作线程加锁,之后未进行解锁,造成如上错误的原因是某个线程在执行完毕后,没有解锁,其他工作线程再次去获取锁时,互斥锁中计数器中的值还是0,就要被阻塞等待:所以加锁之后一定要记得解锁,否则就会导致死锁。
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第53张图片

6.gdb调试:哪个工作线程加锁之后不解锁

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第54张图片
先mythread处于运行状态,在使用gdb attach [pid] 命令(用gdb调试一个正在运行的进程)进入gdb调试界面,如下图所示:
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第55张图片
在用bt查看调用堆栈,在使用thread apply all bit 查看所有调用堆栈如下图:
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第56张图片
然后通过 t [线程编号] 跳转到某一个线程的堆栈,然后通过 bt 查看线程的调用栈,f 跳转到某一个具体的堆栈里如上图所示, 我们可以进行调试
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第57张图片
再打印互斥锁变量 My_lock,而在互斥锁变量中我们可以看到一个 __owner(互斥锁的拥有者) = 27700,而这个27700就是线程5,线程5此时再去加锁,但当它获取这把锁的时候,它就会阻塞在加锁逻辑中(即线程5第一次加锁成功了,第二次再去获取这把锁的时候就会阻塞在加锁逻辑中)
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第58张图片
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第59张图片

5.正确互斥锁的使用

     1	#include<iostream>
     2	#include<pthread.h>
     3	#include<unistd.h>
     4	using namespace std;
     5	int ticket=1000;
     6	
     7	pthread_mutex_t g_lock; //全局变量的互斥锁
     8	
     9	void* get_ticket(void* arg)
    10	{
    11	    while(1)//1位置加锁还是在2位置加锁
    12	    {
    13	        pthread_mutex_lock(&g_lock);
    14	        //pos1
    15	        if(ticket>0)
    16	        {
    17	            usleep(1);
    18	            cout<<"i am "<<pthread_self()<<" get a ticket,no:"<<ticket<<endl;
    19	            //pos2
    20	            ticket--;
    21	            pthread_mutex_unlock(&g_lock);
    22	        }
    23	        else 
    24	        {
    25	            pthread_mutex_unlock(&g_lock);
    26	            break;
    27	        }
    28	    }
    29	    return NULL;
    30	}
    31	int main()
    32	{
    33	    pthread_mutex_init(&g_lock,NULL);//初始化互斥锁
    34	    pthread_t tid[4];
    35	    for(int i=0;i<4;i++)
    36	    {
    37	        int ret=pthread_create(&tid[i],NULL,get_ticket,NULL);
    38	        if(ret!=0)
    39	        {
    40	            cout<<"线程创建失败!"<<endl;
    41	        }
    42	
    43	    }
    44	    for(int i=0;i<4;i++)
    45	    {
    46	        pthread_join(tid[i],NULL);
    47	    }
    48	
    49	    cout<<"pthread_join end!"<<endl;
    50	    pthread_mutex_destroy(&g_lock);
    51	    return 0;
    52	}

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第60张图片

7.死锁

1.死锁的定义

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第61张图片
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
线程A获取到互斥锁1 ,线程B获取到互斥锁2的时候,线程A和线程B同时还想获取对方手里的锁(线程A还想获取互斥锁2,线程B还想获取互斥锁1),此时就会导致死锁
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第62张图片

2.死锁的模拟实现

     1	#include<iostream>
     2	#include<unistd.h>
     3	#include<pthread.h>
     4	using namespace std;
     5	
     6	pthread_mutex_t g_lock1;
     7	pthread_mutex_t g_lock2;
     8	void* ThreadStart1(void* args)
     9	{
    10	    (void)args;
    11	    pthread_mutex_lock(&g_lock1);
    12	    sleep(5);
    13	    pthread_mutex_lock(&g_lock2);
    14	    return NULL;
    15	}
    16	void* ThreadStart2(void* args)
    17	{
    18	    (void)args;
    19	    pthread_mutex_lock(&g_lock2);
    20	    sleep(5);
    21	    pthread_mutex_lock(&g_lock1);
    22	    return NULL;
    23	}
    24	int main()
    25	{
    26	    pthread_mutex_init(&g_lock1,NULL);
    27	    pthread_mutex_init(&g_lock2,NULL);
    28	
    29	    pthread_t tid;
    30	    int ret=pthread_create(&tid,NULL,ThreadStart1,NULL);
    31	    if(ret<0)
    32	    {
    33	        cout<<"线程创建失败!"<<endl;
    34	        return 0;
    35	    }
    36	
    37	    ret=pthread_create(&tid,NULL,ThreadStart2,NULL);
    38	    if(ret<0)
    39	    {
    40	        cout<<"线程创建失败!"<<endl;
    41	        return 0;
    42	    }
    43	
    44	    while(1)
    45	    {
    46	        ;
    47	    }
    48	    pthread_mutex_destroy(&g_lock1);
    49	    pthread_mutex_destroy(&g_lock2);
    50	    return 0;
    51	}

结果如下图:
在这里插入图片描述
通过gdb调试存在的进程如图:
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第63张图片
查看所有线程的调用堆栈:
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第64张图片
进入线程3并查看线程3的调用堆栈,进入其中一个调用堆栈。
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第65张图片
线程ThreadStart1拥有g_lock1想拿g_lock2锁,线程ThreadStart2拥有g_lock2想拿g_lock1锁,这样两个线程就造成了死锁。
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第66张图片

3.死锁生成的4个必要条件

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第67张图片

  • 互斥条件:一个资源每次只能被一个执行流使用。(一个互斥锁,只能被一个执行流在同一时刻拥有)
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。(线程A拿着 1 锁还想请求 2 锁,线程B拿着 2 锁还想请求 1 锁)
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。(线程A等待线程B拿的锁,线程B等待线程A拿的锁)

4.避免死锁的条件(满足其中一个条件就可以)

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第68张图片

  • 加锁顺序一致。
  • 避免锁未释放的场景。
  • 资源一次性分配。

5.避免死锁的算法

  • 死锁检测算法。
  • 银行家算法。

五 、线程同步

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第69张图片
用途:多个线程访问临界资源不会发生错误:

1.模拟加锁的未加条件变量单个人吃面例子

模拟吃面和做面的加锁例子:

     1	#include<iostream>
     2	#include<pthread.h>
     3	#include<unistd.h>
     4	using namespace std;
     5	
     6	int g_bowl=1;
     7	pthread_mutex_t g_lock;
     8	
     9	void* EatStart(void* arg)
    10	{
    11	    (void)arg;
    12	
    13	    while(1)
    14	    {
    15	        pthread_mutex_lock(&g_lock);
    16	        g_bowl--;
    17	        cout<<"i am:"<<pthread_self()<<" i eat:"<<g_bowl<<endl;
    18	        pthread_mutex_unlock(&g_lock);
    19	    }
    20	    return NULL;
    21	}
    22	void *MakeStart(void* arg)
    23	{
    24	    (void)arg;
    25	    while(1)
    26	    {
    27	        pthread_mutex_lock(&g_lock);
    28	        g_bowl++;
    29	        cout<<"i am:"<<pthread_self()<<" i make:"<<g_bowl<<endl;
    30	        pthread_mutex_unlock(&g_lock);
    31	    }
    32	    return NULL;
    33	}
    34	int main()
    35	{
    36	    pthread_mutex_init(&g_lock,NULL);
    37	
    38	        pthread_t tid_eat;
    39	        pthread_t tid_make;
    40	        int ret=pthread_create(&tid_eat,NULL,EatStart,NULL);
    41	        if(ret<0)
    42	        {
    43	            cout<<"线程创建失败!"<<endl;
    44	        }
    45	
    46	        ret=pthread_create(&tid_make,NULL,MakeStart,NULL);
    47	        {
    48	            if(ret<0)
    49	            {
    50	                cout<<"线程创建失败!\n"<<endl;
    51	            }
    52	        }
    53	
    54	    while(1)
    55	    {
    56	        sleep(1);
    57	    }
    58	
    59	
    60	    pthread_mutex_destroy(&g_lock);
    61	    return 0;
    62	}

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第70张图片
能会看到bowl已经减为负数,也有可能看到bowl加为超过1的正数,为什么会出现上述情况呢?因为线程EatStart负责吃面,线程MakeStart负责做面,当线程MakeStart拿到CPU资源时,它可能持续往碗里做面,就会出现bowl>1的情况,当线程EatStart拿到CPU资源时,它可能持续吃面,就会出现bowl<0的情况,然后一直吃面,导致bowl减少到0甚至负数的情况。

2.条件变量

在这里插入图片描述

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

1.PCB等待队列

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第71张图片

3.条件变量函数

1.条件变量初始化

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第72张图片
静态初始化:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

动态初始化:

int pthread_cond_init(pthread_cond_t restrict cond, const pthread_condattr_t restrict attr);

  • cond:待要初始化的“条件变量”的变量,一般情况下,传递一个pthread_cond_t类型变量的地址
  • attr:一般情况下给NULL,采用默认属性

2.条件变量销毁

int pthread_cond_destroy(pthread_cond_t cond)

3.条件变量唤醒

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第73张图片

int pthread_cond_signal(pthread_cond_t cond);

  • 作用:通知PCB等待队列当中的线程,线程接收到了,则从PCB等待队列当中出队操作。
  • 至少唤醒一个PCB等待队列当中的线程。

int pthread_cond_broadcast(pthread_cond_t cond);

  • 唤醒所有PCB等待队列当中的线程。

4.条件变量等待

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第74张图片

int pthread_cond_wait(pthread_cond_t restrict cond,pthread_mutex_t restrict mutex);
参数:

  • cond:要在这个条件变量上等待
  • mutex:互斥量,后面详细解释

4.模拟加锁的加条件变量单个人吃面例子

给线程EatStart加上条件变量bowl小于等于0就不可以在吃面,给线程MakeStart加上条件变量bowl>1就不可以在做面。

     1	#include<iostream>
     2	#include<pthread.h>
     3	#include<unistd.h>
     4	using namespace std;
     5	
     6	int g_bowl=1;
     7	pthread_mutex_t g_lock;
     8	
     9	pthread_cond_t g_eat_cond;
    10	pthread_cond_t g_make_cond;
    11	
    12	//吃面
    13	void* EatStart(void* arg)
    14	{
    15	    (void)arg;
    16	
    17	    while(1)
    18	    {
    19	        pthread_mutex_lock(&g_lock);
    20	        if(g_bowl<=0)
    21	        {
    22	            pthread_cond_wait(&g_eat_cond,&g_lock);//传递互斥锁是后面要解锁
    23	        }
    24	        g_bowl--;
    25	        cout<<"i am:"<<pthread_self()<<" i eat:"<<g_bowl<<endl;
    26	        pthread_mutex_unlock(&g_lock);
    27	
    28	        pthread_cond_signal(&g_make_cond);//通知做面的人来做面
    29	    }
    30	    return NULL;
    31	}
    32	
    33	//做面
    34	void *MakeStart(void* arg)
    35	{
    36	    (void)arg;
    37	    while(1)
    38	    {
    39	        pthread_mutex_lock(&g_lock);
    40	        if(g_bowl>0)
    41	        {
    42	            pthread_cond_wait(&g_make_cond,&g_lock);
    43	        }
    44	        g_bowl++;
    45	
    46	        cout<<"i am:"<<pthread_self()<<" i make:"<<g_bowl<<endl;
    47	        pthread_mutex_unlock(&g_lock);
    48	
    49	        pthread_cond_signal(&g_eat_cond);//通知吃面的人来吃面
    50	    }
    51	    return NULL;
    52	}
    53	int main()
    54	{
    55	    //初始化互斥锁
    56	    pthread_mutex_init(&g_lock,NULL);
    57	    
    58	    //初始化条件变量
    59	    pthread_cond_init(&g_eat_cond,NULL);
    60	    pthread_cond_init(&g_make_cond,NULL);
    61	
    62	    int i=0;
    63	    for(i=0;i<1;i++)
    64	    {
    65	        pthread_t tid;
    66	        int ret=pthread_create(&tid,NULL,EatStart,NULL);
    67	        if(ret<0)
    68	        {
    69	            cout<<"线程创建失败!"<<endl;
    70	        }
    71	
    72	        ret=pthread_create(&tid,NULL,MakeStart,NULL);
    73	        {
    74	            if(ret<0)
    75	            {
    76	                cout<<"线程创建失败!\n"<<endl;
    77	            }
    78	        }
    79	    }
    80	
    81	    while(1)
    82	    {
    83	        sleep(1);
    84	    }
    85	
    86	    //销毁互斥锁
    87	    pthread_mutex_destroy(&g_lock);
    88	
    89	    //销毁条件变量
    90	    pthread_cond_destroy(&g_eat_cond);
    91	    pthread_cond_destroy(&g_make_cond);
    92	    return 0;
    93	}

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第75张图片

5.模拟加锁的加条件变量多个人吃面例子

     1	#include<iostream>
     2	#include<pthread.h>
     3	#include<unistd.h>
     4	using namespace std;
     5	
     6	int g_bowl=1;
     7	pthread_mutex_t g_lock;
     8	
     9	pthread_cond_t g_eat_cond;
    10	pthread_cond_t g_make_cond;
    11	
    12	//吃面
    13	void* EatStart(void* arg)
    14	{
    15	    (void)arg;
    16	
    17	    while(1)
    18	    {
    19	        pthread_mutex_lock(&g_lock);
    20	        if(g_bowl<=0)
    21	        {
    22	            pthread_cond_wait(&g_eat_cond,&g_lock);//传递互斥锁是后面要解锁
    23	        }
    24	        g_bowl--;
    25	        cout<<"i am:"<<pthread_self()<<" i eat:"<<g_bowl<<endl;
    26	        pthread_mutex_unlock(&g_lock);
    27	
    28	        pthread_cond_signal(&g_make_cond);//通知做面的人来做面
    29	    }
    30	    return NULL;
    31	}
    32	
    33	//做面
    34	void *MakeStart(void* arg)
    35	{
    36	    (void)arg;
    37	    while(1)
    38	    {
    39	        pthread_mutex_lock(&g_lock);
    40	        if(g_bowl>0)
    41	        {
    42	            pthread_cond_wait(&g_make_cond,&g_lock);
    43	        }
    44	        g_bowl++;
    45	
    46	        cout<<"i am:"<<pthread_self()<<" i make:"<<g_bowl<<endl;
    47	        pthread_mutex_unlock(&g_lock);
    48	
    49	        pthread_cond_signal(&g_eat_cond);//通知吃面的人来吃面
    50	    }
    51	    return NULL;
    52	}
    53	int main()
    54	{
    55	    //初始化互斥锁
    56	    pthread_mutex_init(&g_lock,NULL);
    57	    
    58	    //初始化条件变量
    59	    pthread_cond_init(&g_eat_cond,NULL);
    60	    pthread_cond_init(&g_make_cond,NULL);
    61	
    62	    int i=0;
    63	    for(i=0;i<2;i++)
    64	    {
    65	        pthread_t tid;
    66	        int ret=pthread_create(&tid,NULL,EatStart,NULL);
    67	        if(ret<0)
    68	        {
    69	            cout<<"线程创建失败!"<<endl;
    70	        }
    71	
    72	        ret=pthread_create(&tid,NULL,MakeStart,NULL);
    73	        {
    74	            if(ret<0)
    75	            {
    76	                cout<<"线程创建失败!\n"<<endl;
    77	            }
    78	        }
    79	    }
    80	
    81	    while(1)
    82	    {
    83	        sleep(1);
    84	    }
    85	
    86	    //销毁互斥锁
    87	    pthread_mutex_destroy(&g_lock);
    88	
    89	    //销毁条件变量
    90	    pthread_cond_destroy(&g_eat_cond);
    91	    pthread_cond_destroy(&g_make_cond);
    92	    return 0;
    93	}

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第76张图片
假设,碗里有面,此时make1拿到了锁,则make1判断碗里有面后将自己放入PCB等待队列中进行等待,然后释放互斥锁,假设此时,eat1拿到了互斥锁,然后eat1吃掉碗里的面,然后释放锁并通知PCB等待队列,此时make1已经出队,假设此时make2拿到了锁,并做了一碗面,然后释放锁,然后make1又拿到了锁,而此时make1将要执行的是跳过pthread_cond_wait函数,则make1跳过了判断碗里是否有面,直接往碗里做面,此时bowl的值就由1变为了2。
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第77张图片
假设,碗里有面,此时eat1拿到了锁,则eate1判断碗里没有面后将自己放入PCB等待队列中进行等待,然后释放互斥锁,假设此时,make1拿到了互斥锁,然后make1做碗里的面,然后释放锁并通知PCB等待队列,此时eat1已经出队,假设此时eat2拿到了锁,并吃了一碗面,然后释放锁,然后eat1又拿到了锁,而此时eat1将要执行的是跳过了pthread_cond_wait函数,则eate1跳过了判断碗里是否有面,直接往碗里吃面,此时bowl的值就由0变成-1。

解决这个问题只需要把if判断条件改成while循环即可。

#include
#include
#include
using namespace std;

int g_bowl=1;
pthread_mutex_t g_lock;

pthread_cond_t g_eat_cond;
pthread_cond_t g_make_cond;

//吃面
void* EatStart(void* arg)
{
    (void)arg;

    while(1)
    {
        pthread_mutex_lock(&g_lock);
        while(g_bowl<=0)
        {
            pthread_cond_wait(&g_eat_cond,&g_lock);//传递互斥锁是后面要解锁
        }
        g_bowl--;
        cout<<"i am:"<<pthread_self()<<" i eat:"<<g_bowl<<endl;
        pthread_mutex_unlock(&g_lock);

        pthread_cond_signal(&g_make_cond);//通知做面的人来做面
    }
    return NULL;
}

//做面
void *MakeStart(void* arg)
{
    (void)arg;
    while(1)
    {
        pthread_mutex_lock(&g_lock);
        while(g_bowl>0)
        {
            pthread_cond_wait(&g_make_cond,&g_lock);
        }
        g_bowl++;

        cout<<"i am:"<<pthread_self()<<" i make:"<<g_bowl<<endl;
        pthread_mutex_unlock(&g_lock);

        pthread_cond_signal(&g_eat_cond);//通知吃面的人来吃面
    }
    return NULL;
}
int main()
{
    //初始化互斥锁
    pthread_mutex_init(&g_lock,NULL);
    
    //初始化条件变量
    pthread_cond_init(&g_eat_cond,NULL);
    pthread_cond_init(&g_make_cond,NULL);

    int i=0;
    for(i=0;i<2;i++)
    {
        pthread_t tid;
        int ret=pthread_create(&tid,NULL,EatStart,NULL);
        if(ret<0)
        {
            cout<<"线程创建失败!"<<endl;
        }

        ret=pthread_create(&tid,NULL,MakeStart,NULL);
        {
            if(ret<0)
            {
                cout<<"线程创建失败!\n"<<endl;
            }
        }
    }

    while(1)
    {
        sleep(1);
    }

    //销毁互斥锁
    pthread_mutex_destroy(&g_lock);

    //销毁条件变量
    pthread_cond_destroy(&g_eat_cond);
    pthread_cond_destroy(&g_make_cond);
    return 0;
}

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第78张图片

6.条件变量关于等待接口的几个问题

1.条件变量的等待接口参数为什么需要互斥锁?

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第79张图片
由于需要是在pthread_cond_wait函数内部进行解锁,当有线程进去之后要把锁释放别人才能用,解锁之后,其他的执行流才能获取到这把互斥锁,所以,需要传入互斥锁,否则,如果在调用pthread_cond_wait线程在进行等待的时候,不释放互斥锁,其他线程就不能解锁。
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第80张图片

2.pthread_cond_wait函数的实现原理

在这里插入图片描述
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第81张图片
假设,吃面的线程判断碗里没有面,要进行等待,若它先释放互斥锁,在它释放完互斥锁还未进入等待队列之前,可能做面的线程拿到了互斥锁,并做完了面,通知PCB等待队列,而此时吃面的线程,还未进入PCB等待队列,PCB等待队列此事为空,做面的线程又拿到互斥锁去做面,此时,做面的线程判断碗里有面,则将自己放入PCB等待队列中进行等待,而此时吃面的线程也在PCB等待队列中进行等待,所以不能先释放互斥锁。

3.线程等待的时候,被唤醒之后,需要做什么事情

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第82张图片

六、生产者消费者模型

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第83张图片

1.生产者、消费者遵循的原则

  • 321原则(便于记忆)
    【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第84张图片
    【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第85张图片

2.生产者、消费者遵循的原则

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第86张图片
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

3.生产者消费者模型优点

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第87张图片

  • 解耦。
  • 支持并发。
  • 支持忙闲不均。
    【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第88张图片

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

BlockingQueue 在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)。
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第89张图片

5.基于BlockingQueue的生产者消费者代码实现

BlockQueue.cpp

     1	#pragma once  //防止头文件重复包含
     2	
     3	#include<iostream>
     4	
     5	#include<pthread.h>
     6	#include<unistd.h>
     7	#include<queue>
     8	//using namespace std;
     9	
    10	class Task
    11	{
    12	    public:
    13	        int _x;
    14	        int _y;
    15	    public:
    16	        Task()
    17	        {}
    18	        Task(int x,int y)
    19	            :_x(x)
    20	             ,_y(y)
    21	    {}
    22	        int run()
    23	        {
    24	            return _x+_y;
    25	        }
    26	        ~Task()
    27	        {}
    28	};
    29	class BlockQueue
    30	{   
    31	    private:
    32	   // std::queue q; //设置一个队列
    33	    std::queue<Task> q; //设置一个队列
    34	    int _cap; //容量
    35	    pthread_mutex_t lock;  //设置一把互斥锁
    36	
    37	    pthread_cond_t c_cond;  //满了的话通知消费者
    38	    pthread_cond_t p_cond; //空的话通知生产者
    39	
    40	    private: //封装起来
    41	    void LockQueue() //加锁
    42	    {
    43	        pthread_mutex_lock(&lock);
    44	    }
    45	    void UnLockQueue() //解锁
    46	    {
    47	        pthread_mutex_unlock(&lock);
    48	    }
    49	
    50	
    51	    bool IsEmpty() //判断队列是否为空
    52	    {
    53	        return q.size()==0;
    54	    }
    55	    bool IsFull() //判断队列是否满了
    56	    {
    57	        return q.size()==_cap;
    58	    }
    59	    
    60	    void ProductWait() //生产者等待
    61	    {
    62	        pthread_cond_wait(&p_cond,&lock);
    63	    }
    64	    void ConsumerWait() //消费者等待
    65	    {
    66	        pthread_cond_wait(&c_cond,&lock);
    67	    }
    68	
    69	    void WakeUpProduct() //唤醒生产者
    70	    {
    71	        std::cout<<"wake up Product..."<<std::endl;
    72	        pthread_cond_signal(&p_cond);
    73	    }
    74	    void WakeUpConsumer() //唤醒消费者
    75	    {
    76	        std::cout<<"wake up Consumer..."<<std::endl;
    77	        pthread_cond_signal(&c_cond);
    78	    }
    79	
    80	    public:
    81	    BlockQueue(int cap)    //构造函数初始化
    82	    :_cap(cap)
    83	    {
    84	        pthread_mutex_init(&lock,NULL);
    85	        pthread_cond_init(&c_cond,NULL);
    86	        pthread_cond_init(&p_cond,NULL);
    87	    }
    88	    ~BlockQueue()        //析构函数销毁
    89	    {
    90	        pthread_mutex_destroy(&lock);
    91	        pthread_cond_destroy(&c_cond);
    92	        pthread_cond_destroy(&p_cond);
    93	    }
    94	    
    95	    void put(Task in)
    96	    {
    97	        //Queue是临界资源,就要加锁,而且判断是否为满,把接口封装起来
    98	        LockQueue();
    99	        while(IsFull())
   100	        {
   101	                WakeUpConsumer();
   102	                std::cout<<"queue full,notify consume data,product stop!"<<std::endl;
   103	                ProductWait();  //生产者线程等待
   104	        }
   105	        q.push(in);
   106	        
   107	        UnLockQueue();
   108	    }
   109	    void Get(Task& out)
   110	    {
   111	        LockQueue();
   112	        while(IsEmpty())
   113	        {
   114	            WakeUpProduct();
   115	            std::cout<<"queue empty,notify product data,consumer stop"<<std::endl;
   116	            ConsumerWait();
   117	        }
   118	        out=q.front();
   119	        q.pop();
   120	
   121	        UnLockQueue();
   122	    }
   123	
   124	    //线程接口函数
   125	    /*void* Product(void* arg)
   126	    {
   127	
   128	    }
   129	    void* Consumer(void* arg)
   130	    {
   131	
   132	    }*/
   133	
   134	};

main.cpp

     1	#include"BlockQueue.cpp"
     2	using namespace std;
     3	#include<stdlib.h>
     4	
     5	pthread_mutex_t p_lock;
     6	pthread_mutex_t c_lock;
     7	void* Product_Run(void* arg)
     8	{
     9	    BlockQueue* bq=(BlockQueue*)arg;
    10	
    11	    srand((unsigned int)time(NULL));
    12	    while(true)
    13	    {
    14	        pthread_mutex_lock(&p_lock);
    15	       // int data=rand()%10+1;
    16	       int x=rand()%10+1;
    17	       int y=rand()%100+1;
    18	       Task t(x,y);
    19	        bq->put(t);
    20	        pthread_mutex_unlock(&p_lock);
    21	        cout<<"product data is:"<<t.run()<<endl;
    22	    }
    23	}
    24	void* Consumer_Run(void* arg)
    25	{
    26	    BlockQueue* bq=(BlockQueue*)arg;
    27	    while(true)
    28	    {
    29	        pthread_mutex_lock(&c_lock);
    30	       // int n=0;
    31	        Task t;
    32	        bq->Get(t);
    33	        pthread_mutex_unlock(&c_lock);
    34	        cout<<"consumer  is:"<<t._x<<"+"<<t._y<<"="<<t.run()<<endl;
    35	        sleep(1);
    36	    }
    37	}
    38	int main()
    39	{
    40	    BlockQueue* bq=new BlockQueue(10);
    41	    pthread_t c,p;
    42	
    43	    pthread_create(&c,NULL,Product_Run,(void*)bq);
    44	
    45	    pthread_create(&p,NULL,Consumer_Run,(void*)bq);
    46	
    47	    pthread_join(c,NULL);
    48	    pthread_join(p,NULL);
    49	
    50	    delete bq;
    51	    return 0;
    52	}

makefile

     1	main:main.cpp
     2		g++ $^ -o $@ -lpthread
     3	.PHONY:clean
     4	clean:
     5		rm -f main

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第90张图片

七、POSIX信号量

1.POSIX信号量的概念

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第91张图片
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第92张图片

2.POSIX信号量初始化

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第93张图片

#include
int sem_init(sem_t sem, int pshared, unsigned int value);

参数:

  • pshared:0表示线程间共享,非零表示进程间共享
  • value:信号量初始值

3.POSIX信号量销毁

在这里插入图片描述

int sem_destroy(sem_t sem);

4.POSIX信号量等待

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第94张图片

功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t sem);

5.POSIX信号量发布

在这里插入图片描述

功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t sem);

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

  • 上一个生产者-消费者的例子是基于queue的,其空间可以动态分配,现在基于固定大小的环形队列重写这个程序(POSIX信号量)。
  • 环形队列采用数组模拟,用模运算来模拟环状特性。
  • 环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态。
  • 但是我们现在有信号量这个计数器,就很简单的进行多线程间的同步过程。
    【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第95张图片

7.基于环形队列的生产消费模型代码实现

RingQueue.cpp

     1	#pragma once 
     2	
     3	#include<iostream>
     4	#include<unistd.h>
     5	#include<vector>
     6	#include<semaphore.h>
     7	
     8	#include<stdlib.h>
     9	#define NUM 10
    10	
    11	class RingQueue
    12	{
    13	    private:
    14	        std::vector<int> v;
    15	        int _cap;           //容量
    16	        sem_t sem_blank;   //生产者
    17	        sem_t sem_data;        //消费者
    18	        
    19	        int c_index;      //消费者索引
    20	        int p_index;      //生产者索引
    21	
    22	    public:
    23	        RingQueue(int cap=NUM)
    24	            :_cap(cap)
    25	            ,v(cap)
    26	        {
    27	            sem_init(&sem_blank,0,cap);
    28	            sem_init(&sem_data,0,0);
    29	            c_index=0;
    30	            p_index=0;
    31	        }
    32	        ~RingQueue()
    33	        {
    34	            sem_destroy(&sem_blank);
    35	            sem_destroy(&sem_data);
    36	        }
    37	
    38	        void Get(int& out)
    39	        {
    40	            sem_wait(&sem_data);
    41	            //消费
    42	            out=v[c_index];
    43	            c_index++;
    44	            c_index=c_index%NUM; //防止越界,构成环形队列
    45	            sem_post(&sem_blank);
    46	        }
    47	        void Put(const int& in)
    48	        {
    49	            sem_wait(&sem_blank);
    50	            //生产
    51	            v[p_index]=in;
    52	            p_index++;
    53	            p_index=p_index%NUM;
    54	            sem_post(&sem_data);
    55	        }
    56	};

main.cpp

     1	#include"RingQueue.h"
     2	using namespace std;
     3	
     4	
     5	void* Consumer(void* arg)
     6	{
     7	    RingQueue *bq=(RingQueue*)arg;
     8	    int data;
     9	    while(1)
    10	    {
    11	        bq->Get(data);
    12	        cout<<"i am:"<<pthread_self()<<" i consumer:"<<data<<endl;
    13	    }
    14	}
    15	void* Product(void* arg)
    16	{
    17	    RingQueue* bq=(RingQueue*)arg;
    18	    srand((unsigned int)time(NULL));
    19	    while(1)
    20	    {
    21	        int data=rand()%100;
    22	        bq->Put(data);
    23	        cout<<"i am:"<<pthread_self()<<" i product:"<<data<<endl;
    24	        sleep(1);
    25	    }
    26	}
    27	int main()
    28	{
    29	    RingQueue* pq=new RingQueue();
    30	    pthread_t c;
    31	    pthread_t p;
    32	    pthread_create(&c,NULL,Consumer,(void*)pq);
    33	    pthread_create(&p,NULL,Product,(void*)pq);
    34	
    35	    pthread_join(c,NULL);
    36	    pthread_join(p,NULL);
    37	    return 0;
    38	}

Makefile

     1	main:main.cpp
     2		g++ $^ -o $@ -lpthread
     3	.PHONY:clean
     4	clean:
     5		rm -f main

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第96张图片

八、线程池

1.线程池概念

什么是线程池?简单点说,线程池就是有一堆已经创建好了的线程,初始它们都处于空闲等待状态,当有新的任务需要处理的时候,就从这个池子里面取一个空闲等待的线程来处理该任务,当处理完成了就再次把该线程放回池中,以供后面的任务使用。当池子里的线程全都处理忙碌状态时,线程池中没有可用的空闲等待线程,此时,根据需要选择创建一个新的线程并置入池中,或者通知任务线程池忙,稍后再试。

2.线程池作用

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第97张图片
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

3.线程池应用场景

  • 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
  • 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  • 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。

4.线程池示例

  • 创建固定数量线程池,循环从任务队列中获取任务对象。
  • 获取到任务对象后,执行任务对象中的任务接口。

5.基于队列的线程池实现代码

Thread_Pool.h:

     1	#include<iostream>
     2	#include<math.h>
     3	#include<unistd.h>
     4	#include<stdlib.h>
     5	#include<pthread.h>
     6	#include<queue>
     7	
     8	#define NUM 5
     9	class Task 
    10	{
    11	    private:
    12	        int _b;
    13	    public:
    14	        Task()
    15	        {
    16	            
    17	        }
    18	        Task(int b)
    19	            :_b(b)
    20	        {
    21	            
    22	        }
    23	        ~Task()
    24	        {
    25	
    26	        }
    27	        void Run()
    28	        {
    29	            std::cout<<"i am:"<<pthread_self()<<" Task run.... :base# "<<_b<<" pow is "<<pow(_b,2)<<std::endl;
    30	        }
    31	};
    32	class ThreadPool
    33	{
    34	    private:
    35	        std::queue<Task*> q;
    36	        int _max_num;        //线程总数
    37	
    38	        pthread_mutex_t lock;
    39	        pthread_cond_t cond;   //只能让消费者操作
    40	
    41	    private:
    42	        void LockQueue()
    43	        {
    44	            pthread_mutex_lock(&lock);
    45	        }
    46	        void UnLockQueue()
    47	        {
    48	            pthread_mutex_unlock(&lock);
    49	        }
    50	
    51	        bool IsEmpty()
    52	        {
    53	            return q.size()==0;
    54	        }
    55	        bool IsFull()
    56	        {
    57	            return q.size()==_max_num;
    58	        }
    59	
    60	        void ThreadWait()
    61	        {
    62	            pthread_cond_wait(&cond,&lock);   //等待条件变量满足
    63	        }
    64	
    65	        void ThreadWakeUp()
    66	        {
    67	            pthread_cond_signal(&cond);
    68	        }
    69	    public:
    70	        ThreadPool(int max_num=NUM )
    71	            :_max_num(max_num)
    72	        {
    73	
    74	        }
    75	
    76	        static void* Routine(void* arg)
    77	        {
    78	            while(1)
    79	            {
    80	                ThreadPool *tp=(ThreadPool*)arg;
    81	                while(tp->IsEmpty())
    82	                {
    83	                    tp->LockQueue();  //静态成员方法不能访问非静态成员方法,所以传(void*)this传过去
    84	                    tp->ThreadWait();  //为空挂起等待
    85	                }
    86	                    
    87	                Task t;
    88	                tp->Get(t);   //获取这个任务
    89	                tp->UnLockQueue();
    90	                t.Run(); //拿到这个任务运行
    91	            }
    92	        }
    93	
    94	        void ThreadPoolInit()
    95	        {
    96	            pthread_mutex_init(&lock,NULL);
    97	            pthread_cond_init(&cond,NULL);
    98	
    99	            int i=0;
   100	            pthread_t t;
   101	            for(i=0;i<_max_num;i++)
   102	            {
   103	                pthread_create(&t,NULL,Routine,(void*)this);
   104	            }
   105	        }
   106	        ~ThreadPool()
   107	        {
   108	            pthread_mutex_destroy(&lock);
   109	            pthread_cond_destroy(&cond);
   110	        }
   111	
   112	        //server  放数据
   113	        void Put(Task& in)
   114	        {
   115	            LockQueue();
   116	
   117	            q.push(&in);
   118	
   119	            UnLockQueue();
   120	            
   121	            ThreadWakeUp();
   122	        }
   123	        //ThreadPool 取数据
   124	        void Get(Task& out)
   125	        {
   126	            //线程池里面直接拿不用加锁
   127	            Task* t=q.front();
   128	            q.pop();
   129	            out=*t;
   130	        }
   131	};
   132	

main.cpp

     1	#include"Thread_Pool.h"
     2	using namespace std;
     3	
     4	
     5	int main()
     6	{
     7	    ThreadPool *tp=new ThreadPool();
     8	    
     9	    tp->ThreadPoolInit();
    10	
    11	    while(true)
    12	    {
    13	        int x=rand()%10+1;
    14	        Task t(x);
    15	        tp->Put(t);
    16	        sleep(1);
    17	    }
    18	    return 0; 
    19	}

makefile

     1	main:main.cpp
     2		g++ $^ -o $@ -lpthread
     3	.PHONY:clean
     4	clean:
     5		rm -f main

6.线程池惊群问题

【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第98张图片
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第99张图片
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第100张图片

九、线程安全的单例模式

1.单例模式概念

单例模式是一种创建型模式,它会限制应用程序,使其只能创建某一特定类>类型的一个单一的实例。举例来说,一个web站点将会需要一个数据库连接>对象,但是应该有且只能有一个,因此我们通过使用单例模式来实现这种限>制。我们可以使用一个静态属性来保证对于一个特定的类来说只存在一个单一的>实例。

2.单例模式特点

某些类, 只应该具有一个对象(实例), 就称之为单例。

3.饿汉方式实现单例模式

  • 洗碗的例子:
  • 吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭。
template <typename T>
class Singleton 
{
private:
	static T data;      //定义静态的类对象,程序加载类就加载对象
public:
	static T* GetInstance() 
	{
		return &data;
	}
};

4.懒汉方式实现单例模式

  • 洗碗的例子:
  • 吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式。
class Singleton 
{
	static T* inst;  //定义静态的类对象指针,程序运行时才加载对象
public:
	static T* GetInstance() 
	{
		if (inst == NULL) 
		{
			inst = new T();
		} r
			eturn inst;
	}
};

存在一个严重的问题, 线程不安全.第一次调用 GetInstance 的时候, 如果两个线程同时调用, 可能会创建出两份 T 对象的实例.但是后续再次调用, 就没有问题了。

5.懒汉方式实现单例模式(线程安全版本)

template <typename T>

// 懒汉模式, 线程安全
template <typename T>
class Singleton {
	volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
	static std::mutex lock;
public:
	static T* GetInstance() 
	{
		if (inst == NULL) // 双重判定空指针, 降低锁冲突的概率, 提高性能. //判断两个线程不同时进去直接return
		{ 
			lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new. //两个线程同时进去加锁
			if (inst == NULL)  
			{
				inst = new T();
			} 
			lock.unlock();
		} 
		return inst;
	}
};
  • 注意事项:
  • 加锁解锁的位置。
  • 双重 if 判定, 避免不必要的锁竞争。
  • volatile关键字防止过度优化。

十、STL智能指针和线程安全

1.STL中的容器是否是线程安全

不是.原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全。

十一、其他常见的各种锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
  • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
  • 自旋锁,公平锁,非公平锁?
    【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第101张图片在这里插入图片描述

总结

以上就是今天要讲的内容,本文超级详细介绍了Linux多线程概念和线程控制、死锁、吃面的使用、线程池的实现、生产者、消费者等的使用,多线程提供了大量的方法供我们使用,非常的便捷,我们务必掌握。希望大家多多支持!另外如果上述有任何问题,请懂哥指教,不过没关系,主要是自己能坚持,更希望有一起学习的同学可以帮我指正,但是如果可以请温柔一点跟我讲,爱与和平是永远的主题,爱各位了。加油啊!
【Linux从青铜到王者】第十三篇:Linux多线程四万字详解_第102张图片

你可能感兴趣的:(Linux,线程池,多线程)