【linux】多线程

线程是操作系统调度的最小单位
进程是操作系统分配资源的最小单位
线程是什么

线程:操作系统调度的最小单位,线程被包含在进程中,是进程中实际的运行单位。
  一个线程指的是进程中一个单一顺序的控制流,一个进程可以并发多个线程,每个线程并行执行不同任务

  线程是独立调度和分派的基本单位

  同一进程的多个线程将共享进程中的全部系统资源,ege:进程的虚拟地址空间,文件描述符表

  同一进程的多个线程有属于自己的调用栈,自己的寄存器

  一个进程可以有很多线程,每个线程并行执行不同的任务

  有时线程也被称为轻量级进程,但轻量级线程更多指内核线程,而用户线程称为线程

线程的优点:
  1. 创建一个线程的开销比创建一个进程的开销小的多
  2. 创建一个线程所用的资源也比创建一个进程所用的资源小的多
  3. 同一进程当中不同的线程可以并行运行
  4. 多线程程序可以提高程序的运行效率
线程的缺点:
  1. 健壮性低:假如多线程程序的多个执行流,一旦有一个发生异常,就会导致运行结果异常,所以线程之间缺乏保护
  2. 缺乏访问控制:当两个线程对一个临界资源同时访问时,该谁访问,该怎么访问,就得提前说明
  3. 调试,编写困难,需要考虑的东西多,就比如说,需要事先说明访问临界资源的顺序,防止程序产生二义性
  4. 性能损失:线程的上下文切换是有可能耗费大量的资源
线程的独有和共享
独有:                     共享:
    tid:线程id                  共享进程的虚拟地址空间
    自己的调用栈                文件描述符表
    信号屏蔽字                  当前进程的工作路径
    调用优先级                  用户id和用户组id
    errno                       
    自己的寄存器
多线程和多进程的区别

  多进程:每个进程都有属于自己的虚拟地址空间,这也是进程独立的原因,所以一个进程的奔溃,不会对其他进程产生影响。多进程确实提高程序运行效率,但也带来了进程间通信的问题

  多线程:每个进程中的执行流都共享当前进程的那一个虚拟地址空间,所以一个执行流的异常,会导致整个程序的异常。多线程也提高了程序运行效率,但也使得程序健壮性低,代码编写复杂的问题

多线程的好处

有的程序执行时,不是只有main函数这一个执行流,还有进程当中就会有多个执行流,这些执行流可以在同一时刻,拿着不同的cpu进行执行相应的指令(执行代码),可以是一个并行状态运行代码;由于在同一时刻进程中可以执行众多不同的代码,这样就可以大大的提高程序的执行效率

线程控制:线程创建 线程终止 线程等待 线程分离

前提:线程控制中的接口都是库函数,使用线程控制的接口需要链接线程库(pthread),链接的时候增加Ipehread
线程创建

pthread_self()

头文件
#include <pthread.h>

函数原型
pthread_t pthread_self(void);

函数作用:
获得线程自身的ID。pthread_t的类型为unsigned long int,所以在打印的时候要使用%lu方式,否则显示结果出问题

功能
获取当前调用线程的 thread identifier(标识号).

代码演示
    1 #include<stdio.h>
    2 #include<pthread.h>
    3 
W>  4 void* func(void* arg)                               
    5 {
    6   printf("func pthread:%lu\n",pthread_self());
W>  7 }
    8 
    9 int main()
   10 {
   11   pthread_t pid;
   12   int ret = pthread_create(&pid,NULL,func,NULL);
E> 13   sleep(3);
E> 14   printf("main pthread:%d\n",getpid());
   15 
   16   return 0;
   17 }

打印
[1@localhost 2]$ vi self.c 
[1@localhost 2]$ make
gcc tc.c -o t_out  -g  -lpthread 
gcc self.c -o s_out -g -lpthread
[1@localhost 2]$ ./s_out 
func pthread:140222301116160
main pthread:8797

pthread_create()

函数简介

  pthread_create是UNIX环境创建线程函数

头文件

  #include<pthread.h>

函数声明

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

返回值

  若成功则返回0,否则返回出错编号

参数
  第一个参数为指向线程标识符的指针。
  thread:线程标识符并不是线程id,pthread_t:线程独有空间的首地址。

  第二个参数用来设置线程属性。
  attr:线程属性,pthread_attr_t 是一个结构体,这个结构体完成对新创建线程属性的设置;
      ege:创建线程时,这个参数为NULL,则认为采用默认属性(线程栈的大小,线程栈的起始位置,线程的分离属性,线程的优先级调度属性)

  第三个参数是线程运行函数的地址。
    thread_start:线程入口函数,接收一个函数地址,这个函数是void*返回值,void*:参数
  最后一个参数是运行函数的参数。
    arg:给线程入口函数传递的参数值
    void* 可以传递任意类型,包括自定义结构体,类实例化指针对象,也可以传递在堆上开辟的内存(但在线程入口函数结束时,要将其释放掉,否则就可能造成内存泄漏)
注意
  在编译时注意加上-lpthread参数,以调用静态链接库。因为pthread并非Linux系统的默认库。 
  
  
  
线程终止(只终止某个线程不终止整个进程)
线程终止的方式
  1. 从线程入口函数的return返回
  2. 线程可以调用pthread_exit终止自己
  3. 一个线程也可以调用pthread_cancel终止同一进程中的其他线程

pthread_exit()

