线程(线程控制、线程安全(同步与互斥实现)、单例模式)

线程介绍

线程的概念
  • 在传统操作系统中,pcb就是进程,但是在Linux下,因为线程是通过进程的pcb描述实现的,因此linux下的pcb实际上描述的是一个线程。那线程是一个pcb,进程是什么呢?此时在Linux下,进程现在就是一个概念,也就是一个线程组了。所以说:线程是进程当中的一条执行流,而这个执行流的实现是通过进程pcb描述实现的,并且同一个线程组(进程)中的线程共用同一个虚拟地址空间,因此这个Linux下的pcb相比传统pcb更加轻量化,因此叫做轻量级进程。
    在这里插入图片描述
  • 在Linux下,线程是CPU调度的基本单位,进程是系统资源分配的基本单位。
多线程的优缺点
  • 优点:

    ①:线程间通信更加方便灵活:因为共用同一个虚拟地址空间 PS:进程间通信方式线程间都可以用:管道,共享内存,消息对列,信号量。进程间不能通信的线程间也可以通信,比如:全局变量,函数传参。
    ②:线程的创建与销毁成本更低: 只有少量独有的信息需要创建,线程间共用进程大部分资源(比如:创建一个线程时,只需创建一个pcb,不需要创建虚拟地址空间,页表信息)。
    ③:线程间的调度切换成本更低:因为多进程间调度的时候,需要重新加载pcb信息,页表信息;而多线程之间的页表信息就不需要重新进行加载。

  • 缺点:

    ①:线程间缺乏访问控制:一些系统调用和异常是直接针对整个进程生效的。(比如:exit)
    ②:健壮性降低:一个线程崩溃,则进程崩溃。

多进程的优点与缺点
  • 优点:独立性强,稳定性强,健壮性高:一个进程崩溃了,对其他进程不产生影响。所以多进程适用于对主程序安全稳定性要求较高的场景场景:shell(让子进程背锅,保持自己本身不能挂掉),服务器。
  • 缺点:多线程的优点就是多进程的缺点。
多线程与多进程的用途
  • 合理的使用多线程/多进程,能提高CPU密集型程序(在程序中不断进行数据运算)的执行效率 (分摊压力,提高处理效率)
  • 合理的使用多线程/多进程,能提高IO密集型程序(在程序中大量进行IO操作)的用户体验(并行压缩等待时间)。
线程的独有与共享
  • 独有

    栈(防止调用栈混乱)寄存器:上下文数据+程序计数器、线程标识、优先级、errno、信号屏蔽字

  • 共享

    虚拟地址空间、文件描述符表、当前工作路径、用户ID、组ID

线程控制(创建、终止、等待、分离)

线程控制用到的都是库函数,因为操作系统并没有给用户直接创建一个线程的接口,因此程序员大爷们就封装了一套线程库,用于线程控制。

线程创建
  • int pthread_create(pthread_t *thread, const pthread_attr_t attr,void(*start_routine) (void *), void *arg);

    第一个参数为指向线程标识符的指针。
    第二个参数用来设置线程属性。(比较复杂,一般设置为NULL)
    第三个参数是线程运行函数的起始地址。
    第四个参数是运行函数的参数。

    返回值:成功返回 0,失败返回错误编号。

    void* print(void* arg)//记住返回值与参数一定是void*
    {
      while(1)
      {
        printf("this is child thread---%s\n",(char*)arg);
        sleep(1);
      }
      return NULL;
    }
    
    int main()
    {
      pthread_t tid;
      char* ptr = "hello world";
      int ret = pthread_create(&tid,NULL,print,(void*)ptr);
      if(ret != 0)
      {
        perror("pthread_create error");
        return 0;
      }
    
      while(1)
      {
        printf("this is main thread---%d\n",tid);
        sleep(1);
      }
      return 0;
    }
    

    在编译链接时候,需要链接一个 pthread 库,因为 pthread 库不是 Linux 系统默认的库,如:
    在这里插入图片描述
    运行结果:
    在这里插入图片描述
    问题1:通过对 tid 以 %d 方式打印,会出现一个很大很奇怪的数字,那么 tid 真正的意义到底是什么呢?

    每个线程的独有的信息在虚拟地址空间的共享区都有独立的空间,称之为线程地址空间。tid 可以看作是用户态线程id,但是本质就是这个线程地址空间的首地址(可以%p形式打印查看)。
    在这里插入图片描述
    问题2:当程序运行起来后,通过查看进程信息:
    在这里插入图片描述当前运行的程序里明明有两个进程,但是查看进程信息却只有一个进程的信息,这是什么原因呢?然后可以通过指令(ps -L)查看轻量级进程的信息:
    在这里插入图片描述
    在这里插入图片描述
    可以发现现在有了两个轻量级进程的信息,其中 LWP 是轻量级进程的 pid。PID 与 LWP 相等的是主线程,没有通过 ps -L 查看时,默认查看的是主线程的pid。其实,在每个线程的tast_struct结构体内,有 pid 与 tgid 两个值 ,pid 为轻量级进程 id, tgid (thread group id : 线程组id)为主线程 id。

