【Linux】多线程概念总结、pthread章节要点

线程和进程之间的关系

1.线程和进程的关系就是流水线和工厂之间的关系。

-线程依附于进程才能存在,如果没有进程线程就不能单独存在

-多线程是为了提高整个程序的运行效率

2.在我们写进程方面代码的时候是否存在线程呢:

是有的,从main函数开始运行的线程被称之为“主线程”。

3.之前的博客我们所理解的进程在内核当中就是一个task_struct结构体,该结构体当中的成员变量pid被我们称之为进程ID;

但其实操作系统当中没有线程的概念,程序员所说的创建线程,本质上在Linux操作系统当中就是创建轻量级进程(lwp),所以轻量级进程等价于线程(行业中约定俗成的就称之为线程)。

pid & tgid

1.pid本质上就是轻量级进程ID,换句话说pid其实就是线程ID

2.在task_struct结构体当中:

1)pid_t pid; //就是轻量级进程id,也被称之为线程id,不同的线程具有不同的pid;

2)pid_t tgid; //轻量级进程组id,也被称之为进程id,一个进程当中的线程拥有相同的tgid。

3.但是为什么在介绍进程概念的时候说pid就是进程id?

因为主线程的pid和tgid相等,线程组=>进程,进程概念里面的进程id也就是线程组id:tgid

线程的共享与独有

共享:

文件描述符表、用户ID、用户组ID、信号处理方式、当前进程的工作目录

独有:

在进程虚拟地址空间的共享区当中:调用栈、寄存器、线程ID、errno、信号屏蔽字、调用优先级

线程的优缺点

优点:

1)多线程的程序,拥有多个执行流,合理使用(控制线程的访问时序问题),可以提高程序的运行效率。

2)多线程程序的线程切换比多进程程序快,付出的代价小(有些可以共享的数据(全局变量)就能在线程切换的时候不进行切换)。

3)可以充分发挥多核CPU并行的优势。

4)计算密集型的程序,可以进行拆分,让不同的线程执行计算不一样的事情。

5)I/O密集型的程序,可以进行拆分,让不同的线程执行不同的I/O操作,可以不用串行运行,提高程序运行效率。

缺点:

1)编写代码的难度更高。

2)代码的鲁棒性(稳定性)要求更高:一个线程崩溃,整个进程就崩溃。

3)线程数量并不是越多越好:随着线程的数量增多,进程的运行效率符合正态分布,随着线程数量增多,会导致线程切换的次数增多。

4)缺乏访问控制,可能会导致程序产生二义性结果。

线程创建

接口:

int pthread_create(pthread_t *thread , const pthread *attr , void *(*start_routine)(void *) , void *arg);
头文件:
pthread.h
参数:
thread:获取线程标识符(地址),本质上就是线程独有空间的首地址
attr:线程的属性信息,一般填写NULL,采用默认的线程属性
start_routine:函数指针,线程执行的入口函数(线程执行起来的时候,从该函数开始运行,切记:不是从main函数开始运行)
arg:给线程入口函数传递参数
返回值:
成功返回0,失败返回值<0

代码模拟:

#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
void* mythread_start(void* arg){
   while(1){
     printf("i am my_thread_start\n");
     sleep(1);
   }
   return NULL;                                                  
 }
 int main(){
   pthread_t tid;
   int ret=pthread_create(&tid,NULL,mythread_start,NULL);
   if(ret<0){
     perror("pthread_create\n");
     return 0;
   }
   printf("success...\n");
   return 0;

运行结果:

说明创建成功了,但是为什么工作线程没有继续运行呢

是因为主线程输出success之后就推出了,工作线程自然无法工作。

只要把主线程写进循环不让他那么早的退出,就能够观察到工作线程的工作状况

    1 #include<stdio.h>
    2 #include<pthread.h>
    3 #include<unistd.h>
    4 void* mythread_start(void* arg){
    5   while(1){
    6     printf("i am my_thread_start\n");
    7     sleep(1);
    8   }
    9   return NULL;
   10 }
   11 int main(){
   12   pthread_t tid;
   13   int ret=pthread_create(&tid,NULL,mythread_start,NULL);
   14   if(ret<0){
   15     perror("pthread_create\n");
   16     return 0;
   17   }
   18   while(1){
   19     printf("success...\n");
   20     sleep(1);                                                   
   21   }
   22   return 0;
   23 }

运行结果:

堆栈分析:

结论:

1)当创建完毕线程之后,主线程和工作线程都是独立被调度的。引申含义是:没有办法确定到底是先调用主线程还是工作线程,由操作系统调度决定的。

2)如果创建工作线程完毕之后,但是,进程退出了,有可能看不到工作线程运行的结果。原因就是:没有了进程,就不肯拥有更改创建出来的工作线程。