pthread_exit函数
功能:线程终止
原型:
void pthread_exit(void* value_ptr)
参数:
value_ptr:value_ptr不要指向局部变量
返回值:
无返回值,跟进程一样,线程结束时无法返回它的调用者(自身)
主线程调用pthread_exit退出时,进程不会退出,但主线程状态变成Z
代码演示
  1 #include<stdio.h>
  2 #include<pthread.h>                                                                                                 
  3 #include<unistd.h>
  4 
  5 void* func(void* arg)
  6 {
  7   int i=*(int*) arg;
  8   /*if(i==1)
  9   {
 10     pthread_exit(NULL);
 11     return NULL;
 12   }*/
 13  //sleep(5);
 14   printf("func pthread:%lu\t func_id:%d\n",pthread_self(),i); 
 15   printf("pthread_exit\n");
 16   //谁调用,谁退出
 17   pthread_exit(NULL);
 18 
 19 }
 20 int main()
 21 {
 22   pthread_t pid;
 23   int i=0;
 24   int ret;
 25   for(i=0;i<3;++i)
 26   {
 27     ret=pthread_create(&pid,NULL,func,(void*)&i);
 28     if(ret<0)
 29       {
 30         perror("main pthread");
 31         return 0;
 32       }
 33   }
 34   sleep(1);
 35   int a=5; 
 36   while(a)
 37   {
 38   printf("main pthread:%d\n",getpid());
 39   --a;
 40   }
 41   return 0;
 42 }   
 
 打印
 [1@localhost 2]$ vi test_exit.c 
[1@localhost 2]$ make
g++ tc.c -o t_out  -g  -lpthread 
gcc self.c -o s_out -g -lpthread
gcc test_exit.c -o te_out -g -lpthread
[1@localhost 2]$ ./te_out 
func pthread:140319532144384	 func_id:3
pthread_exit
func pthread:140319540537088	 func_id:3
pthread_exit
func pthread:140319523751680	 func_id:3
pthread_exit
main pthread:17868
main pthread:17868
main pthread:17868
main pthread:17868
main pthread:17868
[1@localhost 2]$ 


pthread_cancel函数 
功能:根据传入的线程id取消一个执行中的线程 
原型 int pthread_cancel(pthread_t thread); 
参数 thread:线程id,通过pthread_self获得 
返回值:成功返回0;失败返回错误码

代码演示:
只将上面代码中func函数修改
    5 void* func(void* arg)
    6 {
    7   int i=*(int*) arg;
    8   /*if(i==1)
    9   {
   10     pthread_exit(NULL);
   11     return NULL;
   12   }*/
   13 
   14   printf("func pthread:%lu\t func_id:%d\n",pthread_self(),i);
   15   // printf("pthread_exit\n");
   16   //谁调用,谁退出
   17   // pthread_exit(NULL);
   18   printf("pthread_cnole\n");
   19   pthread_cancel(pthread_self());             
W> 20 }

打印
[1@localhost 2]$ vi test_exit.c 
[1@localhost 2]$ make
g++ tc.c -o t_out  -g  -lpthread 
gcc self.c -o s_out -g -lpthread
gcc test_exit.c -o te_out -g -lpthread
[1@localhost 2]$ ./te_out 
func pthread:139902174869248	 func_id:2
func pthread:139902166476544	 func_id:3
pthread_cnole
pthread_cnole
func pthread:139902183261952	 func_id:2
pthread_cnole
main pthread:18530
main pthread:18530
main pthread:18530
main pthread:18530
main pthread:18530
[1@localhost 2]$ 

pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数
的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了

默认创建线程时,认为线程是joinable的

joinable:当线程退出的时候,需要其他线程来回收该线程的资源,若没有线程回收,则在共享区中保留该线程的资源,即不会释放该线程的资源,这样就导致了内存泄漏

线程等待

为啥要线程等待?

  及时释放退出线程的资源,防止内存泄漏

pthread_join()函数

函数pthread_join用来等待一个线程的结束,线程间同步的操作
头文件 :
#include <pthread.h>

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

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

参数 :
thread:线程标识符,即线程ID,标识唯一线程。
retval: 用户定义的指针,用来存储被等待线程的返回值。
void** :获取线程退出时
    使用return退出,retval所指向的单元里存放的是thread线程函数的返回值。
    使用pthread_exit退出,retval所指向的单元存放的是传给pthread_exit的参数。
    使用pthread_cancel退出,retval所指向的单元里存放的是常数PTHREAD_CANCELED。
    如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。

返回值 : 
0代表成功。 失败,返回的则是错误号。
线程分离
线程分离的作用:设置线程的joinable属性为detach属性,detach属性:线程退出后所剩资源由操作系统回收
pthread_detach(pthread_self())创建一个线程默认的状态是joinable。
外文名pthread_detach默认的状态joinable调用函数 pthread_join子线程的状态detached返回值成功时返回0,失败返回其他值
创建一个线程默认的状态是joinable, 如果一个线程结束运行但没有被join,则它的状态类似于进程中的Zombie Process,即还有一部分资源没有被回收(退出状态码),所以创建线程者应该pthread_join来等待线程运行结束,并可得到线程的退出代码,回收其资源(类似于wait,waitpid)
但是调用pthread_join(pthread_id)后,如果该线程没有运行结束,调用者会被阻塞,在有些情况下我们并不希望如此,比如在Web服务器中当主线程为每个新来的链接创建一个子线程进行处理的时候,主线程并不希望因为调用pthread_join而阻塞(因为还要继续处理之后到来的链接),这时可以在子线程中加入代码
pthread_detach(pthread_self())
或者父线程调用
pthread_detach(thread_id)(非阻塞,可立即返回)
这将该子线程的状态设置为detached,则该线程运行结束后会自动释放所有资源。