线程终止
  • ①从线程函数return,这种方法对主线程不适用,从main函数return相当于调用exit,退出的是进程。

  • ②void pthread_exit(void *retval);退出线程

    参数 retval: retval不能指向一个局部变量比如:在这里插入图片描述
    需要注意, pthread_exit 或者 return 返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

  • ③ int pthread_cancel(pthread_t thread);退出指定线程

    参数 thread:线程ID
    返回值:成功返回0;失败返回错误码

    return 与 pthread_exit 是自己调用退出,而pthread_cancel 线程组内其他线程对目标线程进行退出
    ps: 线程终止一般终止的是子线程,也可以终止主线程,然而终止主线程却没有多大的意义。并且终止主线程后,主线程会成为僵尸进程(了解一下就可,不必深究)

线程等待
  • 为什么会有线程等待

    因为已经退出的线程,其空间没有被释放,仍然在进程的地址空间内, 而且创建新的线程不会复用刚才退出线程的地址空间,所以需要回收资源,否则会造成系统资源泄漏。(PS:一个线程创建出来,默认在退出时是不会释放所有资源的,这是因为线程有一个 joinable 属性。处于这个 joinable 状态的线程,退出后不会自动释放资源,需要被等待;如果不是这个状态的线程就不需要被等待,线程退出后会自动将所有资源释放。)

  • 等待函数

    int pthread_join(pthread_t thread, void **retval);

    thread:线程ID
    value_ptr:它指向一个指针,后者指向线程的返回值 ,不关心可以设置为NULL
    返回值:成功返回0;失败返回错误码

    调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

    1、如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
    2、如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数 PTHREAD_ CANCELED。
    3、如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参 数。
    4、如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数

    void* print(void* arg)
    {
      //情况1,通过return终止
      int *p = (int*)malloc(sizeof(int));
      *p = 2;
      return (void*)p;
      
      //情况2,通过pthread_exit();终止
      printf("this is a child thread\n");
      char *ptr = "hello world";
      pthread_exit(ptr);
      
     //情况3,通过pthrea_cancel();终止。在主线程里调用退出
      while(1)
       { 
         printf("this is a child thread\n");
         sleep(1);
      }
       return NULL;
    }
    
    int main()
    {
      pthread_t tid;
      char *ptr = "hello world";
      int ret = pthread_create(&tid,NULL,print,(void*)ptr);
      if(ret != 0)
      {
        perror("pthread_create error");
        return 0;
      }
      //情况1
      int* p;
      pthread_join(tid,(void**)&p);
      printf("retval:%d\n",*p);//输出2
      
      //情况2
      char *p;
      pthread_join(tid,(void**)&p);
      printf("retval:%s\n",p);//输出 hello world
      
      //情况3,,通过pthrea_cancel();终止。
      void *p;
      pthread_cancel(tid);
      pthread_join(tid,(void**)&p);
      if(p == PTHREAD_CANCELED)
      {
        printf("retval:%s\n","PTHREAD_CANCELED");//输出 PTHREAD_CANCELED
      }
    
      while(1)
      {
        printf("this is main thread---%d\n",tid);
        sleep(1);
      }
      return 0;
    }
    
线程分离
  • 将线程的属性从 joinable 设置为 detach ,表示分离一个线程。处于 detach 属性的线程退出后会自动释放所有资源,所以说被分离的线程没必要被等待。

  • 可以是线程组内其他线程对目标线程进行分离:

    int pthread_detach(pthread_t tid);

    也可以是线程自己分离:

    pthread_detach(pthread_self());
    pthread_self() 的返回值就是线程的id,函数原型为: pthread_t pthread_self(void);

    分离后的线程没必要进行线程等待,否则会报错。(因为被分离后的线程退出后会释放自己的所有资源,线程等待需要获取返回值,但是返回值的资源已经被释放。)