3)如果想看到工作线程的运行结果,则先保证进程不退出。如果想让工作线程不退出,则线程不能执行线程入口函数完毕。

4)pstack查看线程信息。

是否可以创建多个线程(多线程)?

代码模拟:

    1 #include<stdio.h>
    2 #include<pthread.h>
    3 #include<unistd.h>
    4 void* mythread_start(void* arg){
    5   while(1){
    6     printf("i am my_thread_start\n");
    7     sleep(1);
    8   }
    9   return NULL;
   10 }
   11 int main(){
   12   pthread_t tid;
   13   int ret=pthread_create(&tid,NULL,mythread_start,NULL);
   14   if(ret<0){
   15     perror("pthread_create\n");
   16     return 0;
   17   }
   18   ret=pthread_create(&tid,NULL,mythread_start,NULL);            
   19   if(ret<0){
   20     perror("pthread_create\n");
   21     return 0;
   22    }
   23   while(1){
   24     printf("success...\n");
   25     sleep(1);
   26   }

运行结果:

正儿八经的确实创建出来了两个工作线程。,这两个线程执行的入口函数是同一个。

这两个线程看到的mythread_start栈帧是不是同一个呢?

答案肯定是不是的,利用工厂与流水线的关系也可以看出,不同的流水线完全可以执行不同的工作,如果所有流水线都是同一个的镜像,这样就失去了多线程的意义。

咱们再用for循环来批量生产工作线程

代码模拟:

  1 #include<stdio.h>
  2 #include<pthread.h>
  3 #include<unistd.h>
  4 void* mythread_start(void* arg){
  5   int* i =(int*)arg ;
  6   while(1){
  7     printf("i am workthread... %d\n",*i);
  8     sleep(1);
  9   }
 10   return NULL;
 11 }
 12 int main(){
 13   int i;
 14   for(i=0;i<4;i++){
 15     pthread_t tid;
 16     int ret=pthread_create(&tid,NULL,mythread_start,&i);
 17     if(ret<0){
 18     perror("pthread_create\n");
 19     return 0;
 20     }
 21   }
 22   while(1){
 23     printf("success...\n");
 24     sleep(1);
 25   }
 26   return 0;
 27 }

运行结果:

很乱啊,为什么会出现这种情况;

换句话说为什么再之后的工作线程当中打印的i都是4呢。

因为函数中传递的入口空间一直都是i的地址,而i只是主函数中的一个循环变量,打印的始终都是i这个临时变量的地址空间,当i变成4之后,四个工作线程打印的自然都是4了。

这样运用循环来批量制造工作线程有风险码?

当然有,表现是多个线程在访问的i的空间是非法访问,因为i是临时变量,除了for循环的作用域之后就被销毁了。

结论:线程入口函数的参数不要传递临时变量,临时变量出了作用域之后就会被销毁。有可能线程在非法访问空间。

如何避免4444这种情况:

1)各个线程直接打印自己的线程标识符pthread_self;

2)给线程分别传递不同的堆上空间。

总结:

1)线程入口函数传递参数的时候,传递堆栈空间;

2)释放堆区空间的时候,让线程自己进行释放。

线程终止

接口:

void pthread_exit(void *retval);
参数:
retval:线程退出时,传递给等待线程的退出信息。
作用:
谁调用就由谁退出
int pthread_cancel(pthread_t thread);
参数:
thread:被终止的线程的标识符
作用:退出某个线程

线程等待

线程被创建出来的默认属性是joinable属性,退出的时候,依赖其他线程来回收资源(主要是退出线程使用到的共享区当中的空间)。

接口:

int pthread_join(pthread_t thread , void **retval);
参数:
thread:线程的标识符
retval:退出线程的退出信息
第一种:线程入口函数代码执行完毕,线程退出的,就是入口函数的返回值;
第二种:pthread_exit退出的,就是pthread_exit的参数;
第三组:pthread_cancel退出的,就是一个宏:OTHREAD_CANCELED。

线程分离

设置线程的分离属性,一旦线程设置了分离属性,则线程退出的时候,不需要任何人回收资源,操作系统可以自行回收。

接口:

int pthread_detach(pthread_t thread);
参数:
thread:设置线程分离的线程标识符。

线程互斥

互斥要做的事情:控制线程的访问时序。但多线程能够同时访问到临界资源的时候,有可能会导致线程执行的结果产生二义性。

而互斥就是要保证多个线程在访问同一个临界资源,执行临界区代码的时候(非原子性操作,线程可以被打断)控制访问时序。

让一个线程独占临界资源执行完,再让另一个独占执行。

临界资源:能够被多个线程访问到的资源,称之为临界资源。

临界区代码:访问临界资源的代码。

互斥锁

互斥锁的原理:

互斥锁的本质就是0/1计数器,计数器的取值只能是0或者1;