线程安全:多个线程同时运行,访问临界资源,不会产生二义性

临界资源:在同一时刻,只有一个线程(执行流)可以访问


访问:在临界区当中,对临界区进行非原子操作
原子操作:操作是一步完成,所以要么完成,要么未完成

如何保证代码是线程安全的
  • 同步:保证程序对临界资源的合理访问
  • 互斥:保证同一时刻,只有一个执行流访问临界资源
同步

同步实现的事情:当有资源时,直接获取资源;没有资源时,线程进行等待,直到再次有资源时(这个资源可以是新生产的,也可以是其他线程使用结束后返还的),唤醒刚陷入等待状态的线程,让其获取这个资源

条件变量:
  本质=两个接口(等待接口+唤醒接口)+PCB等待队列
1.定义条件变量
   pthread_cond_t 条件变量类型
2.初始化条件变量

动态:需要调用销毁接口
    函数作用:
        用来初始化一个条件变量。
    函数原型为:
        int pthread_cond_init((pthread_cond_t* cond,pthread_condattr_t* attr));
    参数:
        cond:一个指向结构pthread_cond_t的指针,即传入条件变量的地址
        attr:条件变量的属性,一般设置为NULL,采用默认属性
        cond_attr:一个指向结构pthread_condattr_t的指针。
        pthread_condattr_t:条件变量的属性结构,和互斥锁一样我们可以用它来设置条件变量是进程内可用还是进程间可用
        默认值是PTHREAD_ PROCESS_PRIVATE,即此条件变量被同一进程内的各个线程使用;
        如果选择为PTHREAD_PROCESS_SHARED则为多个进程间各线程公用。注意初始化条件变量只有未被使用时才能重新初始化或被释放。
    返回值:
        函数成功返回0;任何其他返回值都表示错误。

静态:不需要调用销毁接口
    pthread_cond_t cond = PTHREAD_COND_INITIALIZER

3.等待接口

函数作用:
    将调用该等待接口的执行流放到PCB等待队列中,进行等待
函数原型:
    int pthread_cond_wait(pthread_cond_t* cond,pthread_mutex_t* mutex)
参数:
    cond:传入条件变量的地址
    mutex:传入互斥锁变量的地址
        
    3.1传入互斥锁的原因:
    保证在同一时刻只有一个执行流可以访问临界资源,避免该执行流受到其他执行流的影响,也完成了生产线程和消费线程之间的互斥
    3.2pthread_con_wait(...) 函数中,是如何使用互斥锁的
        ①将执行流放到PCB等待队列中
        ②对互斥锁进行解锁操作
        ③等待其他执行流通知PCB等待队列,从而被唤醒,移出PCB队列,进行争抢互斥锁
            抢到互斥锁:去访问临界资源,pthread_cond_wait返回
            
            未抢到互斥锁:卡在抢锁逻辑中继续等待下次枪锁的机会,卡在pthread_cond_wait接口当中
                    
    3.3pthread_cond_wait函数为什么需要将调用者放到PCB等待队列中才释放互斥锁

4.唤醒接口

函数作用:
        通知PCB等待队列中的执行流,进行访问临界资源
函数原型:
     int pthread_cond_signal(pthread_cond_t* cond)--->至少唤醒PCB等待队列中一个线程
        cond:传入条件变量的变量地址
        
    nt pthread_cond_broadcast(pthread_cond_t* cond)--->唤醒PCB等待队列中的全部线程
        cond:传入条件变量的变量地址

5.销毁接口

函数作用:
    释放动态初始化条件变量所占用的内存
函数原型:
    int pthread_cond_destroy(pthread_cond_t* cond)
参数:
    cond:传入条件变量的变量地址

代码示例:一个生产者和一个消费者

    1 //生产者和消费者                          
    2 
    3 #include<stdio.h>
    4 #include<pthread.h>
    5 #include<unistd.h>
    6 
    7 pthread_mutex_t lock;
    8 pthread_cond_t cond;
    9 int flag=0;//临界资源  0:无临界资源 1:有临界资源 
W> 10 void* fun_p(void* arg)
   11 {
   12   while(1)
   13   {
   14     pthread_mutex_lock(&lock);
   15     if(flag==0)
   16     {
   17       //阻塞等待
   18       //1.将该执行流放到PCB等待队列中去
   19       //2.解锁
   20       //3.等待被唤醒
   21       pthread_cond_wait(&cond,&lock);
   22     }
   23     printf("fun_p %d\n",flag);
   24     ++flag;
   25     //唤醒PCB队列中的消费者
   26     pthread_cond_signal(&cond);
   27     pthread_mutex_unlock(&lock);
   28   }
   29 }
   30 