线程安全

了解线程安全之前,先了解一些概念
  • 临界资源:多线程执行流共享的资源就叫做临界资源 。
  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用,保证数据访问的安全性。
  • 同步:通过一些条件判断实现对临界资源的有序访问。(不能访问则等待,能访问则唤醒)
  • 原子性:一次性完成任务,不会被任何调度机制打断的操作。
什么是线程安全?
  • 多个线程间对同一个临界资源进行争抢访问,但是不会造成数据二义或逻辑混乱。
如何实现线程安全?
  • 通过线程的同步与互斥来实现

    互斥的实现:互斥锁、信号量
    同步的实现:条件变量、信号量

线程互斥

通过互斥锁实现线程的互斥

  • 什么是互斥锁?
    只有0/1的计数器,用于标记当前临界资源的访问状态;对临界资源进行访问之前都需要先去加锁访问这个状态计数,若可以进行访问的话则修改这个计数(这个计数的操作本身是个原子,是使用一个交换指令完成寄存器与内存之间的数据交换

  • 互斥量的接口:

    1:初始化互斥量

    pthread_mutex_t mutex;
    int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); mutex:要初始化的互斥量 attr:一般设置为NULL

    2:销毁互斥量

    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    注意:使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
    注意:不要销毁一个已经加锁的互斥量
    注意:已经销毁的互斥量,要确保后面不会有线程再尝试加锁

    3:互斥量加锁

    int pthread_mutex_lock(pthread_mutex_t *mutex);//阻塞加锁(常用)
    int pthread_mutex_trylock(pthread_mutex_t *mutex);//尝试加锁,非阻塞加锁
    int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);//限制阻塞时长的阻塞加锁(了解即可)

    4:互斥量解锁

    int pthread_mutex_unlock(pthread_mutex_t *mutex);

    写了一个模拟在线买票的例子来进行体会:

    #include<stdio.h>
    #include<stdlib.h>
    #include<pthread.h>
    #include<unistd.h>
    #define MAX_thread 5
    int tickets = 100;
    
    void* buyTickets(void* arg)
    {
      while(1){
       if(tickets > 0)
       {
         usleep(1000);//睡眠1000毫秒
         printf("%d---%d\n",pthread_self(),tickets);
         ticket--;
       }
       else
       {
         printf("have no tickets!!\n");
         pthread_exit(NULL);
       }
      }
      return NULL;
    }
    
    int main()
    {
      pthread_t tid[MAX_thread];
      int i = 0;
      for(i; i < MAX_thread; i++)
      {
        int ret = pthread_create(&tid[i],NULL,buyTickets,NULL);
        if(ret != 0)
        {
          perror("pthread_create error");
          return -1;
        }
      }
      for(i = 0; i < MAX_thread; i++)
      {
        pthread_join(tid[i],NULL);
      }
      return 0;
    }
    

    运行结果如下:
    在这里插入图片描述
    发现了一个问题,就是有多个用户(线程)买到了同一张编号的票,这是在现实情况中不可能存在的,则说明这样的程序实现就是不合理的。但是为什么会出现这样的情况,是因为 :

    • if 语句判断条件为真以后,代码可以并发的切换到其他线程。
    • usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段 。
    • (ticket - - ) 操作本身就不是一个原子操作,而是对应三条汇编指令。

    为了解决这个问题,则我们需要为每个线程访问的临界资源加上互斥锁。每个线程在执行时,并不是直接访问临界资源的,而是先访问互斥锁。互斥锁(mutex)的本质是一种变量,假设mutex为1时表示锁是空闲的,此时某个进程如果调用 lock 函数就可以获得锁资源,进而访问临界资源;当mutex为0时表示锁被其他进程占用,如果此时有进程调用 lock 来获得锁时会被挂起等待。互斥锁实现多个线程互斥访问临界资源的同时,需要先保证自身对互斥量 mutex 的操作是原子性的。如果直接对 mutex 进行操作(如下图1),则对 mutex 值的修改涉及到三个指令操作(①:将共享变量 mutex 从内存加载到寄存器中;②:更新寄存器里面的值,执行赋值操作;③:将新值从寄存器写回共享变量mutex的内存地址),所以整个过程是非原子性的,可能多个线程同时拿到锁资源。所以为了避免此情况的发生,真正的互斥锁底层原理是使用 swap 或 exchange 指令(如图2),这个指令的含义是将寄存器和内存单元中的数据进行交换,这条指令保证了操作的原子性。它先将 0 赋值到寄存器al,再将 mutex 与 al 中的值交换,交换后 mutex == 0,则此时外面的线程就不能访问锁资源,所以拿到锁的线程慢慢进行判断,所以当寄存器的内容 > 0 时,申请锁资源的线程可以加锁,当寄存器的内容 = = 0 时,申请锁资源的线程则挂起等待。
    在这里插入图片描述
    在这里插入图片描述

    对上面买票程序进行加锁修改后:

    #include<stdio.h>
    #include<stdlib.h>
    #include<pthread.h>
    #include<unistd.h>
    #define MAX_thread 5
    int tickets = 100;
    pthread_mutex_t mutex;
    
    void* buyTickets(void *arg)
    {
      while(1){
        pthread_mutex_lock(&mutex);//加锁
        if(tickets > 0)
        {
          usleep(1000);
          printf("%d---%d\n",pthread_self(),tickets);
          tickets--;
          pthread_mutex_unlock(&mutex);//解锁
        }
        else
        { 
          printf("have no tickets!!\n");
          //当 tickets < 0时,也就是抢最后一张票时,就不会进入上面的if(tickets > 0).所以加锁之后,需要在任意有可能退出线程之前的地方都要进行解锁
          pthread_mutex_unlock(&mutex);//解锁
          pthread_exit(NULL);
        }
      }
      return NULL;
    }
    
    int main()
    {
      pthread_t tid[MAX_thread];
      int i = 0;
      pthread_mutex_init(&mutex,NULL);//初始化
      for(i; i < MAX_thread; i++)
      {
        int ret = pthread_create(&tid[i],NULL,buyTickets,NULL);
        if(ret != 0)
        {
          perror("pthread_create error");
          return -1;
        }
      }
      for(i = 0; i < MAX_thread; i++)
      {
        pthread_join(tid[i],NULL);
      }
      pthread_mutex_destroy(&mutex);//销毁
      return 0;
    }	
    

    加锁可以解决线程组访问临界资源的线程安全问题,但是随着加锁也会带来一些问题,比如产生死锁。

    • 什么是死锁呢?

    多个线程对锁资源进行争抢访问,但是因为互相申请被其它线程所占不会释放的锁资源,导致互相等待,造成程序无法继续推进。

    • 死锁的四个必要条件

    1、互斥条件:同一时间只能有一个线程获取锁资源。
    2、不可剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺 。(自己加的锁只有自己能解)
    3、请求和保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放 。(拿着A锁请求B,若请求不到,则一直保持A的所有不释放)
    4、循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系 。线程1拿着A锁请求B锁,线程2拿着B锁请求A锁;线程1请求不到B锁不释放A锁,线程2请求不到A锁不释放B。(哲学家就餐问题)

    • 避免死锁

    1、 破坏死锁的四个必要条件(主要破坏第3个和第4个条件)
    2、避免锁未释放的场景

    • 预防死锁

    银行家算法