计数器的值为1的时候:表示当前线程可以获取到互斥锁,而去访问临界资源;

计数器的值为0的时候:表示当前线程不可以获得到互斥锁,从而不能访问临界资源。

加锁:加锁成功的时候,会将计数器的值从1改成0;

解锁:解锁成功的时候,会将计数器的值从0改成1。

多个线程要执行临界区当中的代码时候为了保证互斥,都是需要先进行加锁保护的

极端情况下,多个线程同时进行加锁操作,只有一个线程可以枷锁成功,其他线程加锁失败。

为什么计数器当中的值从0->1,或者1->0是原子性的呢?

操作不可被分割;要么开始要么没有开始不存在中间状态。

直接使用寄存器当中的值和计数器内存的值交换,而交换时一条汇编指令就可以完成的。

加锁的时候:寄存器当中的值设置为(0),寄存器与计数器当中的值进行交换。

第一种情况:计数器的值为1,说明锁空闲,没有被线程加锁

第二种情况:计数器的值为0,说明锁忙碌,被其他的线程加锁拿走

解锁的时候:寄存器当中的值设置为(1),寄存器与计数器当中的值进行交换。

初始化接口:

初始化:pthread_mutex_t mutex;
动态初始化:
int pthread_mutex_init ( pthread_mutex_t *restrict mutex , const pthread_mutexattr_t *restrict arrt ) ;
参数:
pthread_mutex_t:互斥锁类型(结构体)
attr:互斥锁的属性,一般直接传递NULL
静态初始化: pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

加锁接口:

int pthread_mutex_lock ( pthread_mutex_t *mutex ); --阻塞加锁
int pthread_mutex_trylock ( pthread_mutex_t *mutex ); --非阻塞加锁,搭配循环使用判断是否加锁成功
int pthread_mutex_timelock ( pthread_mutex_t *restrict mutex , const struct timespec *restrict abs_timeout ); --带有超时时间的加锁接口

struct timespec {
tim_t tv_sec; // secods 5
long tv_nsec; // and nanoseconds
};

解锁接口:

int pthread_mutex_unlock ( pthread_mutex_t *mutex );

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

互斥:可以保证多个线程对于临界资源进行访问的互斥属性。

如果只保证互斥,有可能出现“线程饥饿”问题。

线程同步:在保证互斥的前提下,保证多个线程对临界资源访问的合理性。

销毁接口:

int pthread_mutex_distory ( pthread_mutex *mutex );

代码模拟:

场景实现:大意为两个工作线程同时抢票,一共一百张,若没有加入互斥锁,俩线程可能出现二义性,造成线程不安全的情况,而加入互斥锁之后就不存在二义性问题

    1 #include<stdio.h>
    2 #include<pthread.h>
    3 #include<unistd.h>
    4 int g_tickets=100;
    5 pthread_mutex_t g_lock;
    6 void* mythread_start(void* arg){
    7   while(1){
    8     pthread_mutex_lock(&g_lock);
    9     if(g_tickets<=0){
   10       pthread_mutex_unlock(&g_lock);
   11       break;
   12     }
   13     printf("i am %p,i hace tickets %d\n",pthread_self(),g_tickets);
   14     g_tickets--;
   15     pthread_mutex_unlock(&g_lock);
   16   }
   17   return NULL;
   18 }
   19 int main(){
   20   pthread_mutex_init(&g_lock,NULL);
   21   pthread_t tid[2];
   22   int i;
   23   for(i=0;i<2;i++){
   24     int ret=pthread_create(&tid[i],NULL,mythread_start,NULL);
   25     if(ret<0){
   26         perror("pthread_create\n");
   27         return 0;
   28     }
   29   }
   30   for(i=0;i<2;i++)
   31   {
   32     pthread_join(tid[i],NULL);
   33   }
   34   pthread_mutex_destroy(&g_lock);
   35   return 0;
   36 } 

运行结果:

线程同步

使用原理:线程在加锁之后,判断下临界资源是否可用

如果可用,则直接访问临界资源

如果不可用,则调用等待接口,让该线程进行等待

条件变量的原理:

本质上是PCB等待队列(存放在等待的线程PCB)

初始化接口:

初始化:pthread_cond_signal
动态初始化:
int pthread_cond_init ( pthread_cond_t *restrict cond , const pthread_condattr_t *restrict attr );
参数:
pthraed_cond_t:条件变量的类型
pthread_condattr_t:NULL,采用默认属性
静态初始化:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

销毁接口:

int pthread_cond_destroy ( pthread_cond_t *cond );

等待接口:

int pthread_cond_wait ( pthread_cond_t *restrict cond , pthread_mudex_t *restrict mutex);
作用:
谁调用将谁放倒PCB等待队列
参数:
cond:条件变量
mutex:互斥锁
int pthread_cond_timedwait ( pthread_cond_t *retrict cond , pthread_mutex_t *restrict mutex , const struct timespec *restrict abstime );