W> 31 void* fun_s(void* arg)
   32 {
   33   while(1)
   34   {
   35     pthread_mutex_lock(&lock);
   36     if(flag==1)
   37     {
   38       pthread_cond_wait(&cond,&lock);
   39     }
   40     printf("fun_s %d\n",flag);
   41     --flag;
   42     pthread_cond_signal(&cond);
   43     pthread_mutex_unlock(&lock);
   44 
   45   }
   46 }
   47                                                                                                                   
   48 int main()
   49 {
   50   pthread_mutex_init(&lock,NULL);
   51   pthread_cond_init(&cond,NULL);
   52   pthread_t pid[2];
   53   int ret = pthread_create(&pid[0],NULL,fun_p,NULL);
   54   if(ret<0)
   55   {
   56     perror("pthread_create");
   57     return 0;
   58   }
   59   ret = pthread_create(&pid[1],NULL,fun_s,NULL);
   60   if(ret<0)
   61   {
   62     perror("pthread_create");
   63     return 0;
   64   }
   65  int i=0;
   66  for(i=0;i<2;++i)
   67  {
   68    pthread_join(pid[i],NULL); 
   69  }
   70   pthread_cond_destroy(&cond);
   71   pthread_mutex_destroy(&lock);
   72 }
打印
[1@localhost 4]$ ./ps_out 
fun_p 1
fun_s 0
fun_p 1
fun_s 0
fun_p 1
fun_s 0
fun_p 1
fun_s 0
fun_p 1
fun_s 0
fun_p 1
fun_s 0
fun_p 1
fun_s 0
fun_p 1
fun_s 0
fun_p 1
fun_s 0
fun_p 1
fun_s 0
fun_p 1
fun_s 0
^C
[1@localhost 4]$ 

代码示例:多个消费者和多个生产者

  1.在判断临界资源状态时,必须得使用循环进行持续判断,只有真正符合消费或者生产条件的执行流才可以去相应的消费或生产操作

  2.在唤醒的时候,应该是消费者去唤醒生产者,生产者去唤醒消费者,假如是消费者去唤醒消费者,生产者唤醒生产者,由于线程的抢占式访问,新唤醒的消费者或者生产者,根本不符合消费或者生产的条件,但它还是会抢占互斥锁,假如它抢到了,它还是啥也不做,会再次唤醒更新的消费者或生产者,这样的次数会很多,就会导致程序陷入一直抢锁但又解锁,然后再抢,再解的死循环中

    1 //多个生产者和多个消费者       
    2                  
    3 #include<stdio.h>
    4 #include<pthread.h>
    5 #include<unistd.h>
    6 #define NUM 2    
    7 pthread_mutex_t lock;
    8 pthread_cond_t p_cond;
    9 pthread_cond_t s_cond;
   10 int flag=0;//临界资源  0:无临界资源 1:有临界资源 
W> 11 void* fun_p(void* arg)
   12 {                
   13   while(1)       
   14   {              
   15     pthread_mutex_lock(&lock);
   16     // if(flag==0)
   17     while(flag==1)//循环判断,保证只有临界资源为可
   18                   //可消耗态才可以进行消费
   19     {            
   20       //阻塞等待 
   21       //1.将该执行流放到PCB等待队列中去
   22       //2.解锁
   23       //3.等待被唤醒
   24       pthread_cond_wait(&p_cond,&lock);
   25     }
   26     printf("fun_p %d\n",flag);
   27     ++flag;
   28     //唤醒PCB队列中的消费者
   29     pthread_cond_signal(&s_cond);
   30     pthread_mutex_unlock(&lock);
   31   }
   32 }
   33 
W> 34 void* fun_s(void* arg)
   35 {
   36   while(1)
   37   {
   38     pthread_mutex_lock(&lock);
   39     //if(flag==1)
   40     while(flag==0)
   41     {
   42       pthread_cond_wait(&s_cond,&lock);
   43     }
   44     printf("fun_s %d\n",flag);
   45     --flag;
   46     //生产者去唤醒消费者,
   47     //消费者去唤醒生产者                            
   48     pthread_cond_signal(&p_cond);
   49     pthread_mutex_unlock(&lock);
   50 
   51   }
   52 }
   53 
   54 int main()
   55 {
   56   pthread_mutex_init(&lock,NULL);
   57   pthread_cond_init(&p_cond,NULL);
   58   pthread_cond_init(&s_cond,NULL);
   59   pthread_t p_pid[NUM];
   60   pthread_t s_pid[NUM];
   61   int i=0;
   62   int ret=0;
   63   for(i=0;i<NUM;++i)
   64   {
   65     ret = pthread_create(&p_pid[i],NULL,fun_p,NULL);
   66   
   67     if(ret<0)
   68    {
   69       perror("pthread_create");
   70       return 0;
   71     }
   72   }
   73   for(i=0;i<NUM;++i)                                
   74   {
   75     ret = pthread_create(&s_pid[i],NULL,fun_s,NULL);
   76     if(ret<0)
   77    {
   78      perror("pthread_create");
   79      return 0;
   80     }
   81   }
   82  for(i=0;i<2;++i)
   83  {
   84    pthread_join(p_pid[i],NULL); 
   85    pthread_join(s_pid[i],NULL); 
   86  }
   87   pthread_cond_destroy(&p_cond);
   88   pthread_cond_destroy(&s_cond);
   89   pthread_mutex_destroy(&lock);
   90 }
打印:
[1@localhost 5]$ ./ps_out 
fun_p 0
fun_s 1
fun_p 0
fun_s 1
fun_p 0
fun_s 1
fun_p 0
fun_s 1
^C[1@localhost 5]$ 

生产者—消费者模型
123规则:1个场所(一种PCB等待队列,有几种角色就应该有几个等待队列)+2个角色(生产者和消费者)+3种关系(消费者和消费者互斥、生产者和生产者互斥、消费者和生产者同步加互斥)
生产者—消费者模型的优点

  可以解耦合:生产者和消费者通过队列进行交互

  支持忙闲不均:队列起到缓冲作用

  支持并发:消费者只关心队列中是否有资源可以消费,生产者只关心队列中是否有空的资源需要进行补充