通过信号量实现线程的互斥

信号量的本质是:计数器 + 等待队列 + 等待与唤醒的功能接口。

  • 信号量如何实现互斥?
    信号量实现互斥只是通过一个 0/1 计数器来实现的。对临界资源进行访问之前先判断一下这个计数器,若状态为可访问,则将这个状态修改为不可访问,然后去访问数据,访问完毕之后再将状态修改为可访问状态。只需要将计数维持在 0 和 1 之间就可以实现互斥。

  • 信号量接口
    1、信号量初始化

    int sem_init(sem_t *sem, int pshared, unsigned int value);
    第一个参数:信号量变量
    第二个参数:决定了当前的信号量是用于进程间还是线程间,0 表示是用于线程间,非 0 表示使用与进程间
    第三个参数:信号量的初值
    返回值:成功返回 0;失败返回 -1

    2、信号量的等待

    ①:int sem_wait(sem_t *sem);
    阻塞等待,对于实现互斥来说,相当于将计数器置为 0 (相当于互斥锁中的加锁)
    ②:int sem_trywait(sem_t *sem);
    非阻塞等待
    ③: int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
    限制时长等待

    3、信号量的唤醒

    int sem_post(sem_t *sem);
    //唤醒信号量等待队列上的线程(唤醒前还要将计数 +1,相当于互斥锁中的解锁)

    4、信号量的销毁

    int sem_destroy(sem_t *sem);

    通过信号量实现线程互斥代码:

    #include<stdio.h>
    #include<unistd.h>
    #include<stdlib.h>
    #include<pthread.h>
    #include<semaphore.h>
    #define MAX_THR 4
    int tickets = 100;
    sem_t sem;
    void* buyTickets(void* arg)
    {
      while(1)
      {
        sem_wait(&sem);
        if(tickets > 0){
          usleep(1000);
          printf("have bought a ticket---%d\n",tickets--);
          sem_post(&sem);
        }
        else{
          sem_post(&sem);//此处一定不要忘记
          pthread_exit(NULL);
        }
      }
      return NULL;
    }
    int main()
    {
      pthread_t tid[MAX_THR];
      int i = 0;
      sem_init(&sem,0,1);
      for(i = 0; i < MAX_THR; i++)
      {
        int ret = pthread_create(&tid[i],NULL,buyTickets,NULL);
        if(ret != 0)
        {
          perror("pthread_create error");
          return -1;
        }
      }
      for(i = 0; i < MAX_THR; i++)
      {
        pthread_join(tid[i],NULL);
      }
    }
    