唤醒接口

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

条件变量的夺命追问

1)条件变量的扽得改接口第二个参数为什么会有互斥锁?

在函数内部需要解锁操作,如果不解锁就阻塞等待,则其他线程一定拿不到锁。

2)pthread_cond_wait的内部是针对互斥锁做了什么操作?先释放互斥锁还是先将线程放入到PCB等待队列。

解锁操作;先放入PCB等待队列。

3)线程被唤醒之后会执行什么代码,需要再获取互斥锁么?

从PCB等待队列当中移出;抢锁。

死锁

thread apply all bt(back trace)每一个线程都在查看调用栈

死锁使用的两种场景:

1) 不解锁

2) 吃着碗里的看着锅里的

锁1加锁于线程1、锁2加锁于线程2。此时线程1继续请求加锁锁2、亦或者线程2继续请求加锁锁1,都会造成死锁。(挖墙脚是一种不礼貌、不道德的行为)

死锁模拟场景

不解锁情况模拟:

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
void* mythread_strat(void* arg)
{
  int count=10;
  while(1){
    pthread_mutex_lock(&mutex);
    if(count<0){
      break;
    }
    count--;
    pthread_mutex_unlock(&mutex);
  }
  return NULL;
}
 
int main()
{
  for(int i=0;i<2;i++)
    {
       pthread_t tid;
       int ret=pthread_create(&tid,NULL,mythread_strat,NULL);
       if (ret<0){
       perror("pthreat_create");
       return 0;
       }
    }
  pthread_mutex_t mutex;
  pthread_mutex_init(&mutex,NULL);
  pthread_mutex_lock(&mutex);
  pthread_mutex_lock(&mutex);
  printf("mutex...\n");
  while(1)
  {
    sleep(1);
  }
  return 0;
}

结果运行情况:

mutex...

结果表示死锁模拟成功。

2)吃着碗里看着锅里的情况:

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
pthread_mutex_t mutex1;
pthread_mutex_t mutex2;
 
void* mythread_stratA(void* arg){
    pthread_mutex_lock(&mutex1);
    sleep(1); //让线程B将2锁拿走
    pthread_mutex_lock(&mutex2);
 
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
}
 
void* mythread_stratB(void* arg){
    pthread_mutex_lock(&mutex2);
    sleep(1); //让线程A将1锁拿走
    pthread_mutex_lock(&mutex1);
 
 
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);
}
 
int main(){
    pthread_mutex_init(&mutex1, NULL);
    pthread_mutex_init(&mutex2, NULL);
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, mythread_stratA, NULL);
    if(ret < 0){
        perror("pthread_create");
        return 0;
    }
 
    ret = pthread_create(&tid, NULL, mythread_stratB, NULL);
    if(ret < 0){
        perror("pthread_create");
        return 0;
    }
 
    while(1){
        sleep(1);
    }
    pthread_mutex_destroy(&mutex1);
    pthread_mutex_destroy(&mutex2);
    return 0;
}

死锁的gdb分析:

进入gdb调试

(gdb)t 3 //切换到3号线程

(gdb)bt

(gdb)f 3 //查看请求的锁

(gdb)p mutex2 //查看2锁

2锁信息:__owner=23778 //表示本锁被线程23778调走

若线程3想要请求2锁,必须解决23778线程。

死锁的必要条件:

1) 不可剥夺:线程获取到互斥锁之后,除了自己释放,其他线程不能进行释放。

2) 循环等待:线程A拿着锁1,请求锁2,同时线程B拿着锁2,请求锁1

3) 互斥条件:一个互斥锁,在同一时间内只能被一个线程所拥有

4) 请求与保持:吃着碗里的看着锅里的

四个条件当中不可剥夺和互斥条件时互斥锁的技术属性,程序员无法通过更改代码使其改变;

程序员可以通过改变代码的手段破坏循环等待或者请求与保持条件。

代码怎么预防死锁:

1) 破坏必要条件:循环等待/请求与保持

2) 枷锁顺序一致:都先加锁1、再加锁2

3)避免锁没有被释放:在所有可能线程退出的地方进行解锁

4)资源一次性分配:多个资源在代码当中有可能每一个资源都需要使用不同的锁进行保护

123规则

1个线程安全的队列(同步、互斥)

2种角色的线程(生产者、消费者)

3个规则(生产者与生产者互斥、消费者与消费者互斥、生产者与消费者互斥+同步)

1) 生产者之关心生产,关心队列是否有空闲空间

2)消费者只关心消费,关心队列是否有数据存在

学习123规则的意义:

队列:起到了生产者与消费者之间的换种作用

生产者不会因为没人消费而发愁,只需要将生产的数据放进队列当中

消费者不用因为生产者生产了大量的数据而发愁,只需要正常关注正在处理的数据就行。