如何实现生产者和消费者模型

1.队列,可以借助STL中的queue,队列属性:先进先出
2.线程安全的队列
  std:queue并不是线程安全的

    互斥:使用互斥锁

    同步:使用条件变量

3.两种角色的线程
  生产者线程:负责往队列中插入数据

  消费者线程:负责从队列中消费数据

互斥

互斥锁:用于保证互斥属性,底层是互斥量,互斥量的本质是一个计数器,只有0或者1两种情况

  0代表无法获取互斥锁,表示需要访问的临界资源不可以被访问

  1代表可以获取互斥锁,表示需要访问的临界资源可以被访问

加锁操作:对互斥量的计数器-1操作

解锁操作:对互斥量的计数器+1操作

互斥量计数器本身也是一个变量,拿着在进行+1或者-1时,是如何保证原子性的
情况一:    寄存器:0   <--------->    内存:1
                         xchgb    
情况二:    寄存器:0   <--------->    内存:0  (已经有一个执行流进行了加锁操作,所以值由1变为0)
                         xchgb

不论哪种情况,都会先将寄存器的值置为0
加锁:将寄存器中的值和内存中的值直接通过xchgb进行互换,而xchgb这种操作时原子性,直接一步完成

当交换完成之后,需要对寄存器中的值进行判断
    如果寄存器==0,则表示不能进行加锁,意味着当前加锁操作会被阻塞,从而不能访问临界资源
    如果寄存器==1,则表示可以加锁,对计数器进行-1操作后,会锁住资源,并加锁操作返回,从而去访问临界资源


xchgb不是交换函数,而是一个宏,根据不同的字长调用不同的汇编指令操作。

在头文件中是这样定义的:
#define xchg(ptr,v) ((__typeof__(*(ptr)))__xchg((unsigned long) \
                                         (v),(ptr),sizeof(*(ptr))))
 
static inline unsigned long __xchg(unsigned long x, 
                                  volatile void * ptr, int size)
{
    switch (size) {
        case 1:
            __asm__ __volatile__("xchgb %b0,%1"
                :"=q" (x)
                :"m" (*__xg(ptr)), "0" (x)
                :"memory");
            break;
        case 2:
            __asm__ __volatile__("xchgw %w0,%1"
                :"=r" (x)
                :"m" (*__xg(ptr)), "0" (x)
                :"memory");
            break;
        case 4:
            __asm__ __volatile__("xchgl %0,%1"
                :"=r" (x)
                :"m" (*__xg(ptr)), "0" (x)
                :"memory");
            break;
    }
    return x;
}

它只是将第二个参数x放入寄存器中与第一个指针参数所指的内容交换,返回所指内容原先的值。谈不上两数交换值。但是它可以“原子”的设置ptr所指内容并取出原值

互斥锁的使用流程

1.定义互斥锁

  pthread_mutex_t:互斥锁变量类型

2.初始化互斥锁

函数功能:
    互斥锁的初始化。
    动态初始化:(需搭配pthread_mutex_destory()函数予以销毁)
        头文件:
            #include <pthread.h>
        函数原型:
            int pthread_mutex_init(pthread_mutex_t* mutex,const pthread_mutexattr_t* attr)
            mutex:互斥锁变量,传参时传入互斥锁变量的地址
            attr:互斥锁的属性,一般采用默认属性,传值为NULL即可
    
    静态初始化:不需要进行销毁
        pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER

3.加锁

阻塞加锁方式:
    头文件
        #include <pthread.h>

    函数原型
        int pthread_mutex_lock(pthread_mutex_t *mutex);

        mutex:传入互斥锁变量的地址,来进行加锁操作

    返回值
        在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。
返回值种类,引用自说明文档:
    EINVAL
         The value specified by mutex does not refer to an initialized mutex object.
      互斥锁指定的值不引用已初始化的互斥锁对象
  EAGAIN
  The mutex could not be acquired because the maximum number of recursive locks for mutex has been exceeded.
  The pthread_mutex_lock() function may fail if:
      无法获取互斥锁,因为已超过互斥锁的递归锁的最大数目
  EDEADLK
  The current thread already owns the mutex.
      当前线程已经拥有互斥锁
    描述:
        如果计数器中的值==1,可以加锁,加锁操作之后,计算器中的值由1--->0
        如果计数器中的值==0,不可以加锁,加锁接口阻塞等待,执行流不会继续向下执行

非阻塞加锁:
    int pthread_mutex_trylock(pthread_muxtex_t*)
        如果互斥锁计数器的值==1,表示可以加锁,调用该接口,正常加锁
        如果互斥锁计数器的值==0,表示不可以加锁,调用该接口,报错返回,返回EBUSY(锁资源不可用)
        
带有超时 时间的加锁:    
    int pthread_mutex_timedlock(pthread_mutex_t* ,const struct timespec*)
        struct timespec(秒级别, 纳秒级别)
        当加锁时,互斥锁资源被其他执行流占用,在等待超过等待时长后,仍然没有获得互斥锁,则报错,返回ETIMEOUT

4.解锁

头文件
    #include<pthread.h>

函数原型
    int pthread_mutex_unlock(pthread_mutex_t* mutex)

函数功能:
    释放互斥锁

参数:
    mutex:需要解锁的锁变量对象。

5.销毁互斥锁
释放动态初始化的互斥锁变量所占用内存资源,否则会导致内存泄漏