线程同步

通过条件变量实现线程的同步

  • 什么是条件变量

    条件变量提供了一个pcb等待队列,以及让一个线程等待(等待指的是将线程的状态置为可中断休眠状态)与唤醒(唤醒值得是将线程的可中断休眠状态置为运行状态)的功能。在线程满足获取资源的情况下,让线程访问,不满足则让线程等待,直到条件满足后唤醒等待的线程。

    但是条件变量并没有条件判断的功能,也就是不具备线程什么时候等待与唤醒的功能,因此条件判断需要用户自身来完成。

  • 条件变量函数

    1、条件变量初始化

    int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
    第一个参数:条件变量
    第二个参数:一般设置为空

    2、让一个线程阻塞,等待条件满足
    (条件变量 cond 里面有一个等待队列,将执行条件未满足的线程,加入条件变量的等待队列)

    int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
    第一个参数:条件变量
    第二个参数:互斥量

    3、唤醒等待(通过条件变量唤醒等待队列上等待的线程)

    int pthread_cond_signal(pthread_cond_t *cond);//至少唤醒一个线程
    int pthread_cond_broadcast(pthread_cond_t *cond);//广播唤醒所有线程

    第2点与第3点,使用阻塞等待、唤醒的优点在于:更实时,更加节省CPU资源,更加方便
    4、销毁条件变量

    int pthread_cond_destroy(pthread_cond_t *cond);

    在上面让线程阻塞的函数中(上面第2条函数),我们发现它的第二个参数为互斥锁,为什么pthread_ cond_ wait 需要互斥量?

    条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必 须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在 条件变量上的线程。 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据

    写了一个厨师做饭,一个顾客就餐的例子来进行体会:

    在体会之前先分析一个问题(以顾客就餐线程为例),如图:
    在这里插入图片描述
    代码如下:

    int noodles = 0;
    pthread_cond_t cond;//定义一个条件变量
    pthread_mutex_t mutex;//定义一个互斥锁
    void* custmer(void *arg)//顾客就餐
    {
      while(1){
        pthread_mutex_lock(&mutex);
        if(noodles == 0)
        {
          pthread_cond_wait(&cond,&mutex);
        }
        printf("eat a bowl of noodles\n");
        noodles--;
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&cond);
      }
      return NULL;
    }
    
    void* cooker(void *arg)//厨师做餐
    {
      while(1){
        pthread_mutex_lock(&mutex);
        while(noodles == 1)
        {
          pthread_cond_wait(&cond,&mutex);
        }
        printf("cook bowl of noodle\n");
        noodles++;
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&cond);
      }
      return NULL;
    }
    #define MAX_CUSTMER 4
    int main()
    {
      pthread_t custmer_tid,cooker_tid;
      pthread_cond_init(&cond,NULL);//初始化条件变量
      pthread_mutex_init(&mutex,NULL);//初始化互斥锁
      int i = 0;
      int ret = 0;
      ret = pthread_create(&custmer_tid,NULL,custmer,NULL);
      if(ret != 0)
      {
        perror("pthread_create custmer error");
        return -1;
      }
      ret = pthread_create(&cooker_tid,NULL,cooker,NULL);
      if(ret != 0)
      {
        perror("pthread_t create cooker error");
        return -1;
      }
      pthread_join(custmer_tid,NULL);
      pthread_join(cooker_tid,NULL);
      pthread_cond_destroy(&cond);//销毁条件变量
      pthread_mutex_destroy(&mutex);//销毁互斥锁
      return 0;
    }
    

    运行结果:
    在这里插入图片描述
    上述代码只是一个厨师,一个顾客的做饭就餐问题,但是当有多个顾客就餐时或者多个厨师做餐时(此处以多个顾客就餐为例)这个程序就会出现问题:
    在这里插入图片描述
    在这里插入图片描述
    为什么出现这个问题呢?分析如下图(以多个顾客就餐为例): 在这里插入图片描述
    注意:用户条件的判端需要使用 while 循环,因为被唤醒的多个线程,有可能都等待在锁上,解锁后,这时候有可能线程在不具备访问条件的情况下,加锁成功进行资源访问。
    修改后的代码:

    int noodles = 0;
    pthread_cond_t cond;//定义一个条件变量
    pthread_mutex_t mutex;//定义一个互斥锁
    void* custmer(void *arg)//顾客就餐
    {
      while(1){
        pthread_mutex_lock(&mutex);
        while(noodles == 0)//一定要进行循环判断
        {
          pthread_cond_wait(&cond,&mutex);
        }
        printf("eat a bowl of noodles\n");
        noodles--;
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&cond);
      }
      return NULL;
    }
    
    void* cooker(void *arg)//厨师做餐
    {
      while(1){
        pthread_mutex_lock(&mutex);
        while(noodles == 1)//一定要进行循环判断
        {
          pthread_cond_wait(&cond,&mutex);
        }
        printf("cook bowl of noodle\n");
        noodles++;
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&cond);
      }
      return NULL;
    }
    #define MAX_CUSTMER 4
    int main()
    {
      pthread_t custmer_tid,cooker_tid;
      pthread_cond_init(&cond,NULL);//初始化条件变量
      pthread_mutex_init(&mutex,NULL);//初始化互斥锁
      int i = 0;
      int ret = 0;
      for(i; i < MAX_CUSTMER; i++)//实现多个顾客线程
      {
        ret = pthread_create(&custmer_tid,NULL,custmer,NULL);
        if(ret != 0)
        {
          perror("pthread_create custmer error");
          return -1;
        }
      }
      ret = pthread_create(&cooker_tid,NULL,cooker,NULL);
      if(ret != 0)
      {
        perror("pthread_t create cooker error");
        return -1;
      }
      pthread_join(custmer_tid,NULL);
      pthread_join(cooker_tid,NULL);
      pthread_cond_destroy(&cond);//销毁条件变量
      pthread_mutex_destroy(&mutex);//销毁互斥锁
      return 0;
    }
    

    输出结果:
    在这里插入图片描述
    会发现程序卡住了,这是因为上面程序的不同角色(一个厨师与多个顾客)共用同一个条件变量,所有不具备访问资源的线程都会挂在一个条件变量的等待队列上。当厨师做了一碗面,然后唤醒一个顾客,有面之后厨师不在具备做面的条件所以也被挂再这个等待队列上,这个被唤醒的顾客进行加锁、吃面、解锁、最后需要唤醒等待队列上的线程,其实真正是想唤醒厨师线程进行做面,但是也可能唤醒另外一个顾客。另外一个顾客进行加锁访问资源,但是发现已经没有面可吃,所以就陷入了休眠等待条件满足,但是在休眠之前又没有唤醒其他线程,就会一直休眠,所以这就是程序卡住的原因。为了解决这个问题,就需要保证不同的角色要使用多个条件变量并且等待在不同的条件变量上。
    修改代码:

    int noodles = 0;
    pthread_cond_t cond_cooker;//定义一个cooker条件变量
    pthread_cond_t cond_custmer;//定义一个custmer条件变量
    pthread_mutex_t mutex;//定义一个互斥锁
    void* custmer(void *arg)//顾客就餐
    {
      while(1){
        pthread_mutex_lock(&mutex);
        while(noodles == 0)
        {
          pthread_cond_wait(&cond_custmer,&mutex);
        }
        printf("eat a bowl of noodles\n");
        noodles--;
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&cond_cooker);
      }
      return NULL;
    }
    
    void* cooker(void *arg)//厨师做餐
    {
      while(1){
        pthread_mutex_lock(&mutex);
        while(noodles == 1)
        {
          pthread_cond_wait(&cond_cooker,&mutex);
        }
        printf("cook a bowl of noodle\n");
        noodles++;
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&cond_custmer);
        //sleep(0);
      }
      return NULL;
    }
    
    #define MAX_CUSTMER 4
    int main()
    {
      pthread_t custmer_tid,cooker_tid;
      pthread_cond_init(&cond_custmer,NULL);//初始化custmer条件变量
      pthread_cond_init(&cond_cooker,NULL);//初始化cooker条件变量
      pthread_mutex_init(&mutex,NULL);//初始化互斥锁
      int i = 0;
      int ret = 0;
      for(i; i < MAX_CUSTMER; i++)
      {
        ret = pthread_create(&custmer_tid,NULL,custmer,NULL);
        if(ret != 0)
        {
          perror("pthread_create custmer error");
          return -1;
        }
      }
      ret = pthread_create(&cooker_tid,NULL,cooker,NULL);
      if(ret != 0)
      {
        perror("pthread_t create cooker error");
        return -1;
      }
      pthread_join(custmer_tid,NULL);
      pthread_join(cooker_tid,NULL);
      pthread_cond_destroy(&cond_custmer);//销毁条件变量
      pthread_cond_destroy(&cond_cooker);//销毁条件变量
      pthread_mutex_destroy(&mutex);//销毁互斥锁
      return 0;
    }
    

    从以上各种情况我们应该知道在使用条件变量实现线程同步的时候应该注意的问题:

    1、用户条件的判端需要使用 while 循环,因为被唤醒的多个线程,有可能都等待在锁上,解锁后,这时候有可能线程在不具备访问条件的情况下,加锁成功进行资源访问。
    2、保证不同的角色要使用多个条件变量并且等待在不同的条件变量上。