它好我也好~~

优点:

^生产者与消费者解耦

^忙闲不均

^支持高并发

生产者、消费者以及安全队列代码模拟实现:

要用到的主要函数

线程安全的队列:

队列:std::queue

线程安全:

同步:pthread_mutex_t

互斥:pthread_cond_t

两种角色(生产消费)的线程:

pthread_create

代码模拟:

queuesafe.hpp:

#pragma once
 
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <queue>
 
/*
 * 线程安全的队列
 * */
class SafeQueue{
    public:
        SafeQueue(){
            pthread_mutex_init(&que_lock_, NULL);
            pthread_cond_init(&cons_cond_, NULL);
            pthread_cond_init(&prod_cond_, NULL);
            capacity_ = 1; //假设队列当中只能放一个元素
        }
 
        ~SafeQueue(){
            pthread_mutex_destroy(&que_lock_);
            pthread_cond_destroy(&cons_cond_);
            pthread_cond_destroy(&prod_cond_);
        }
 
        /*
         * 插入接口-生产者调用的
         * */
        void Push(int data){
            pthread_mutex_lock(&que_lock_);
            while(que_.size() >= capacity_){
                pthread_cond_wait(&prod_cond_, &que_lock_);
            }
 
            que_.push(data);
            printf("i am product, i product %d\n", data);
            pthread_mutex_unlock(&que_lock_);
 
            /*通知消费者进行消费*/
            pthread_cond_signal(&cons_cond_);
        }
 
        /*
         * 获取元素的接口-消费者调用的
         * */
        int Pop(){
            pthread_mutex_lock(&que_lock_);
            while(que_.empty()){
                pthread_cond_wait(&cons_cond_, &que_lock_);
            }
            int tmp = que_.front();
            que_.pop();
            printf("i am consume, i consume %d\n", tmp);
            pthread_mutex_unlock(&que_lock_);
 
            /*通知生产者进行生产*/
            pthread_cond_signal(&prod_cond_);
            return tmp;
        }
    private:
        /* stl当中的queue是线程不安全的, 所以需要进行保护 */
        std::queue<int> que_;
        /* 人为约定队列的大小 */
        size_t capacity_;
        pthread_mutex_t que_lock_;
        /*消费者的条件变量*/
        pthread_cond_t cons_cond_;
        /*生产者的条件变量*/
        pthread_cond_t prod_cond_;
 
};

cons_prod.cpp:

#include "queuesafe.hpp"
 
#define THREADCOUNT 1
 
void* cons_start(void* arg){
    SafeQueue* sq = (SafeQueue*)arg;
 
    while(1){
        int ret = sq->Pop();
        //printf("i am consume, i consume %d\n", ret);
        if(ret == 999){
            break;
        }
    }
    return NULL;
}
 
void* prod_start(void* arg){
    /*
     * 获取线程安全的队列
     * */
    SafeQueue* sq = (SafeQueue*)arg;
 
    int data = 0;
    while(1){
        if(data >= 1000){
            break;
        }
        sq->Push(data);
        //printf("i am product, i product %d\n", data);
        data++;
    }
    return NULL;
}
 
int main(){
    SafeQueue* sq = new SafeQueue();
    if(sq == NULL){
        perror("new");
        return 0;
    }
 
    pthread_t cons[THREADCOUNT], prod[THREADCOUNT];
    for(int i = 0; i < THREADCOUNT; i++){
        int ret = pthread_create(&cons[i], NULL, cons_start, (void*)sq);
        if(ret < 0){
            perror("pthread_create");
            return 0;
        }
 
        ret = pthread_create(&prod[i], NULL, prod_start, (void*)sq);
        if(ret < 0){
            perror("pthread_create");
            return 0;
        }
    }
 
    /*主线程进行线程等待 - 阻塞等待*/
    for(int i = 0; i < THREADCOUNT; i++){
        pthread_join(cons[i], NULL);
        pthread_join(prod[i], NULL);
    }
    return 0;
}

代码分析:

信号量

信号量原理:

资源计数器(描述资源的可用情况)+PCB等待队列(当资源不可用的时候,将线程放到PCB等待队列进行一个等待)

资源计数器:执行流获取信号量

获取成功信号量计数器进行-1操作;获取失败执行流放入到PCB等待队列。

信号量的接口:

初始化:int sem_init(sem_t *sem,int pshared,unsigned int value);
sem:信号量,sem_t是信号量的类型
pshared:该信号量是用于进程之间还是线程之间(0:用于线程,全局变量;非0:用于进程)
value:资源的个数,初始化信号量计数器的
等待:int sem_wait(sem_t *sem);
1) 对资源计数器进行-1操作
2) 判断资源计数器的值是否小于0。是则堵塞等待,将执行流放入PCB等待队列;不是则接口返回
释放:ubt sem_post(sem_t *sem);
1)对资源计数器进行+1操作
2)判断资源计数器的值是否小于等于0。是则通知OCB等待队列;否则不用通知
销毁:int sem_destroy(sem_t *sem);