头文件
  #include <pthread.h>
  
函数原型
int pthread_mutex_destroy(pthread_mutex_t *mutex);

函数功能:
    销毁互斥锁
    
参数
    mutex:要销毁互斥锁的指针

返回值
    成功后返回 0,否则返回错误码
代码示例:抢票程序
1.加锁在哪定义、初始化、销毁
    定义:C语言---全局变量
          C++-----类的成员变量
    初始化:在创建线程之前
    销毁:在线程退出之后
2.在哪里加锁、解锁
    加锁:在访问临界资源之前
    解锁:在所有可能退出线程的地方
    
    1 #include<stdio.h>               
    2 #include<unistd.h>
    3 #include<pthread.h>
    4 
    5 #define pthread_num 3
    6 pthread_mutex_t lock;
    7 int ticket=60;
    8 void* func(void* arg)
    9 {
   10   (void) arg;
   11   while(1)
   12   {
   13     pthread_mutex_lock(&lock);
   14     if(ticket>0)
   15     {
   16       //usleep(1000);
W> 17       printf("func pthread_t %p \t ticket num :%d\n",pthread_self(),ticket);
   18       --ticket;
   19     }
   20     else
   21     {
   22       pthread_mutex_unlock(&lock);
   23      //printf("sold out!!!\n");
   24       break;
   25     }
   26     pthread_mutex_unlock(&lock);
   27   }
   28 
W> 29 }
   30 int main()
   31 {
   32   pthread_mutex_init(&lock,NULL);
   33   pthread_t pid[pthread_num];
   34   int ret;
   35   int i=0;
   36   for(i=0;i<pthread_num;++i)
   37   {
   38     ret=pthread_create(&pid[i],NULL,func,NULL);
   39     if(ret<0)
   40     {
   41       perror("pthread_create");
   42       return 0;
   43     }
   44   }
   45   //sleep(1); 
   46   for(i=0;i<pthread_num;++i)                                                                                      
   47   {
   48   pthread_join(pid[i],NULL);
   49   }
   50   pthread_mutex_destroy(&lock);
   51   return 0;
   52 }
打印
[1@localhost 3]$ ./s_out 
func pthread_t 0x7fe5a77fd700 	 ticket num :60
func pthread_t 0x7fe5a77fd700 	 ticket num :59
func pthread_t 0x7fe5a77fd700 	 ticket num :58
func pthread_t 0x7fe5a77fd700 	 ticket num :57
func pthread_t 0x7fe5a77fd700 	 ticket num :56
func pthread_t 0x7fe5a77fd700 	 ticket num :55
func pthread_t 0x7fe5a77fd700 	 ticket num :54
func pthread_t 0x7fe5a77fd700 	 ticket num :53
func pthread_t 0x7fe5a77fd700 	 ticket num :52
func pthread_t 0x7fe5a77fd700 	 ticket num :51
func pthread_t 0x7fe5a77fd700 	 ticket num :50
func pthread_t 0x7fe5a77fd700 	 ticket num :49
func pthread_t 0x7fe5a77fd700 	 ticket num :48
func pthread_t 0x7fe5a77fd700 	 ticket num :47
func pthread_t 0x7fe5a77fd700 	 ticket num :46
func pthread_t 0x7fe5a77fd700 	 ticket num :45
func pthread_t 0x7fe5a77fd700 	 ticket num :44
func pthread_t 0x7fe5a77fd700 	 ticket num :43
func pthread_t 0x7fe5a77fd700 	 ticket num :42
func pthread_t 0x7fe5a77fd700 	 ticket num :41
func pthread_t 0x7fe5a77fd700 	 ticket num :40
func pthread_t 0x7fe5a77fd700 	 ticket num :39
func pthread_t 0x7fe5a77fd700 	 ticket num :38
func pthread_t 0x7fe5a77fd700 	 ticket num :37
func pthread_t 0x7fe5a77fd700 	 ticket num :36
func pthread_t 0x7fe5a77fd700 	 ticket num :35
func pthread_t 0x7fe5a77fd700 	 ticket num :34
func pthread_t 0x7fe5a77fd700 	 ticket num :33
func pthread_t 0x7fe5a77fd700 	 ticket num :32
func pthread_t 0x7fe5a77fd700 	 ticket num :31
func pthread_t 0x7fe5a77fd700 	 ticket num :30
func pthread_t 0x7fe5a77fd700 	 ticket num :29
func pthread_t 0x7fe5a67fb700 	 ticket num :28
func pthread_t 0x7fe5a67fb700 	 ticket num :27
func pthread_t 0x7fe5a67fb700 	 ticket num :26
func pthread_t 0x7fe5a67fb700 	 ticket num :25
func pthread_t 0x7fe5a67fb700 	 ticket num :24
func pthread_t 0x7fe5a67fb700 	 ticket num :23
func pthread_t 0x7fe5a67fb700 	 ticket num :22
func pthread_t 0x7fe5a67fb700 	 ticket num :21
func pthread_t 0x7fe5a67fb700 	 ticket num :20
func pthread_t 0x7fe5a67fb700 	 ticket num :19
func pthread_t 0x7fe5a67fb700 	 ticket num :18
func pthread_t 0x7fe5a67fb700 	 ticket num :17
func pthread_t 0x7fe5a67fb700 	 ticket num :16
func pthread_t 0x7fe5a67fb700 	 ticket num :15
func pthread_t 0x7fe5a67fb700 	 ticket num :14
func pthread_t 0x7fe5a67fb700 	 ticket num :13
func pthread_t 0x7fe5a67fb700 	 ticket num :12
func pthread_t 0x7fe5a67fb700 	 ticket num :11
func pthread_t 0x7fe5a67fb700 	 ticket num :10
func pthread_t 0x7fe5a67fb700 	 ticket num :9
func pthread_t 0x7fe5a67fb700 	 ticket num :8
func pthread_t 0x7fe5a67fb700 	 ticket num :7
func pthread_t 0x7fe5a67fb700 	 ticket num :6
func pthread_t 0x7fe5a67fb700 	 ticket num :5
func pthread_t 0x7fe5a67fb700 	 ticket num :4
func pthread_t 0x7fe5a67fb700 	 ticket num :3
func pthread_t 0x7fe5a67fb700 	 ticket num :2
func pthread_t 0x7fe5a67fb700 	 ticket num :1
[1@localhost 3]$ 