通过信号量实现线程的同步
信号量的本质是:计数器 + 等待队列 + 等待与唤醒的功能接口。

  • 信号量如何实现同步?
    通过自身的计数器进行资源计数,对临界资源访问之前先访问信号量,通过计数判断是否有资源能够访问,若不能访问(计数 <= 0),则等待,并且计数 -1(负数表示等待队列上的等待的线程个数);若可以访问(计数 > 0),则计数 -1,直接访问;其他线程促使条件满足后,计数+1,然后再判断是否能对临界资源进行访问。

  • 信号量接口
    1、信号量初始化

    int sem_init(sem_t *sem, int pshared, unsigned int value);
    第一个参数:信号量变量
    第二个参数:决定了当前的信号量是用于进程间还是线程间,0 表示是用于线程间,非 0 表示使用与进程间
    第三个参数:信号量的初值
    返回值:成功返回 0;失败返回 -1

    2、信号量的等待

    ①:int sem_wait(sem_t *sem);
    阻塞等待:若计数 <= 0,则-1后阻塞;否则 -1 后立即返回
    ②:int sem_trywait(sem_t *sem);
    非阻塞等待
    ③: int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
    限制时长等待

    3、信号量的唤醒

    int sem_post(sem_t *sem);
    //唤醒信号量等待队列上的线程(唤醒前还要将计数 +1)

    4、信号量的销毁

    int sem_destroy(sem_t *sem);

    通过信号量实现线程同步代码:

    #include<stdio.h>
    #include<stdlib.h>
    #include<pthread.h>
    #include<semaphore.h>
    sem_t sem_cooker;
    sem_t sem_diner;
    void* cooker(void* arg)
    {
      while(1){
        sem_wait(&sem_cooker);
        printf("cook a noole\n");
        sem_post(&sem_diner);
      }
      return NULL;
    }
    void* diner(void* arg)
    {
      while(1){
        sem_wait(&sem_diner);
        printf("eat a noodle\n");
        sem_post(&sem_cooker);
      }
      return NULL;
    }
    int main()
    {
      sem_init(&sem_cooker,0,1);
      sem_init(&sem_diner,0,0);
      pthread_t cooker_tid;
      pthread_t diner_tid;
      int ret = 0;
      ret = pthread_create(&cooker_tid,NULL,cooker,NULL);
      if(ret != 0){
        perror("pthred_create cooker error");
        return -1;
      }
      ret = pthread_create(&diner_tid,NULL,diner,NULL);
      {
        if(ret != 0)
        {
          perror("pthread_create diner error");
          return -1;
        }
      }
      pthread_join(cooker_tid,NULL);
      pthread_join(diner_tid,NULL);
      sem_destroy(&sem_cooker);
      sem_destroy(&sem_diner);
      return 0;
    }
    

    思考:信号量与条件变量实现同步的区别?

    • 信号量通过自身计数判断实现同步,条件变量需要用户条件判断。并且,由于条件变量使用的是外部用户条件判断的,所以条件变量必须搭配互斥锁一起使用;而信号量是自身计数判断,那么它是在内部保证了自身资源计数的一个原子性,因此信号量并不需要搭配互斥锁使用。