代码模拟:

#include <stdio.h>
#include <semaphore.h>   //信号量的头文件
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
sem_t sem; //拥有计数器+PCB等待队列
 
void sigcallback(int sig){
    sem_post(&sem);
}
 
int main(){
    signal(2, sigcallback);
    /*
     * 为了验证 sem 的接口
     *    sem_init
     *    sem_wait
     *    sem_post
     *    sem_destroy
     * */
    sem_init(&sem, 0, 1); //信号量初始维护的资源个数为0
 
    sem_wait(&sem); //第一次获取信号量, 因为资源个数为1, 所以可以获取到
 
    printf("The code has to go here\n");
 
    sem_wait(&sem); //阻塞在这, 因为资源计数器的值为0, 说明资源不可用
 
    printf("The code would be damned if it went this far\n");
    return 0;
}

运行结果:

互斥代码模拟:

#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>
#include <pthread.h>
 
int g_tickets = 100;
sem_t g_lock;
 
void* thread_startA(void* arg){
    pthread_detach(pthread_self());
    while(1){
        sem_wait(&g_lock); //“加锁操作”
        if(g_tickets <= 0){
            sem_post(&g_lock); //"解锁操作"
            break;
        }
        printf("i am thread_startA %p, i have %d ticket\n", pthread_self(), g_tickets);
        g_tickets--;
        sem_post(&g_lock); //"解锁操作"
        sleep(1);
    }
}
 
void* thread_startB(void* arg){
    pthread_detach(pthread_self());
    while(1){
        sem_wait(&g_lock); //“加锁操作”
        if(g_tickets <= 0){
            sem_post(&g_lock); //"解锁操作"
            break;
        }
        printf("i am thread_startB %p, i have %d ticket\n", pthread_self(), g_tickets);
        g_tickets--;
        sem_post(&g_lock); //"解锁操作"
        sleep(1);
    }
}
 
int main(){
    sem_init(&g_lock, 0, 1);
 
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, thread_startA, NULL);
    if(ret < 0){
        perror("pthread_create");
        return 0;
    }
    ret = pthread_create(&tid, NULL, thread_startB, NULL);
    if(ret < 0){
        perror("pthread_create");
        return 0;
    }
 
    while(1){
        sleep(1);
    }
    sem_destroy(&g_lock);
    return 0;
}

线程池

线程池的运用场景

多线程程序是为了解决程序运行效率问题

而单线程的代码一定是串行化运行的

线程池不仅要能够提高程序运行效率,还要提高程序处理业务的种类

存在的问题:当业务种类较少的时候可以用switch case/if else列举,业务多的时候就用不了了

线程池的原理(定义)

线程池=一堆线程+线程安全队列(元素带有任务接口)

队列元素的类型=需要处理的数据+处理数据对应的函数(任务接口)

创建固定数量线程,循环从任务队列中获取任务对象(队列元素=数据+函数)

获取到任务对象后,执行任务对象中的任务接口(函数处理数据)

线程池的代码实现:

线程安全的队列:线程安全=同步+互斥

元素类型:数据、处理数据的函数(任务接口)

队列:STL queue

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <queue>
 
/*
 * 定义队列元素的类型
 *    数据
 *    处理数据的方法
 * */
 
typedef void (*Handler)(int data);
 
class QueueData{
    public:
        QueueData(){
 
        }
 
        QueueData(int data, Handler handler){
            data_ = data;
            handler_ = handler;
        }
 
        /*
         * 怎么通过函数处理数据
         * */
        void Run(){
            handler_(data_);
        }
    private:
        //要处理的数据
        int data_;
        //要处理数据的函数(要保存一个函数的地址)
        //Handler 函数指针, handler 函数指针变量, 保存函数的地址
        Handler handler_;
};
 
 
/*
 * 线程队列
 *   互斥+同步
 *   元素类型
 *   队列
 * */
 
class SafeQueue{
    public:
        SafeQueue(){
            capacity_ = 1;
            pthread_mutex_init(&lock_, NULL);
            pthread_cond_init(&prod_cond_, NULL);
            pthread_cond_init(&cons_cond_, NULL);
        }
 
        ~SafeQueue(){
            pthread_mutex_destroy(&lock_);
            pthread_cond_destroy(&prod_cond_);
            pthread_cond_destroy(&cons_cond_);
        }
 
        void Push(QueueData& data){
            pthread_mutex_lock(&lock_);
            while(que_.size() >= capacity_){
                pthread_cond_wait(&prod_cond_, &lock_);
            }
            que_.push(data);
            pthread_mutex_unlock(&lock_);
 
            pthread_cond_signal(&cons_cond_);
        }
 
