• 多线程基本概念


    多线程基本概念

    线程是进程中一个执行流程,是 CPU 进行执行调度的基本单元;
    进程是系统进行资源分配的基本单元。

    Linux 下一个进程中是可以存在多个 pcb 的,一个 pcb 就是一个执行流程。

    1、一个进程中有多个 pcb 与多个进程有多个 pcb 再多执行流程中使用有什么区别?

    例如零件加工厂加工零件,若有多个零件需要加工
    多进程
    相当于多开几个厂子,多个厂子可以同时进行零件加工;
    因此资源消耗大,但是更稳定 健壮性强

    多线程
    相当于在一个厂子里边多开几条生产线
    资源消耗小,但是健壮性不如多进程

    线程是 CPU 调用执行的基本单元,而 Linux 下 pcb 是程序运行过程的描述,因此 Linux 下线程是通过 pcb 来实现的

    在这里插入图片描述

    多进程:把多个任务分成多个程序,一个进程执行一个
    多线程:把一个整体的程序分为几个不同模块,一个 pcb 负责调度一个模块

    在这里插入图片描述

    2、线程 vs 进程

    进程是系统进行资源分配的基本单元(每运行一个程序,系统就要分配一次程序运行所需要的资源)

    线程是 CPU 进行执行调度的基本单元,在Linux下是通过 pcb 来实现的,一个进程中可以有多个pcb,因此也被称为 轻量级进程 LWP

    3、多进程 vs 多线程

    多进程:程序更具健壮性、更稳定

    多线程:
    (1)线程间通信更加灵活(共享虚拟地址,包含进程间通信在内,全局变量、函数传参…)
    (2)创建和销毁成本更低(线程之间很多资源都是共享的,创建一个线程不需要分配太多资源)
    (3)同进程的线程间调度成本更低(CPU 上加载的快表信息,页表指针…都不需要替换)

    对于程序的安全性要求大于性能和资源要求则使用多进程(例如 shell);其余使用多线程

    4、多个线程在同一个进程中同时运行为什么不会混乱?

    (1)其实每个线程调度执行的就是一个函数;
    vfork - 创建子进程,父子进程公用同一个虚拟地址空间,为了避免出现栈运行混乱,因此父进程阻塞直到子进程程序替换或退出

    (2)多线程关于执行出现混乱的解决方案:把所有有可能出现混乱的地方给每个线程单独存一份:

    线程间独有信息:标识符、栈、上下文数据、信号屏蔽字、errno…

    线程间共享信息:虚拟地址空间、文件描述符表、信号处理方式、工作路径、用户ID 、组ID…

    5、多任务处理中

    多任务处理中使用多执行流完成:
    可以更加充分地利用计算机资源, 提升任务处理效率

    在多任务处理中使用多少执行流可以由压力测试得到最合适的数量,并不是执行流越多越好,因为执行流越多,cpu 切换调度就越频繁,若执行流太多,反而会造成切换调度消耗大量资源。

    6、在任务处理中,程序分两种

    (1)cpu 密集型程序:

    一段程序几乎都是数据的运算

    (2)IO 密集型程序:

    一段程序中大部分都是 IO 操作(大部分时间都在进行 IO 操作以及等待,因此对 cpu 使用率并不高)

    线程控制

    线程中接口都是库函数实现的--------链接库文件 -lpthread

    在这里插入图片描述

    #include //头文件

    创建

    int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

    (1)thread:传入一个 pthread_t 类型变量的地址空间,用于接收线程 ID ---- 线程的操作句柄
    (2)attr:线程属性----通常置NULL
    (3)start_routine:函数指针,传入线程入口函数的地址,这个线程调度运行的就是这个函数
    (4)arg:给 start_routine 线程入口函数传入的参数;
    功能:创建一个线程,指定这个线程要运行的函数 start_routine ,并且给这个函数传入一个数据 arg
    返回值:成功 0,失败返回非0(错误码)

        1 #include                                                       
        2 #include
        3 #include
        4 
        5 void* thread_entry(void* arg)
        6 {
        7   //线程入口函数
        8   while(1){
        9     printf("i am nomal thread:%s\n",arg);
       10     sleep(1);
       11   }
       12   return NULL;
       13 }
       14 
       15 
       16 int main()
       17 {
       18   //线程的创建
       19    //int pthread_create(pthread_t *thread, const pthread_attr_t *attr,vo      id *(*start_routine) (void *), void *arg);
       20 
       21   pthread_t tid;     //用来保存线程 ID
    W> 22   char* arg="leihoua!!";     //给线程入口函数传入的参数
       23 
       24   int ret=pthread_create(&tid,NULL,thread_entry,(void*)arg);
       25   if(ret!=0){
       26     printf("thread create error!\n");
       27     return -1;
       28   }
       29    
       30   //普通线程一旦创建成功,创建出来的这个线程调度的是传入的线程入口函数,      因此能够走下来的只有主线程
       31   //线程中不存在父子线程
       32   while(1){
       33     printf("i am main thread!!\n");
       34     sleep(1);
       35   }
       36   return 0;
       37 }                         
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

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

    线程被创建出来之后,谁先运行不一定,看操作系统的调度

    (1)创建一个线程其实就是让操作系统给我们提供一个执行流,至于这个线程做什么,取决于线程的入口函数(由程序员自己决定)

    线程是一个执行流,调度一段函数运行;而函数是一段功能指令的集合

    (进程:fork() 创建一个子进程之后,父子进程公用一段代码)

    多核
    指的是 cpu 多个核心,一个 cpu 核心有一套自己的寄存器,可以独立进行数据处理指令执行
    线程
    是软件层面的一条执行流,是一段程序运行的描述过程

    单核 cpu 可以处理多个线程----并发(cpu 分时处理机制,轮询处理)
    多核 cpu 可以处理多个线程----并行(同时进行)

    查看线程信息 : ps -L 选项
    查看的其实是轻量级进程信息

    (2)一个程序运行起来,默认会创建一个线程(pcb),这个线程有自己的 pid
    如果下边通过 pthread_create 创建了一个线程(pcb),这个线程也有自己的 pid
    真正使用 ps 查看进程信息的时候查看的是主线程 pcb 对应的 pid 信息

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

    (3)pthread_create 接口创建第一个参数获取到的 tid 并不是轻量级进程的 pid(lwp)

    在这里插入图片描述
    tid 实际保存的是一个地址,这个地址指向了线程相对独立的空间(关于该线程的上层描述)

    终止

    线程终止:退出一个线程的运行

    线程其实调用运行的是创建时所传入的入口函数,因此线程的入口函数运行完了,线程就退出了

    1、在线程入口函数中 return

    注意:main 函数中 return 退出的不仅仅是主线程,还有整个程序

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

    2、在任意位置调用接口实现线程的退出
    void pthread_exit(void *retval);
    retval :用于设置线程的退出返回值
    谁调用谁退出

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

    3、在任意位置调用接口用于取消指定线程的运行
    int pthread_cancel(pthread_t thread);
    一个线程若是被取消的,则返回值就不是一个正经的返回值

    线程正常退出是有返回值设置的,但若是 pthread_cancel 取消线程的,则没有设置返回值。

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

    等待

    1、主线程退出,其实并不会影响其他线程的运行(不多见);
    所有线程退出了,则进程退出释放所有资源;
    若进程要退出,则会先退出所有线程。
    2、一个线程退出了,资源也并没有完全被释放(要保存返回值)

    线程等待

    等待指定的线程退出,获取退出线程的返回值,回收退出线程的所有资源

    int pthread_join(pthread_t thread, void **retval);
    (1)thread:要等待退出的指定线程 tid
    (2)retval:用于获取线程的退出返回值;因为线程退出返回值为 void*,因此传入指针变量的地址,将变量的地址放在指针变量空间
    返回值:成功返回 0,失败返回错误编号

    线程等待:
    在这里插入图片描述

    #include
    #include
    #include
    
    void* thread_entry(void* arg)
    {
      //线程入口函数
      int count=0;
      while(1){
        printf("i am nomal thread:%s\n",arg);
        count++;
        
        //pthread_exit(NULL);    //任意位置调用可以退出线程
        
        if(count==3)return "nihao ";         //返回值为字符串 类型
        
        sleep(1);
      }
      return NULL;
    }
    
    int main()
    {
      //线程的创建
       //int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
    
      pthread_t tid;     //用来保存线程 ID
      char* arg="leihoua!!";     //给线程入口函数传入的参数
    
      int ret=pthread_create(&tid,NULL,thread_entry,(void*)arg);
      if(ret!=0){
        printf("thread create error!\n");
        return -1;
      }
    
      //线程等待
      void* retval;   //用于接收退出返回值
      ret=pthread_join(tid,&retval);     //等待指定的线程 tid 退出,并获取退出返回值 retval  
      if(ret!=0){
        printf("pthread_join failed\n");
        return -1;
      }
      printf("exit----%s\n",retval);  //打印退出返回信息
    
    
      //pthread_cancel(tid);         //任意位置调用退出线程
    
       
      //普通线程一旦创建成功,创建出来的这个线程调度的是传入的线程入口函数,因此能够走下来的只有主线程
      //线程中不存在父子线程
      while(1){                      //打印线程ID
        printf("i am main thread!!----%d\n",tid);
        sleep(1);
      }
      return 0;
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    运行结果:
    在这里插入图片描述

    线程之间传递数据一定要注意数据的生命周期
    (可以是 malloc 动态申请的空间 \ static 静态数据 \ 字面常量 )

    若一个线程是被取消的,则获取的返回值是一个宏:PTHREAD_CANCELED ==== 》 (void*)(-1)

    在这里插入图片描述

    分离

    在线程属性中,有一个属性叫做分离属性,默认值是 joinable 状态,表示线程退出后不会自动释放资源, 需要被其他线程等待。

    但有时候我们并不关心一个线程的返回值,也不想等待它退出,则这个时候将这个分离属性设置为 detach 状态,表示线程退出后自动释放所有资源,不需要被释放(资源是自动释放的,因此也不能被等待)

    int pthread_detach(pthread_t thread);
    设置指定线程的分离属性为 detach
    返回值:成功返回0 ,失败返回错误编号

    在这里插入图片描述

    =============================================================================================================================

    在这里插入图片描述

    线程安全

    基本概念

    不安全:在多线程程序中,若涉及到了对共享资源的操作,则有可能导致数据的二义性。

    线程安全:对共享资源的操作不会导致数据二义性

    实现

    实现:如何实现多线程对共享资源的操作不会出问题?
    同步:通过条件控制,让多执行对资源的获取更加合理
    互斥:通过同一时间执行流对资源访问的唯一性,保证访问安全

    互斥的实现:互斥锁(读写锁、自旋锁…)

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

    互斥锁实现互斥----实现对共享资源的唯一访问

    本质也就是一个 0/1计数器,通过 0/1 标记资源的访问状态(0-不可访问,1-可访问)
    在访问资源之前进行加锁操作(通过状态判断是否可以访问,不可访问则阻塞);
    在访问资源之后进行解锁操作(将资源状态置为可访问状态,唤醒其他阻塞的进程);

    多个线程想要实现互斥,就必须访问同一个锁-----------意味着锁是一个共享资源

    互斥

    互斥锁

    互斥锁如何实现自身安全?----> 一步置换

    互斥锁本身的计数器操作是原子操作

    对变量内容的修改过程----------------将变量从内存中加载到(CPU)寄存器中,在寄存器中修改变量值,再将修改之后的值返回到内存中。

    因此对于互斥锁来说,若某一个线程将互斥锁的数据(1-可访问)加载到cpu上进行处理,要修改成 0-不可访问,但是还没有将处理之后的数据返回内存之前,线程进行了切换,第二个进程也要进行加锁操作,要将互斥锁的值(1)加载到 cpu ,此时会形成矛盾。

    因此 指令 exchange :交换 cpu 指定寄存器与内存中的数据
    互斥锁操作:
    1)先将指定寄存器中值修改为 0
    2)将寄存器与内存中数据互换
    3)判断是否符号获取锁的条件/判断能否可以加锁

    置换操作是一条指令完成的,不可被打断
    置换之前,把寄存器的值置为 0,因此置换之后内存中互斥锁的值为 0,这样就保证,不管当前线程是否可以加锁,至少在之后的线程都不能进行加锁。

    接口

    pthread_mutex_t :互斥锁变量类型

    初始化互斥锁

    int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
    (1)mutex:互斥锁变量地址
    (2)attr:互斥锁变量属性-----NULL

    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    这种初始化,不需要使用 destroy 进行释放

    访问资源前进行加锁

    int pthread_mutex_lock(pthread_mutex_t *mutex);
    阻塞加锁; 加不上锁则一直等待

    int pthread_mutex_trylock(pthread_mutex_t *mutex);
    非阻塞加锁;加不上锁则立即报错返回

    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    访问资源完毕之后进行解锁

    释放销毁互斥锁

    int pthread_mutex_destroy(pthread_mutex_t *mutex);

    练习

    1、多线程中共享资源访问若不加锁可能会出什么问题?

    2、如何通过使用互斥锁来保护临界区(共享资源的访问过程)

    练习代码:

    #include
    #include
    
    int ticket=100;
    
    void* Scalper(void* arg)
    {
      //入口函数
      while(1){
        if(ticket>0){
          printf("%p 抢到了 %d 号票\n",pthread_self(),ticket);
          ticket--;
        }else{
          printf("票没了~~!\n");
          break;
        }
      }
      return NULL;
    }
    
    int main()
    {
      pthread_t tid[4];   //创建四个线程
    
      int i=0;
      for(i=0;i<4;++i){
        int ret=pthread_create(&tid[i],NULL,Scalper,NULL);
        //四个线程调用入口函数的顺序是不一定的
        if(ret!=0){
          printf("create pthread error~!\n");
          return -1;
        }
      }
    
      for(i=0;i<4;++i){
        pthread_join(tid[i],NULL);      //等待指定的线程退出,退出返回值 NULL
      }
    
      return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    在这里插入图片描述
    由于判断有没有票与抢票过程不是原子性的不是一次性完成的,中间有可能被打断,因此存在其他线程也可以抢-----------------------定义互斥锁

    在定义线程之前定义锁, 并对其进行初始化操作。

    #include
    #include
    #include
    
    int ticket=100;
    
    void* Scalper(void* arg)
    {
      pthread_mutex_t *mutex=(pthread_mutex_t*)arg;            //定义互斥锁
      //入口函数
      while(1){
        pthread_mutex_lock(mutex);   //进行加锁操作
        usleep(10);
        
        if(ticket>0){
          printf("%p 抢到了 %d 号票\n",pthread_self(),ticket);
          ticket--;
          //抢票结束进行解锁
          pthread_mutex_unlock(mutex);
    
        }else{
          printf("票没了~~!\n");
          pthread_mutex_unlock(mutex);   
      //加锁后在任意有可能退出线程的地方都需要解锁
          pthread_exit(NULL);    //线程退出
        }
      }
      usleep(1);
      return NULL;
    }
    
    int main()
    {
      pthread_t tid[4];   //创建四个线程
      
      pthread_mutex_t mutex;   //定义互斥锁
      pthread_mutex_init(&mutex,NULL);   //互斥锁的初始化操作
    
      int i=0;
      for(i=0;i<4;++i){
        int ret=pthread_create(&tid[i],NULL,Scalper,&mutex);  //将锁传给入口函数参数
        if(ret!=0){
          printf("create pthread error~!\n");
          return -1;
        }
      }
    
      for(i=0;i<4;++i){
        pthread_join(tid[i],NULL);      //等待指定的线程退出,退出返回值 NULL
      }
    
      //线程运行结束释放锁
      pthread_mutex_destroy(&mutex);
      return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    死锁

    多个线程对锁资源的争抢使用不当导致程序流程卡死,无法继续向下推进的状态

    (1)加锁之后没有释放就退出,导致其它线程获取不到锁资源卡死;
    (2)多锁使用,加锁顺序不当,线程 1 加锁顺序为 AB,线程 2 加锁顺序为 BA

    死锁产生的必要条件

    (1)互斥条件
    同一时间一个锁只能被一个线程获取,多个线程无法同时加同一把锁;
    (2)不可剥夺条件
    一个线程加的锁,只有自己可以释放,其他线程不能释放
    (3)请求与保持条件
    线程加 A 锁后请求 B 锁,若请求不到 B 锁也不释放 A 锁
    (4)环路等待条件
    线程 1 加锁 A 请求 B 锁,线程 2 加锁 B 请求 A 锁

    预防死锁产生-------破坏死锁产生的必要条件

    1、2 是互斥锁的要义所在,是无法进程破坏的;因此在写代码时应格外注意:
    (1)多个线程间加锁顺序要保持一致:预防环路条件产生
    (2)采用非阻塞加锁,若加不上锁,则把已经加锁成功的释放----破环请求与保持条件

    避免死锁

    (1)银行家算法--------风险控制

    将系统运行状态分为:安全状态 、不安全状态
    建立三张表:
    资源剩余表:有没有这个资源可以分配

    资源分配表:一旦请求的资源被分配了,是否会导致系统进入非安全状态(不能分配)

    资源请求表:谁请求什么资源

    (2)死锁检测算法
    、、、

    同步

    通过条件控制让多线程对资源的获取更加合理

    互斥只能保证安全,不一定能保证合理
    同步能保持合理,不一定能保持安全

    资源获取的合理:
    有资源才能处理,没资源则阻塞—等有资源在唤醒处理

    条件变量

    提供一个 pcb 等待队列以及阻塞和唤醒线程的接口

    思想
    若一个线程不满足获取资源的条件则通过阻塞接口阻塞线程
    一个线程促使资源获取条件满足了,则通过唤醒接口唤醒线程
    注意
    (1)条件变量本身并不知道什么时候该阻塞,什么时候唤醒,仅仅是提供了阻塞和唤醒的接口-------------具体由程序员来控制
    (2)促使条件满足 & 判断获取条件是否满足,是在多个线程中操作,因此条件的操作其实就是一个临界资源的操作(需要加锁保护)

    条件变量是与互斥锁搭配使用的

    操作接口:

    1.定义条件变量
    pthread_cond_t :条件变量的变量类型

    2.初始化条件变量
    (1)pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
    (2)int pthread_cond_init(pthread_cond_t *restrict cond , const pthread_condattr_t *restrict attr);
    cond:条件变量
    attr:条件变量的属性----- NULL

    3.阻塞接口:条件变量是搭配互斥锁一起使用
    (1)阻塞
    int pthread_cond_wait(pthread_cond_t *restrict cond , pthread_mutex_t *restrict mutex);
    cond:条件变量
    mutex:互斥锁

    (2)有时长限制的阻塞
    int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
    返回值为:ETIMEDOUT 表示时间到了

    4.唤醒接口
    (1)int pthread_cond_signal(pthread_cond_t *cond);
    唤醒至少一个阻塞队列中的线程
    (2)int pthread_cond_broadcast(pthread_cond_t *cond);
    唤醒等待队列中的所有线程

    5.销毁接口
    int pthread_cond_destroy(pthread_cond_t *cond);

    示例代码

    例如,顾客前往餐厅吃饭的过程

    //首先定义好所需变量-----全局
         int counter=0;  //0-柜台没饭,1-柜台有饭
         pthread_mutex_t mutex;  //定义互斥锁
         pthread_cond_t cond;  //定义条件变量
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    顾客:
    假如餐厅柜台上此时有饭,则顾客进来之后可以直接吃饭,假如柜台上此时没有饭或者想要再来一碗,则会等待厨师做饭(阻塞----阻塞前唤醒厨师来做饭)

        9 void* Customer(void* arg){
       10   while(1){
       11     pthread_mutex_lock(&mutex);   //首先加锁
       12     if(counter==0){  //判断有无饭
       13       //柜台没有饭,需要阻塞等待----解锁-休眠-被唤醒后加锁
       14       pthread_cond_wait(&cond,&mutex);
       15     }
       16     //否则,counter = 1 有饭
       17     printf("饭好吃,再来一碗!\n");
       18     counter=0;    //此时需要修改 counter 的值
       19     pthread_cond_signal(&cond);  //唤醒厨师做饭
       20     pthread_mutex_unlock(&mutex);  //解锁
       21   }
       22   return 0;
       23 }
       24 
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    厨师:
    假如餐厅柜台上没有饭则做饭,假如柜台上有饭则阻塞(唤醒阻塞等待队列中顾客来吃饭)

       25 void* Cook(void* arg){
       26   while(1){
       27     pthread_mutex_lock(&mutex);  //首先加锁
       28     if(counter==1){
       29       //有饭,则阻塞
       30       pthread_cond_wait(&cond,&mutex);
       31     }
       32     //否则,counter = 0 没有饭,需要做饭
       33     printf("饭好了,快来吃饭!\n");
       34     counter=1;   //做好饭,修改 counter 值
       35     pthread_cond_signal(&cond);   //唤醒顾客来吃饭
       36     pthread_mutex_unlock(&mutex);  //解锁                                    
       37   }
       38   return 0;
       39 }
       40 
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    因此,顾客与厨师之间是交替进行操作的一个过程(一个顾客一个厨师的测试),详细代码参考gitee中 thread_conda.c 文件

    在这里插入图片描述

    倘若对于多顾客多厨师的现象:

    不更改顾客与厨师入口函数,仅增加线程个数则会发生不匹配现象

    在这里插入图片描述

    分析
    有四名顾客,此时 1号顾客加锁成功可以吃饭,其余顾客则会阻塞在加锁这一步,而 1号顾客吃完饭后唤醒厨师(唤醒阻塞队列中至少一个线程)来做饭,若此时cpu时间片并未轮转到厨师,而是分配给了2号3号4号顾客,顾客判断没有饭吃,也会唤醒厨师来做饭,因此会导致厨师被唤醒多次,同理,厨师做好饭唤醒顾客也会唤醒多次,从而导致不匹配现象的出现。

    多个线程需要将入口函数中 if 换为 while 循环,但又发生了阻塞:

    在这里插入图片描述

    这是因为顾客与厨师都等待在同一个阻塞队列中,就有可能在一个顾客吃完饭之后,唤醒的不是厨师而是其他顾客,导致多名顾客没有饭吃而重新陷入休眠,造成流程卡死--------------------------------解决方案:让不同的线程等待在不同的队列上(多个条件变量)

    因此,改进之后的最优化代码:

        1 #include                                                            
        2 #include
        3 
        4 //多个顾客多个厨师
        5 //
        6 
        7 int counter=0;  //0-柜台没饭,1-柜台有饭
        8 
        9 pthread_mutex_t mutex;  //定义互斥锁
       10 pthread_cond_t cond_cook;  //定义条件变量
       11 pthread_cond_t cond_cus;  //定义条件变量
       12 
       13 void* Customer(void* arg){
       14   while(1){
       15     pthread_mutex_lock(&mutex);   //首先加锁
       16     while(counter<=0){  //判断有无饭
       17       //柜台没有饭,需要阻塞等待----解锁-休眠-被唤醒后加锁
       18       pthread_cond_wait(&cond_cus,&mutex);
       19     }
       20     //否则,counter = 1 有饭
       21     printf("饭好吃,再来一碗!\n");
       22     counter--;
       23     pthread_cond_signal(&cond_cook);  //唤醒厨师做饭
       24     pthread_mutex_unlock(&mutex);  //解锁
       25   }
       26   return NULL;
       27 }
       28 
       29 void* Cook(void* arg){
       30   while(1){
       31     pthread_mutex_lock(&mutex);  //首先加锁
       32     while(counter>=1){
       33       //有饭,则阻塞
       34       pthread_cond_wait(&cond_cook,&mutex);
       35     }
       36     //否则,counter = 0 没有饭,需要做饭
       37     printf("饭好了,快来吃饭!\n");
       38     counter++;
       39     pthread_cond_signal(&cond_cus);   //唤醒顾客
       40     pthread_mutex_unlock(&mutex);  //解锁
       41   }
       42   return NULL;
       43 }
       44 
       45 int main()
       46 {
       47   //创建两个线程----顾客,厨师
       48   pthread_t cus_tid[4],cook_tid[4];
       49   int ret;                                                                   
       50   //初始化操作----初始化互斥锁与条件变量
       51   pthread_mutex_init(&mutex,NULL);
       52   pthread_cond_init(&cond_cus,NULL);
       53   pthread_cond_init(&cond_cook,NULL);
       54 
       55   int i=0;
       56   for(;i<4;++i){
       57     ret=pthread_create(&cus_tid[i],NULL,Customer,NULL);
       58     if(ret!=0){
       59       perror("pthread_create error!\n");
       60       return -1;
       61     }
       62   }
       63 
       64   for(i=0;i<4;++i){
       65     ret=pthread_create(&cook_tid[i],NULL,Cook,NULL);
       66     if(ret!=0){
       67       perror("pthread_create error!\n");
       68       return -1;
       69     }
       70   }
       71 
       72   //线程等待-----等待任意固定线程退出
       73   pthread_join(cus_tid[0],NULL);
       74   pthread_join(cook_tid[0],NULL);
       75                                                                              
       76 
       77   //销毁
       78   pthread_mutex_destroy(&mutex);
       79   pthread_cond_destroy(&cond_cus);
       80   pthread_cond_destroy(&cond_cook);
       81 
       82   return 0;
       83 }        
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83

    正确运行的结果:
    在这里插入图片描述

    线程应用

    生产者与消费者模型

    应用场景:有大量数据任务产生的同时需要进行任务处理的场景

    单执行流处理的缺点:
    (1)效率低
    (2)资源利用不一定合理(由于任务的产生与处理在同一个线程中)
    (3)耦合度比较强:若一个任务的处理方式发生了一些变化,则需要对整体代码进行修改重新编译

    生产者与消费者模型针对的是大量数据产生并处理的场景,针对这种场景的优势:
    (1)解耦合
    (2)支持忙闲不均
    (3)支持并发

    可以根据数据的产生速度来决定生产者线程的数量;
    可以根据任务处理的速度来决定消费者线程的数据;
    并且若峰值压力下,数据产生过快,可以在任务队列中将数据放入缓冲区,慢慢处理

    实现
    生产者与消费者模型中,其实生产者与消费者就是两种不同功能角色的线程,通过线程安全的队列进行缓冲交互

    生产者与生产者之间的关系:互斥
    消费者与消费者之间的关系:互斥
    生产者与消费者之间的关系:同步+互斥(不能同时进行操作)

    封装实现一个线程安全的阻塞队列,创建多个线程入队数据,多个线程出队数据处理

    在这里插入图片描述

    生产者与消费者模型相关代码:(阻塞队列中会存在边进边出的情况)
    添加链接描述

    信号量

    主要用来实现线程间的互斥与同步
    本质上是一个计数器:
    P操作:对计数器-1,若小于0则表示没有资源–阻塞线程
    V操作:对计数器+1,唤醒一个阻塞中的线程

    同步
    初始化计数器为资源数量,线程A获取资源前进行P操作;线程B产生一个资源后进行V操作
    互斥
    初始化计数器为1,线程访问资源之前进行P操作,访问资源完毕之后进行V操作(同一线程模拟PV实现加解锁)

    信号量是通过资源计数来实现同步,主要是统计资源的(信号量通过自身的计数就可以完成条件判断)

    POSIX标准下接口信息:

    #include 头文件

    信号量变量类型:sem_t sem

    信号量初始化:
    int sem_init(sem_t *sem, int pshared, unsigned int value);
    (1)sem:定义的信号量变量
    (2)pshared:0-线程间使用,!0-进程间使用(共享内存实现)
    (3)value:初始化值

    P 操作:
    int sem_wait(sem_t *sem); //阻塞接口
    int sem_trywait(sem_t *sem); //非阻塞接口–报错返回
    int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); //限制时长的阻塞

    V 操作:
    int sem_post(sem_t *sem);

    信号量的销毁:
    int sem_destroy(sem_t *sem);

    采用信号量实现环形队列(通常采用数组来实现):
    pwrite 指向当前可以进行写入数据的位置
    pread 指向当前可以进行读取数据的位置
    当 (pwrite+1)%capacity==pread 时候,说明当前环形队列已经存满了

    在这里插入图片描述

    使用信号量来实现环形队列相关测试代码:
    添加链接描述

    线程池*

    构建一个线程的池子,有数据来了就从池子中分配一个线程出来处理即可,处理完毕还回去

    实际的实现:
    先创建固定数量的线程,以及一个线程安全的任务队列,池子中线程的工作就是不断从任务队列中取出任务进行处理,处理完了没任务了就阻塞

    优点:
    (1)数据任务可以在任务队列中缓冲,并且数量都是可以灵活控制的,避免峰值压力下资源耗尽的风险;
    (2)避免了线程的频繁创建与销毁带来的时间成本;

    重点:
    线程池只是一个执行流的池子,本身并不知道一个任务该如何进行处理,因此需要设计一个任务类:

    在这里插入图片描述

    在这里插入图片描述

    线程池相关测试代码参考:
    添加链接描述

    单例模式

    设计模式:针对典型场景所设计的解决方案
    单例模式:就是一种典型的设计模式

    单例模式针对的场景:一个类只能实例化一个对象

    (1)资源角度:资源在内存中只能存在一份
    (2)数据角度:若只有一个对象,则数据无论在什么时候获取都是一致的

    单例模式的实现:对象只有一个,提供一个访问接口进行访问

    饿汉模式

    直接将对象实例化完毕,资源申请完毕,以便于用的时候能够随时使用(以空间换时间)

    好处:效率最大化,资源使用时候直接可以用(不涉及线程安全---对象实例化在初始化时候完成)
    坏处:程序初始化速度慢,占用内存资源多

    实现
    将对象设置为静态资源,这样就可以在程序初始化阶段完成实例化

    在这里插入图片描述
    (1)类内声明唯一的对象,需要声明为静态对象(静态成员类外定义),这样才能保证在初始化阶段进行实例化;
    (2)构造函数私有化(保证无法在类外构造其他对象,一个类只能构造一个对象);
    (3)提供访问接口进行访问

    懒汉模式

    对象在使用的时候在进行实例化,资源用的时候在申请(延迟加载思想)

    好处:占用内存资源少,程序初始化速度快(涉及到线程安全问题)
    坏处:需要访问一个资源时候需要实例化对象进行加载资源

    实现
    在这里插入图片描述

    (1)定义静态对象指针,用的时候进行 new;
    (2)new 过程中需要考虑线程安全问题,需要进行加解锁;
    (3)double check 双重检验,提升效率;
    (4)volatile 进行修饰,防止编译器过度优化(提示内存可见性)

    ps:
    线程相关知识就分享到这里啦,欢迎各位读者评论留言鸭~~

    在这里插入图片描述

  • 相关阅读:
    Vue学习:axios
    测试记录-验证码测试
    PPT素材、PPT模板免费下载
    c# Dictionary vs SortedDictionary
    QTableView样式设置
    【C语言 数据结构】队列 - 链式、顺序
    Lazarus上好用的 Indy TCP client 组件
    音视频从入门到精通——超简单的基于FFMPEG+SDL的视频播放器(一)
    nginx(五十三)nginx中使用的PCRE正则
    Powershell脚本自动备份dhcp数据库
  • 原文地址:https://blog.csdn.net/weixin_46655027/article/details/128076094