STL,智能指针和线程安全
  • STL中的容器是否是线程安全的?

    不是.,因为 STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响。如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全。

  • 智能指针是否是线程安全的?

    对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题。
    对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了 这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数

线程安全的单例模式

什么是单例模式
  • 单例模式是一种比较典型的设计模式,是程序员针对典型场景而设计出的解决方案,其中单例模式,指的是一个对象/资源只能被实例化加载一次。
线程安全的单例模式实现方式
  • 饿汉模式
  • 懒汉模式
饿汉模式
  • 什么是饿汉模式?

    所有的资源加载/对象实例化都放在程序的初始化阶段,接下来在各个线程中直接使用就可。

  • 饿汉模式的优缺点

    优点:初始化时资源直接加载,因此运行时性能会比较高以及运行比较流畅。多线程安全,无需加锁。
    缺点:将当前不用的资源也加载到内存中,因此资源消耗比较大,初始化阶段耗时比较长

    • 饿汉模式实现单例模式
    #include<iostream>
    using namespace std;
    template <class T>
    class singleton
    {
      private:
        static T _data;
      public:
       static T* GetInstance()
        {
          return &_data;
        }
    };
    template <class T>
    T singleton<T>::_data = 123;
    int main()
    {
      singleton<int> A,B;
      cout << *A.GetInstance() << " " << A.GetInstance() << endl;
      cout << *B.GetInstance() << " " << B.GetInstance() << endl;
      return 0;
    }
    

    在这里插入图片描述