        /*
         * data : 出参, 返回给调用者的
         * */
        void Pop(QueueData* data, int flag_exit){
            pthread_mutex_lock(&lock_);
            while(que_.empty()){
                if(flag_exit == 1){
                    pthread_mutex_unlock(&lock_);
                    pthread_exit(NULL);
                }
                pthread_cond_wait(&cons_cond_, &lock_);
            }
            *data = que_.front();
            que_.pop();
            pthread_mutex_unlock(&lock_);
 
            pthread_cond_signal(&prod_cond_);
        }
 
        void BroadcaseAllConsume(){
            pthread_cond_broadcast(&cons_cond_);
        }
    private:
        std::queue<QueueData> que_;
        size_t capacity_;
        /* 互斥锁 */
        pthread_mutex_t lock_;
 
        /*
         * 同步 :
         *   生产者的条件变量()
         *   消费者的条件变量(线程池当中的一堆线程, 在逻辑上就是消费线程)
         * */
        pthread_cond_t prod_cond_;
        pthread_cond_t cons_cond_;
};
 
 
class ThreadPool{
    public:
        ThreadPool(){
        }
 
        ~ThreadPool(){
            if(sq_ != NULL){
                delete sq_;
            }
        }
 
        int InitThreadPool(int thread_count){
            /*
             * 0 : 线程继续运行
             * 1 : 线程退出
             * */
            flag_exit_ = 0;
 
            sq_ = new SafeQueue;
            if(sq_ == NULL){
                printf("Init thread pool failed\n");
                return -1;
            }
 
            thread_count_ = thread_count;
 
            /*
             * thread_count : 10
             * */
            for(int i = 0; i < thread_count_; i++){
                pthread_t tid;
                int ret = pthread_create(&tid, NULL, worker_start, (void*)this);
                if(ret < 0){
                    thread_count--;
                    continue;
                }
            }
 
            /* 
             * 判断一下线程的数量
             *    thread_count_ <= 0 : 创建工作线程全部失败, 程序就返回负数,告诉给调用者
             *    thread_count_ > 0 : 说明线程池初始化成功了
             * */
            if(thread_count_ <= 0){
                printf("create thread all failed\n");
                return -1;
            }
            //代表初始化成功
            return 0;
        }
 
        /* 
         * 线程池的使用接口 
         *   只需要给使用者提供push接口,让他能够将数据push到队列当中就好
         *   而 pop接口不需要提供, 因为线程池当中的线程可以自己调用到
         * */
        void Push(QueueData& qd){
            sq_->Push(qd);
        }
 
        /* 线程入口函数 */
        static void* worker_start(void* arg){
            pthread_detach(pthread_self());
            /*
             * 1. 从队列当中拿元素
             * 2. 处理元素
             * */
            ThreadPool* tp = (ThreadPool*)arg;
            while(1){
                QueueData qd;
                tp->sq_->Pop(&qd, tp->flag_exit_);
                
                qd.Run();
            }
        }
 
        void thread_pool_exit(){
            flag_exit_ = 1;
            sq_->BroadcaseAllConsume();
        }
    private:
        /* 线程安全的队列 */
        SafeQueue* sq_;
        /* 线程池当中线程的数量 */
        int thread_count_;
        /* 标志线程是否退出的标志位 */
        int flag_exit_;
};
 
 
 
void Deal1(int data){
    printf("i am Deal1, i deal %d\n", data);
}
 
void Deal2(int data){
    printf("hhhhhh, i am Deal2, deal %d\n", data);
}
 
int main(){
    /*
     * 1. create thread pool
     *
     * 2. push data to thread pool
     * */
 
    ThreadPool tp;
    int ret = tp.InitThreadPool(2);
    if(ret < 0){
        return 0;
    }
 
    for(int i = 0; i < 100; i++){
        QueueData qd(i, Deal1);
        tp.Push(qd);
    }
 
    tp.thread_pool_exit();
 
    while(1){
        sleep(1);
    }
    return 0;
}

线程池当中的线程应该如何退出:

主线程退出了,导致线程退出。

优雅的退出方式:

1)线程自己退出(收到某一种指令),而不是因为进程退出了被迫销毁

2)队列的元素没有带要处理的

读写锁

读写锁的模式

以读模式加锁/以写模式加锁(读写锁使用的场景一定是大量读少量写的情况,因为读写锁允许多个线程并行的读,多个线程是互斥写的:读-写、写-写)

多个线程,访问临界资源的时候都是读临界资源的内容,不会产生二义性的结果。

读写锁的接口:

初始化:int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
销毁:int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
加锁:
只读模式加锁(读模式可共享):int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
只写模式加锁(相当于互斥锁):int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
解锁:int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

代码示例:

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
 