如果加锁之后没有进行解锁,会造成所有相要获取该互斥锁的执行流陷入阻塞等待

在这里插入图片描述

死锁

1.什么是死锁

  ①程序当中有一个执行陆没有释放资源,就会导致所有相要获取该互斥锁执行流陷入阻塞等待,这种情况就会导致死锁

  ②程序当中每个执行流都占有了一个互斥锁,但于此同时还想申请其他的互斥锁,这种情况也会导致死锁

2.死锁产生的4个必要条件

  ①互斥:每个锁同时只能被一个执行流占用

  ②请求和保持:当前执行流已经占用一个互斥锁,还想去申请其他新的互斥锁

  ③循环等待:多个执行流在请求锁资源时,形成一个闭环

  ④不可剥夺:只有当前占用互斥锁的执行流才可以释放该互斥锁

3.避免死锁的方法

  ①破坏产生死锁的必要条件

  ②加锁顺序一致(每个执行流依次去申请互斥锁1,互斥锁2…互斥锁n)

  ③在使用完互斥锁之后,进行释放

  ④一次性分配资源(给执行流分配资源时,一次性分配所有资源)

  

Posix信号量:完成线程间或者进程间的同步互斥
本质:资源计数器+PCB等待队列+等待和唤醒接口

  资源计数器:用于对临界资源进行计数,信号量通过资源计数器来进行判断,从而得知当前资源是否可用

    可用:获取资源,然后进行访问

    不可用:进行阻塞等待,直至被唤醒

操作接口

1.定义
sem_t sem;
2.初始化

头文件
    #include <semaphore.h> 
函数原型
    int sem_init(sem_t *sem, int pshared, unsigned int value); 
参数:
    sem:传入信号量的地址
    pshared:0表示线程之间共享,非零表示进程之间共享 
        当使用sem_init初始化信号量为进程之间的时候,会在内核创建一块共享内存,来保持信号量的数据结构,其中资源计数器,PCB等待队列都在共享内存中维护。
        所以我们调用唤醒或者等待接口时,就通过操作共享内存实现不同进程之间的通信,进而实现不同进程之间的同步和互斥
    value:实际资源数量,用于初始化信号量中资源计数器
        

3.等待

函数功能:
    等待信号量,会将信号量的值减1 
函数原型:
    int sem_wait(sem_t *sem)--->阻塞方式等待
    int sem_trywait(sem_t* sem)--->非阻塞方式等待
    int sem_timeedwait(sem_t* sem,const struct timespec* timeval)--->带有超时 时间的等待
    
    如果调用等待接口进行获取信号量,会对资源计数器-1操作

4.唤醒

函数功能:
    发布信号量,表示资源使用完毕,需要归还资源或者生产者重新生产一个资源,对信号量中资源计数器,进行+1操作,唤醒PCB等待队列当中的PCB
函数原型
    int sem_post(sem_t *sem);

5.销毁

函数功能
    销毁信号量
函数原型
    int sem_destroy(sem_t *sem);
如何保证同步和互斥

同步:


  ①初始化时,根据资源的数量初始化Posix信号量中的资源计数器

  ②每次获取临界资源前,都需要调用等待接口获取信号量


    等待接口没有返回,表示不能获取信号量(此时资源信号量的值<=0)


    等待接口有返回,表示已经获取信号量,可以正常访问临界资源


  ③访问完临界资源后,调用唤醒接口,对信号量进行+1操作


  ④销毁信号量,释放内存


互斥:


  初始化时,必须把信号量中资源计数器的值初始化为1,保证当前最多只能有1个线程有访问临时资源的权力

读写锁

适用场景:少量写,大量读,不同用于读的线程,可以获取读模式下的读写锁并行运行
读写锁的三种状态

读模式下的加锁状态

写模式下的加锁状态

不加锁

加锁规则
1.读写锁的写状态一次只能被一个执行流所占用
2.读写锁的读状态一次可以被多个执行流所占用,读写锁内部有个引用计数,用以记录当前有多少个执行流读取了读写锁的读状态,增加+1,减少-1,只有引用计数==0时,才表示读写锁的读状态完全解锁
操作接口

定义:

pthread_rwlock_t


初始化:

头文件
    #include <pthread.h>
函数功能
    初始化pthread_rwlock_t类型的变量
函数原型
    int pthread_rwlock_init(pthread_rwlock_t* rwlock, const pthread_rwlockattr_t* attr);
参数
    rwlock:一个指向读写锁的指针,
    attr:一个读写锁属性对象的指针,如果将
NULL 传递给它,则使用默认属性来初始化一个读写锁。
返回值
    成功返回0,不成功返回非零的错误码