懒汉模式
  • 什么是懒汉模式?

    资源并不在程序初始化阶段全部加载/初始化,而是等到使用的时候才去做判断来加载/初始化资源。

  • 懒汉模式的优点

    优点:初始化比较快,在运行阶段使用的时候也只需要加载一次(前提是不释放)
    缺点:为保证多线程安全需要加锁,损失了性能。

  • 懒汉模式实现单例模式

    实现之前应该注意的:

    1、需要设置 volatile 关键字, 防止被编译器过度优化.
    2、加锁解锁的位置,保证初始化加载过程线程安全
    3、双重 if 判定, 避免不必要的锁竞争

    互斥锁是C++ 11 的新特性,在编译时要加上 g++ -std=c++11

    #include<iostream>
    #include<mutex>
    #include<thread>
    using namespace std;
    mutex mtx;
    template<class T>
    class singleton
    {
      private:
       volatile static T* _data;
      public:
       volatile T *GetInstance()
        {
          if(_data == NULL)
          {
            mtx.lock();
            if(_data == NULL)
            {
                _data = new T(123);
            }
            mtx.unlock();
          }
          return _data;
        }
    };
    
    template<class T>
    volatile T* singleton<T>:: _data = NULL;
    
    int main()
    {
      singleton<int> A,B;
      printf("%p\n",A.GetInstance());
      cout << *A.GetInstance() << " " << A.GetInstance() << endl;
      cout << *B.GetInstance() << " " << A.GetInstance() << endl;
      return 0;
    }
    
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值