pthread_rwlock_t  rwl;
void* Thread_start1(void* arg){
    pthread_rwlock_wrlock(&rwl);
    printf("线程1 写模式加锁成功...\n");
 
    sleep(10000);
    return NULL;
}
 
void* Thread_start2(void* arg){
    sleep(2);
 
    pthread_rwlock_rdlock(&rwl);
    printf("线程2 读模式加锁成功...\n");
 
 
    sleep(10000);
    return NULL;
}
 
int main(){
    /*
     * 读模式 + 读模式
     * */
    for(int i = 0; i < 1; i++){
        pthread_t  tid;
        pthread_create(&tid, NULL, Thread_start1, NULL);
        pthread_create(&tid, NULL, Thread_start2, NULL);
    }
    pthread_rwlock_init(&rwl, NULL);
 
 
 
    //pthread_rwlock_wrlock(&rwl);
    //printf("第三次写模式加锁成功...\n");
    //
    while(1){
        sleep(1);
    }
 
    pthread_rwlock_destroy(&rwl);
    return 0;
}

引用计数

引用计数:用来记录当前读写锁有多少个线程以读模式获取了读写锁

当有线程以读模式进行加锁,加锁成功,则引用计数++

当以读模式打开读写锁的进程,释放了读写锁之后,引用计数--

只有当引用计数为0的时候,线程才可以以写的模式打开读写锁

如果读写锁已经以读模式打开了,有一个线程A想要以写模式打开读写锁,则需要等待,等待期间又来了新的读模式加锁的线程,读模式的线程也要跟着等待

单例模式:

单例类只能有一个实例

单例类必须自己创建自己的唯一实例

单例类必须给所有其他对象提供这一实例

意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

主要解决:一个全局使用的类频繁的创建于销毁。

何时使用:当你想控制实例数目,节省系统资源的时候。

如何解决:判断系统是否已经有了这个单例,有返回,无创建。

关键代码:构造函数是私有的。

单例模式的两种形式(懒汉&饿汉)

饿汉模式:在程序启动的时候就创建唯一实例对象,饿汉模式不需要加锁。

代码模拟

#include<iostream>
/*
 * 饿汉模式的单例类
 * */
class sigleton{
    public:
        static sigleton* GetInstance();
        void Print(){
            std::cout << "sigleton print" << std::endl;
        }
    private:
        /* 这个静态的成员变量, 后面指向唯一的实例化对象 */
        static sigleton* st;
        sigleton(){};
};
 
/* 程序一旦启动, 就会创建全局唯一的实例化对象 */
sigleton* sigleton::st = new sigleton;
sigleton* sigleton::GetInstance(){
    return st;
}
 
int main(){
    sigleton* st = sigleton::GetInstance();
    st->Print();
 
    sigleton* st1 = new sigleton;
    sigleton st2;
 
    return 0;
}

懒汉模式:当你第一次使用是才创建唯一实例对象,从而实现延迟加载效果。懒汉模式在第一次使用单例对象是才能完成初始化工作,因此可能存在多线程竞态环境,如若不加锁会导致重复构造、构造不完全。

代码模拟:

#include <iostream>
 
/*
 * 懒汉模式的单例类
 *    并不是程序启动的时候, 就实例化对象
 *    而是用到的时候, 才进行实例化对象
 * */
 
class sigleton{
    public:
        static sigleton* GetInstance();
        void Print(){
            std::cout << "sigleton print" << std::endl;
        }
    private:
        sigleton(){};
        static sigleton* st;
        static pthread_mutex_t lock_;
};
 
pthread_mutex_t sigleton::lock_ = PTHREAD_MUTEX_INITIALIZER;
sigleton* sigleton::st = NULL;
sigleton* sigleton::GetInstance(){
    if(st == NULL){
        pthread_mutex_lock(&sigleton::lock_);
        if(st == NULL){
            st = new sigleton;
        }
        pthread_mutex_unlock(&sigleton::lock_);
    }
    return st;
}
 
int main(){
    sigleton* st = sigleton::GetInstance();
    st->Print();
    std::cout << st << std::endl;
 
 
    sigleton* st1 = sigleton::GetInstance();
    st1->Print();
    std::cout << st1 << std::endl;
 
    return 0;
}

乐观锁&悲观锁:

悲观锁:针对某个线程访问临界区修改数据的时候,都会认为可能有其他线程并行修改的情况发生,所以在线程修改数据之前就进行加锁,让多个线程互斥访问。悲观锁包括:互斥锁、读写锁、自旋锁等等。

乐观锁:针对某个线程访问临界区修改数据的时候,乐观的认为只有该线程在修改,大概率不会存在并行的情况,所以修改数据不加锁,但是在修改完毕进行更新的时候进行判断。

自旋锁(busy-waiting类型)和互斥锁(sleep-waiting类型)的区别

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值