加锁:

头文件
    #include <pthread.h>
函数功能
    给pthread_rwlock_t类型的变量进行加锁操作
函数原型   
pthraed_rwlock_rdlock(pthread_rwlock_t* rwlock)--->以读模式进行加锁
pthread_rwlock_wrlock(pthread_rwlock_t* rwlock)--->以写模式进行加锁
参数  
    rwlock:一个指向读写锁的指针
返回值
    成功返回0,不成功返回非零的错误码

解锁:

头文件
    #include <pthread.h>
函数功能
    给pthread_rwlock_t类型的变量进行解锁操作
函数原型  
    pthread_rwlock_unlock(pthread_rwlock_t* rwlock)
参数 
    rwlock:一个指向读写锁的指针
返回值
    成功返回0,不成功返回非零的错误码  

销毁

头文件
    #include <pthread.h>
函数功能
    释放pthread_rwlock_t类型的变量的内存空间
函数原型  
    pthread_rwlock_destroy(pthread_rwlock_t* rwlock)
参数 
    rwlock:一个指向读写锁的指针
返回值
    成功返回0,不成功返回非零的错误码 

线程池=一个线程安全队列+一些线程

线程池:一种线程使用模式,线程过多时,使用原来的使用线程时创建,使用完成时销毁,就会导致CPU大量重复了这两步骤,有点耗损CPU,所以直接创建一些线程,把饶命放到一个池(容器)中,有任务时去处理,完成之后这些线程不会被销毁,而是继续在线程池中等待下一个任务,这样就不会反复的去创建、销毁线程了。

如何让这些同一的线程去执行不同的任务

1.switch case,只适合处理少量任务


2.向线程池传入数据时,还传入相应的处理函数地址,这样线程池中的线程只需要调用传入的函数去处理相应的数据即可

线程池如何正确地退出
直接退出:如果线程池的线程队列中还有数据没处理而恰恰这些数据必须马上处理,此时退出,就会造成损失。—>不正确
1.加互斥锁
2.调用pthread_cond_wait
3.
4.
设计模式:大佬根据一些常见的问题或常见常见,给出的解决方案
设计模式的优点:代码复用程度高、代码可靠,代码框架稳定
设计模式分类

创建型模式—>单例模式


结构型模式—>适配器模式


行为型模式—>观察者模式

单例模式

1.特点:全局提供唯一一个类的实例,具有全局变量的特点

2.使用场景:内存池,数据池

3.基础要点

  ①全局只有一个实例—>staic+禁止构造+禁止拷贝构造+禁止赋值拷贝

  ②线程安全

  ③调用者通过类的函数来获取实例

4.具体实现:饿汉模式、懒汉模式

饿汉模式
    程序启动时就进行初始化,资源在程序初始化时就全部加载完毕
        优点:程序运行速度快,流畅
        缺点:程序初始化时,耗时长

template <typename T>
class e_han
{
    private:
            static T data;
            //禁止构造、拷贝构造、赋值拷贝
    public:
            static T* get_init()
            {
            if(data==NULL)
                {
                    data=new T();
                }
            return &data;
            }
}


懒汉模式
    资源在使用时才进行实例化,单例类在使用时才进行实例化
        优点:程序初始化的时候快
        缺点:运行时没有饿汉模式流畅
              在程序运行时实例化时,可能会发生线程安全的问题

template <typename T>
pthread_mutex_t lock;
class lan_han
{
    private:
        lan_han();
        volatile static lan_han* p;
        //设置 volatile 关键字, 防止被编译器优化
    public:
        static lan_han* getinit()
        {
            if(p==NULL)//双重if,降低锁冲突概率,从而提高代码效率
            {
                pthread_mutex_lock(&lock);
                //使用互斥锁,保证多线程情况下,值调用一次new
                if(p==NULL)
                {
                    p=new lan_han();
                }
                pthread_mutex_unlock(&lock);
            }
            return p;
        }
}
volatile:确保指令不会因编译器的优化而省略,且要求每次从它所在的内存读取数据,并且读取的数据立刻被保存

volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问



声明时语法:int volatile vInt;

当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。

  1 #include<stdio.h>  
  2 #include<signal.h>   
  3                      
  4                      
  5 int count=1;         
  6 void func(int signo)  
  7 {                    
  8   printf("signo:%d \t count:%d \n",signo,count);  
  9   //printf("signo:%d\n",signo);  
 10   count=0;           
 11 }                    
 12                      
 13 int main()           
 14 {                    
 15   signal(2,func);    
 16                      
 17   while(count)       
 18   {                  
 19    // printf("main\n");  
 20   }                  
 21   printf("no main\n");                                
 22   return 0;          
 23                      
 24                      
 25 } 
 gcc -O1 volatile.c -o v1_out
 gcc -O2 volatile.c -o v2_out
只要使用了优化编译就会出问题
打印
[1@localhost 1多线程]$ ./v2_out 
^Csigno:2 	 count:1 
^Csigno:2 	 count:0 
^Csigno:2 	 count:0 
^Csigno:2 	 count:0 
^Csigno:2 	 count:0 
^Csigno:2 	 count:0 
^Csigno:2 	 count:0 
^Csigno:2 	 count:0 
^Csigno:2 	 count:0 
^Csigno:2 	 count:0 
^Csigno:2 	 count:0 
^Z
[2]+  已停止               ./v2_out
所以将int count=1;  ------->volatile int count=1;

  


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值