多线程(线程概念、线程控制、线程安全、信号量、线程池)

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

一、线程概念

功能:进行多任务处理
一个进程能够完成任务的处理,通过pcb的描述,完成对程序的调度处理。

多任务处理:
  • 多创建几个进程,一个进程就有一个pcb,能够串行化的完成一个任务;
  • 在一个进程中多创建几个pcb,因为pcb是调度程序运行的描述,因此有多少个pcb就有多少个执行流程。

在进程中,我们说进程就是一个pcb,是程序动态运行的描述,通过pcb可以实现操作系统对程序运行的调度管理。现在学习的多线程,我们说线程是进程中的一条执行流,这个执行流在Linux下是通过pcb实现的,因此实际上Linux下的线程就是一个pcb,然而pcb是进程,并且Linux下的pcb共用一个虚拟地址空间,相较于传统pcb更加轻量化,因此也被称为轻量级进程

示例:
在这里插入图片描述

多进程

在这里插入图片描述

多线程

在这里插入图片描述
== Linux下的进程其实是一个线程组,一个进程中可以有多个线程(多个pcb),编程是进程中的一条执行流。(进程(一个工厂),线程(工厂中干活的工人),在Linux下干活的就是pcb)。==

进程:是一个程序动态的运行,其实就是一个程序运行的描述 - - - pcb。
线程:是进程中的一条执行流,执行一个程序中的某段代码。

在Linux下pcb可以实现程序的调度运行,因此在实现线程的时候,使用了pcb来实现;创建线程会伴随在内核中创建一个pcb来实现程序的调度,作为进程中的一条执行流。进程就是多个线程的一个合集,并且这个进程中的所有pcb共用进程中的大部分资源(程序运行时,操作系统为程序运行所分配的所有资源),因此这些pcb在Linux下又称为轻量级进程。

根据不同学习阶段,对pcb有不同的理解

  • 第一阶段:pcb是进程,用于调度一个程序运行;
  • 第二阶段:pcb是线程,是轻量级进程(为了跟印象中的传统进程进行区分),因为线程是运行中程序的一条执行流,Linux下通过pcb实现这个执行流,并且共用同一份运行资源。

进程是操作系统资源分配的基本单位;(操作系统会为一个程序的的运行分配所需的所有资源)
线程是cpu调度的基本单位

线程之间的独有与共享
  • 独有:标识符、寄存器、栈、信号屏蔽字、errno(系统调用完毕后会重置一个全局变量)、优先级。
  • 共享:虚拟地址空间(代码段/数据段)、文件描述符表、信号处理的回调函数、用户ID/组ID/工作路径。
多线程/多进程进行多任务处理的优缺点分析
多线程的优点:
  • 线程间通信更加灵活方便;(除了进程间通信方式之外还有全局变量以及函数传参 - - - 共有同一个虚拟地址空间,只要知道地址就能访问同一块空间);
  • 线程的创建和销毁成本更低(创建线程创建一个pcb,共用的数据只需要使用一个指针指向同一处就可以了);
  • 同一个进程中的线程间调度成本更低(调度切换需要切换页表);
多进程的优点:

适用于对于主程序安全性要求很高的场景:shell/网络服务器

  • 多进程的健壮性,稳定性更高(异常以及一些系统调用(如:exit)直接针对整个进程生效);
共同的优点:
  • IO密集型程序:多任务并行处理(多磁盘可以实现同时处理);
    (IO密集型:程序中大量进行IO操作,对cpu要求并不高,因此执行流个数没有太大要求);
  • CPU密集型程序:程序中进行大量的数据运算处理;cpu资源足够,就可以同时处理,提高效率(通常执行流的个数是cpu核心数+1),创建线程很多的话,而cpu资源不够多,会造成进程切换调度成本提高。

二、线程控制

通过代码实现线程的创建 / 退出 / 等待 / 分离
进行线程控制的接口代码,其实都是库函数,也就是说,操作系统其实并没有提供创建线程的接口,因此人们在用户态使用函数封装了一套线程库,这套封装的线程库函数,提供了线程的各种操作。
使用库函数创建一个线程(用户态线程),本质上是在内核中创建一个轻量级进程来实现程序的调度。

线程创建

int pthread_create (pthread_t *thread, const pthread_attr_t *arr, void *( *start_routine)(void * ), void *arge);

thread:输出型参数,用于获取线程id(线程的操作句柄);
attr:线程属性,用于在创建线程的同时设置线程属性,通常置NULL;
start_routine:函数指针,这是一个线程的入口函数(线程运行的就是这个函数,函数运行完毕,则线程就会退出);
arg:通过线程入口函数,传递给线程的参数;
返回值:成功返回0,失败返回非0值(错误编号);

tid是一个无符号长整型数据。一个线程有一个pcb,每个pcb都有一个pid(是一个整型数据)。

tid和pid有什么联系?
在这里插入图片描述
tid是一个用户态线程的id,线程的操作句柄,这个id其实就是线程独有的这块空间的首地址。
每个线程被创建出来后,都会开辟一块空间,存储自己的栈,自己的描述信息
pid是一个轻量级进程id,是内核中task_struct结构体中的id;
task_struct ->pid:轻量级进程id,也就是ps -efL看到的LWP;
task_struct ->tgid:线程组id,等于主线程id (也就是外边所看到的进程id);

线程的操作大都是通过tid完成的

#include <stdio.h>
#include <unistd.h>  //sleep头文件
#include <string.h>  //字符串操作头文件
#include <pthread.h> //线程库接口头文件

int a = 100;

void *thr_start(void *arg)
{
    while(1) {
        printf("i am thread~~%s ---- %d\n", (char*)arg, a);
        sleep(1);
    }
    return NULL;
}
int main()
{
    pthread_t tid;
    char ptr[1024] = "chilema~~?";
    //pthread_create(获取线程id, 线程属性, 线程入口函数, 参数)
    int ret = pthread_create(&tid, NULL, thr_start, (void*)ptr);
    if (ret != 0) {
        printf("create thread failed!!\n");
        return -1;
    }
    printf("create thread success!! normal thread tid:%lu\n", tid);
    while(1) {
        printf("leihoua~~ ----%d\n", a);
        sleep(1);
    }
    return 0;
}

线程终止

如何退出一个线程

  • 线程入口函数运行完毕,线程就会自动退出- - - -在线程入口函数中调用return(但是main函数中return,退出的是进程而不是主线程);
  • void pthread_exit(void *retval); 退出线程接口,谁调用谁退出,retval是退出返回值;(exit无论在哪个进程调用,都是退出整个进程);
  • 主线程退出,并不会导致进程退出,只有所有的线程都退出了,进程才会退出;
  • int pthread_cancel(pthread_t thread); 终止一个线程;退出的线程是被动取消的;

线程等待

等待一个线程的退出,获取退出线程的返回值,回收线程所占的资源。

  • 线程有一个属性,默认创建出来是 joinable,处于这个属性的线程,退出后,需要被其它线程等待获取返回值回收资源。
  • int pthread_join(pthread_t thread, void *retval); - - - 等待指定线程退出,获取其返回值;
    thread:需要等待退出的线程tid; – -- - -阻塞函数,线程没有退出则一直等待;
    retval:输出型参数,用于返回线程的返回值;
    线程的返回值是一个void
    ,是一个一级指针,若要通过一个函数的参数获取一级指针,就要传入一个一级指针变量的地址进来。

默认情况下,一个线程必须被等待,若不等待,则会造成资源泄露。

线程分离

将线程 joinable 属性修改成 detach 属性

  • 若是joinable 那么就必须被等待;
  • 若是detach 那么这个线程退出后则自动释放资源,不需要被等待(因为资源已经自动被释放了);

分离一个线程,一定是对线程的返回值不感兴趣,根本就不想获取,但又不想一直等待线程退出,这种情况才会分离线程。

int pthread_detach(pthread_t thread); - - - 将制定的线程分离出去(属性改为detach)
pthread_t pthread_self( void ); - - - 返回调用线程的id
  • pthread_exit是谁调用谁退出— 线程主动退出;
  • pthread_cancel 取消其它线程 —线程是被动退出;
  • tid通常创建这个线程的时候保存起来,后边就可以通过tid对这个线程进行操作;
  • 分离线程,只是说线程退出后自动释放资源;
#include <stdio.h>
#include  <unistd.h>
#include <stdlib.h>
#include <pthread.h>

void function()
{
    //char ptr[] = "这是我的返回值";//ptr有一块空间在栈中,将字符串的值赋值进去
    char *ptr = "这是我的返回值"; //字符串在常量区,ptr只是保存了这个常量区的地址
    pthread_exit((void*)ptr);//在任意位置调用,都可以退出调用线程
}
void *thr_start(void *arg)
{
    //pthread_self 返回调用线程的tid
    //pthread_detach(pthread_t tid);
    pthread_t tid = pthread_self();
    pthread_detach(tid);//自己分离自己---实际上就是设置个属性而已
    while(1) {
        printf("i am normal thread\n");
        sleep(5);
        function();
    }
    return NULL;
}
int main()
{
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, thr_start, NULL);
    if (ret != 0) {
        printf("create thread failed!!\n");
        return -1;
    }
    //char *ptr;
    //pthread_join(tid, (void**)&ptr);
    //printf("retval:%s\n", ptr);
    //pthread_cancel(tid);
    while(1) {
        printf("i am main thread\n");
        sleep(1);
    }
    return 0;
}

三、线程安全

多个执行流对临界资源争抢访问,但是不会出现数据二义性。

线程安全实现

同步:通过条件判断保证对临界资源访问的合理性;
互斥:通过同一时间对临界资源访问的唯一性实现临界资源访问的安全性;

互斥的实现:互斥锁

互斥锁实现互斥原理:互斥锁本身是一个只有0/1的计数器,描述了一个临界资源当前的访问状态,所有执行流在访问临界资源时都需要先判断当前的临界资源状态是否允许访问,如果不允许则让执行流等待,否则可以让执行流访问临界资源,但是在访问期间需要将状态修改为不可访问状态,这期间不允许其他执行流进行访问。

互斥锁具体操作流程和接口介绍
  • 定义互斥锁变量 pthread_mutex_t mutex;
  • 初始化互斥锁变量
    pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *atr);
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER;
  • 在访问临界资源之前进行加锁操作(不能加锁则等待,可以加锁则修改资源状态,然后调用返回,访问临界资源)
    pthread_mutex_lock(pthread_mutex_t *mutex); (阻塞加锁,如果当前不能加锁(锁已经被别人加了),则一直等待直到加锁成功调用返回);
    /pthread_mutex_trylock(pthread_mutex_t *mutex);(非阻塞加锁,如果当前不能加锁,则立即报错返回 - - - - EBUSY)
    挂起等待:将线程状态置为可中断休眠状态(表示当前休眠);
    被唤醒:将线程状态置为运行状态;
  • 在临界资源访问完毕之后进行解锁操作(将资源状态置为可访问,将其他执行流唤醒);
    pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 销毁互斥锁
    pthread_mutex_destroy(pthread_mutex_t *mutex);

所有的执行流都需要通过同一个互斥锁实现互斥,意味着互斥锁本身就是一个临界资源,大家都会访问。

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

int ticket = 100;
pthread_mutex_t mutex;

void *thr_scalpers(void *arg)
{
    while(1) {
        //加锁一定是只保护临界资源的访问
        pthread_mutex_lock(&mutex);
        if (ticket > 0) {
            //有票就一直抢
            usleep(1000);
            printf("%p-I got a ticket:%d\n", pthread_self(), ticket);
            ticket--;
            pthread_mutex_unlock(&mutex);
        }else {
            //加锁后在任意有可能退出线程的地方都要解锁
            pthread_mutex_unlock(&mutex);
            pthread_exit(NULL);
        }
    }
    return NULL;
}

int main()
{
    pthread_t tid[4];

    int i, ret;
    //互斥锁的初始化一定要放在线程创建之前
    pthread_mutex_init(&mutex, NULL);
    for (i = 0; i< 4; i++) {
        ret = pthread_create(&tid[i], NULL, thr_scalpers, NULL);
        if (ret != 0) {
            printf("thread create error");
            return -1;
        }
    }
    for (i = 0; i < 4; i++) {
        pthread_join(tid[i], NULL);
    }
    //互斥锁的销毁一定是不再使用这个互斥锁
    pthread_mutex_destroy(&mutex);
    return 0;
}

互斥锁本身的操作首先必须是安全的,互斥锁自身计数的操作是原子操作。

在这里插入图片描述
不管当前mutex的状态是什么,反正一步交换后,其他的线程都是不可访问的;这时候当前进程就可以慢慢判断了;

死锁

多个执行流对锁资源进行争抢访问,但是因为访问推进顺序不当,造成互相等待最终导致程序流程无法继续推进,这时候就造成了死锁,(死锁实际上就是一种程序流程无法继续推进,卡在某个位置)。

死锁产生的必要条件(有一条不具备就不会产生死锁)

  1. 互斥条件:我加了锁,别人就不能再继续加锁;
  2. 不可剥夺条件:我加了锁,别人不能解我的锁,只有我能解锁;
  3. 请求与保持条件:我加了A锁,然后去请求B锁;如果不能对B锁加锁,则也不释放A锁;
  4. 环路等待条件:我加了A锁,然后去请求B锁;另一个人也加了B锁,然后去请求A锁;

死锁的预防:破坏死锁产生的必要条件(主要避免3和4两个条件的产生)。
死锁的避免:死锁检测算法 / 银行家算法

银行家算法的思路:系统的安全状态/非安全状态
一张表记录当前有哪些锁,一张表记录已经给谁分配了哪些锁,一张表记录谁当前需要哪些锁
按照三张表进行判断,判断若给一个执行流分配了指定的锁,是否会达成环路等待条件,导致系统的运行进入不安全状态,如果有可能就不能分配。反之,若分配了之后不会造成环路等待,系统是安全的,则分配这个锁,(破坏环路等待条件)。
后续若不能分配锁,可以资源回溯,把当前执行流中已经加了的锁释放掉,(破坏请求与保持)。

死锁是如何产生的?如何预防和避免?
加锁对临界资源进行保护,实际上对程序的性能时一个极大的挑战。在高性能程序中会将就一种无锁编程(CAS锁 / 一对一的阻塞队列 / atomic原子操作)。

同步的实现:通过条件判断实现临界资源访问的合理性(条件变量);

  • 当前是否满足获取资源的条件,若不满足,则让执行流等待,等到满足条件能够获取的时候再唤醒执行流。
  • 条件变量实现同步:只提供了两个功能接口:让执行流等待的接口和唤醒执行流的接口。因此条件的判断是需要进程自身进行操作,自身判断是否满足条件,不满足的时候调用条件变量接口使线程等待。

使用接口介绍

  • 定义条件变量: pthread_cond_t cond;
  • 初始化条件变量:
    pthread_cond_init( pthread_cond_t *cond, pthread_condattr_t *attr);
    pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  • 是线程挂起休眠,若资源获取条件不满足时调用接口进行阻塞等待
    pthread_cond_wait( pthread_cond_t *cond, pthread_mutex_t *mutex); 一直死等别人的唤醒条件变量是搭配互斥锁一起使用的。
    pthread_cond_timedwait( pthread_cond_t * ,pthread_mutex_t *,struct timespec *); 设置阻塞超时时间的等待接口,(等待指定时间内都没有被唤醒则自动醒来)
  • 唤醒线程的接口
    pthread_cond_signal(pthread_cond_t *); - - -唤醒至少一个等待的进程(并不是唤醒单个)
    pthread_cond_broadcast(pthread_cond_t *); - - -唤醒所有等待的进程
  • 销毁条件变量
    pthread_cond_destroy(pthread_cond_t *);

例子:
在这里插入图片描述
在这里插入图片描述

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

int bowl = 0;//默认0表示碗中没有饭


pthread_cond_t cook_cond;   // 实现线程间对bowl变量访问的同步操作
pthread_cond_t customer_cond;   // 实现线程间对bowl变量访问的同步操作
pthread_mutex_t mutex; // 保护bowl变量的访问操作

void *thr_cook(void *arg)
{
    while(1) {
        //加锁
        pthread_mutex_lock(&mutex);
        while (bowl != 0){//表示有饭,不满足做饭的条件
            //让厨师线程等待,等待之前先解锁,被唤醒之后再加锁
            //pthread_cond_wait接口中就完成了解锁,休眠,被唤醒后加锁三部操作
            //并且解锁和休眠操作是一步完成,保证原子操作
            pthread_cond_wait(&cook_cond, &mutex);
        }
        bowl = 1; //能够走下来表示没饭,bowl==0, 则做一碗饭,将bowl修改为1
        printf("I made a bowl of rice\n");
        //唤醒顾客吃饭
        pthread_cond_signal(&customer_cond);
        //解锁
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}
void *thr_customer(void *arg)
{
    while(1) {
        //加锁
        pthread_mutex_lock(&mutex);
        while (bowl != 1) { // 没有饭,不满足吃饭条件,则等待
            //没有饭则等待,等待前先解锁,被唤醒后加锁
            pthread_cond_wait(&customer_cond, &mutex);
        }
        bowl = 0; // 能够走下来表示有饭 bowl==1, 吃完饭,将bowl修改为0
        printf("I had a bowl of rice. It was delicious\n");
        //唤醒厨师做饭
        pthread_cond_signal(&cook_cond);
        //解锁
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}
int main()
{
    pthread_t cook_tid[4], customer_tid[4];
    int ret, i;

    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cook_cond, NULL);
    pthread_cond_init(&customer_cond, NULL);

    for (i = 0; i < 4; i++) {
        ret = pthread_create(&cook_tid[i], NULL, thr_cook, NULL);
        if (ret != 0) {
            printf("pthread_create error\n");
            return -1;
        }
        ret = pthread_create(&customer_tid[i], NULL, thr_customer, NULL);
        if (ret != 0) {
            printf("pthread_create error\n");
            return -1;
        }
    }

    pthread_join(cook_tid[0], NULL);
    pthread_join(customer_tid[0], NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cook_cond);
    pthread_cond_destroy(&customer_cond);
    return 0;
}
生产者与消费者模型

优点:解耦合、支持忙闲不均、支持开发
实现:一个场所(线程安全的缓冲区- - - 数据队列),两种角色(生产者和消费者),三种关系(实现线程安全)。
在这里插入图片描述

#include <cstdio>
#include <iostream>
#include <vector>
#include <pthread.h>
#include <semaphore.h>

#define QUEUE_MAX 5
class RingQueue{
    public:
        RingQueue(int maxq = QUEUE_MAX):_queue(maxq), _capacity(maxq),
        _step_read(0), _step_write(0){
            //sem_init(信号量, 进程/线程标志, 信号量初值)
            sem_init(&_lock, 0, 1);//用于实现互斥锁
            sem_init(&_sem_data, 0, 0);//数据空间计数初始为0
            sem_init(&_sem_idle, 0, maxq);//空闲空间计数初始为节点个数
        }
        ~RingQueue(){
            sem_destroy(&_lock);
            sem_destroy(&_sem_data);
            sem_destroy(&_sem_idle);
        }
        bool Push(int data){
            //1. 判断是否能够访问资源,不能访问则阻塞
            sem_wait(&_sem_idle);//-空闲空间计数的判断,空闲空间计数-1
            //2. 能访问,则加锁,保护访问过程
            sem_wait(&_lock);//lock计数不大于1,当前若可以访问则-1,别人就不能访问了
            //3. 资源的访问
            _queue[_step_write] = data;
            _step_write = (_step_write + 1) % _capacity;//走到最后,从头开始
            //4. 解锁
            sem_post(&_lock);//lock计数+1,唤醒其它因为加锁阻塞的线程
            //5. 入队数据之后,数据空间计数+1,唤醒消费者
            sem_post(&_sem_data);
            return true;
        }
        bool Pop(int *data){
            sem_wait(&_sem_data);//有没有数据
            sem_wait(&_lock); // 有数据则加锁保护访问数据的过程
            *data = _queue[_step_read]; //获取数据
            _step_read = (_step_read + 1) % _capacity;
            sem_post(&_lock); // 解锁操作
            sem_post(&_sem_idle);//取出数据,则空闲空间计数+1,唤醒生产者
            return true;
        }
    private:
    std::vector<int> _queue; // 数组, vector需要初始化节点数量
    int _capacity; // 这是队列的容量
    int _step_read; // 获取数据的位置下标
    int _step_write;//写入数据的位置下标
    //这个信号量用于实现互斥
    sem_t _lock ;
    //这个信号量用于对空闲空间进行计数
    //---对于生产者来空闲空间计数>0的时候才能写数据 --- 初始为节点个数
    sem_t _sem_idle; 
    // 这个信号量用于对具有数据的空间进行计数
    // ---对于消费者来说有数据的空间计数>0的时候才能取出数据 -- 初始为0
    sem_t _sem_data; 
};


void *thr_productor(void *arg) 
{
    //这个参数是我们的主线程传递过来的数据
    RingQueue *queue = (RingQueue*)arg;//类型强转
    int i = 0;
    while(1) {
        //生产者不断生产数据
        queue->Push(i);//通过Push接口操作queue中的成员变量
        printf("productor push data:%d\n", i++);
    }
    return NULL;
}
void *thr_customer(void *arg) 
{
    RingQueue *queue = (RingQueue*)arg;
    while(1) {
        //消费者不断获取数据进行处理
        int data;
        queue->Pop(&data);
        printf("customer pop data:%d\n", data);
    }
    return NULL;
}
int main()
{
    int ret, i;
    pthread_t ptid[4], ctid[4];
    RingQueue queue;

    for (i = 0; i < 4; i++) {
        ret = pthread_create(&ptid[i], NULL, thr_productor, (void*)&queue);
        if (ret != 0) {
            printf("create productor thread error\n");
            return -1;
        }
        ret = pthread_create(&ctid[i], NULL, thr_customer, (void*)&queue);
        if (ret != 0) {
            printf("create productor thread error\n");
            return -1;
        }
    }
    for (i = 0; i < 4; i++) {
        pthread_join(ptid[i], NULL);
        pthread_join(ctid[i], NULL);
    }
    return 0;
}

多线程的并发- - -操作系统层面的轮询调度(或者CPU资源足够情况下的并行)

生产者与消费者,其实是两种业务处理的线程而已,我们创建线程就可以,实现的关键在于线程安全队列。
封装一个线程安全的BlockQueue- - -阻塞队列- - -向外提供线程安全的入队/出队操作

class BlockQueue
{
public:
	BlockQueue();  //编码风格:纯输入参数 -const& /输出型参数 指针/输出入输出型 &(引用)
	bool Push(const int &data);  //入队数据
	bool Pop(int *data);         //出队数据
private:
	std::queue<int> _queue;     //STL中queue容器,是非线程安全的---因为STL设计之初就是奔着性能去的,并且功能多了耦合度就高了
	int _capacity;             //队列中节点的最大数量---数据也不能无限制添加,内存耗尽程序就崩溃了
	pthread_mutex_t_mutex;
	pthread_cond_t_productor_cond; //生产者队列
	pthread_cond_t_customer_cond; //消费者队列
}

注意事项

1、条件变量需要搭配互斥锁一起使用,prhread_cond_wait 集合了解锁/休眠/被唤醒后加锁的散步操作;
2、程序员在程序中对访问条件是否满足的判断需要使用while 循环进行判断;
3、在同步实现时,多种不同的角色线程需要使用多个条件变量,不要让所有的线程等待在一个条件变量上。

在这里插入图片描述

四、信号量

可以用于实现进程 / 线程间同步与互斥,信号本质就是一个计数器 + pcb等待队列
同步的实现:通过自身的计数器对资源进行计数,并且通过计数器的资源计数,判断进程/线程是否能够符合访问资源的条件,若符合则可以访问,若不符合则调用提供的接口使进程/线程阻塞;其他进程/线程促使条件满足后,可以唤醒pcb等待队列上的pcb。

互斥的实现:保证计数器的技术不大于1,就保证了资源只有一个,同一时间只有一个进程/线程能够访问资源,实现互斥。

代码的操作

  • 1、定义信号量:sem_t
  • 2、初始化信号量:int sem_init(sem_t *sem, int pshared, unsigned int value);
    sem:定义的信号量变量;
    pshared:0 - - -用于线程间; 非0 - - - 用于进程间;
    value:初始化信号量的初值 - - - 初始资源数量有多少计数就是多少;
    返回值:成功则返回0,失败返回 -1;
  • 3、在党文临界资源之前,先访问信号量,判断是否能够访问(计数 -1)
    int sem_wait(sem_t *sem); - - - 通过自身计数判断是否满足访问条件,不满足则直接一直阻塞线程 / 进程;
    int sem_trywait(sem_t *sem); - - - 通过自身计数判断是否满足访问条件,不满足则立即报错返回,WINAL;
    int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); - - - 不满足则等待指定时间,超时后报错返回 - - -ETIMEDOUT
  • 4、促使访问条件满足 +1,唤醒阻塞线程 / 进程
    int sem_post(sem_t *sem); - - - 通过信号量唤醒自己阻塞队列上的pcb;
  • 5、销毁信号量
    int sem_destroy(sem_t *sem);

通过信号量实现一个生产者与消费者模型 - - -线程安全的阻塞队列
使用数组实现一个队列
队列:满足先进先出特性的容器都是队列
在这里插入图片描述

class RingQueue
{
	std::vector<int>_queue;   //数组
	int _capcity;         //这是队列的容量
	int _strp_read;        //获取数据的位置下标
	int _step_write;       //写入数据的位置下标
	
	sem_t lock;     //这个信号量用于实现互斥
	sem_t_sem_idle;   //这个信号量用于对空闲空间进行计数,对于生产者来空闲空间计数>0的时候才能写数据(初始化为节点个数)
	sem_t_sem_data;   //这个信号量用于对具体数据的空间进行计数,对于消费者来说有数据的空间计数 >0 的时候才能取出数据(初始值为0)
}

五、线程池

线程池:线程的池子,有很多线程,但是数量不会超过池子的限制。需要用到多执行流进行任务处理的时候,就从池子中取出一个线程去处理。
应用场景:由大量的数据处理请求,需要多执行流并发/并行处理。

若是一个数据请求的到来伴随着一个线程的创建去处理,则会产生一些风险以及一些不必要的损耗:

  • 1、线程若不限制数量的创建,在峰值压力下,线程创建过多,资源耗尽,有程序崩溃的风险;
  • 2、处理一个任务的时间:创建线程 t1 + 任务处理事件 t2 + 线程销毁时间 t3 = T,若 t2/T 比例占据不够高,则表示大量的资源用于线程的创建和销毁成本上,因此线程池使用已经创建好的线程进行循环任务处理,就避免了大量线程的频繁创建与销毁的时间成本。

自主编写一个线程池:大量线程(每个线程中都是进行循环任务处理)+ 任务缓冲队列

线程的入口函数,都是在创建线程的时候,就固定传入,导致线程池中的线程进行任务处理的方式过于单一
因为线程的入口函数都是一样的,处理流程也都是一样的,只能处理单一方式的请求,灵活性太差

若任务队列中的任务,不仅仅是单纯的数据,而是包含任务处理方法在内的数据,这时候,线程池中的线程只需要使用传入的方法,处理传入的数据即可,不需要关心是什么数据,如何处理,提高线程池的灵活性。

线程池就类似于一个实现了消费者业务的生产者与消费者模型

**每个线程的入口函数中,只需要不断的获取任务节点,调用任务节点中Run接口就可以实现处理了。

typedef void(*_handler)(int data);
class MyTask{
public:
	SteTask(int data, handler_t handler); //用户自己传入要处理的数据和方法,组织出一个任务节点
	Run(){ return_handler(_data);}
private:
	int _data;  //要处理的数据
	handler_t_handler;  //处理数据的方法
}
class ThreadPool{
	int thr_max; //定义线程池中线程的最大数量,初始化时创建相应数量的线程即可
	std::queue<MyTask>_queue; 
	pthread_mutex_t  _mutex; //实现_queue操作的安全性
	pthread_cond_t _cond; //实现线程池中消费者线程的同步
}

要处理什么数据,什么样处理的方法,组织成一个任务节点,交给线程池,线程中找出任意一个线程只需要使用方法处理数据即可。

STL中的容器都是线程安全的吗? - - - - - 不是
只能指针是线程安全的吗? - - unique_ptr 因为局部操作/ shared_ptr 原子操作,不涉及线程安全的问题

线程池.hpp 程序:

#include <cstdio>
#include <iostream>
#include <queue>
#include <stdlib.h>
#include <pthread.h>

typedef void (*handler_t)(int);
class ThreadTask
{
    public:
        ThreadTask(){
        }
        void SetTask(int data, handler_t handler) {
            _data = data;
            _handler = handler;
        }
        void Run() {//外部只需要调用Run,不需要关系任务如何处理
            return _handler(_data);
        }
    private:
        int _data;//任务中要处理的数据
        handler_t _handler;//任务中处理数据的方法
};

#define MAX_THREAD 5
class ThreadPool
{
    public:
        ThreadPool(int max_thr = MAX_THREAD):_thr_max(max_thr){
            pthread_mutex_init(&_mutex, NULL);
            pthread_cond_init(&_cond, NULL);
            for (int i = 0; i < _thr_max; i++) {
                pthread_t tid;
                int ret = pthread_create(&tid, NULL, thr_start, this);
                if (ret != 0) {
                    printf("thread create error\n");
                    exit(-1);
                }
            }
        }
        ~ThreadPool(){
            pthread_mutex_destroy(&_mutex);
            pthread_cond_destroy(&_cond);
        }
        bool TaskPush(ThreadTask &task) {
            pthread_mutex_lock(&_mutex);
            _queue.push(task);
            pthread_mutex_unlock(&_mutex);
            pthread_cond_broadcast(&_cond);//如对后唤醒所有线程,谁抢到谁处理
            return true;
        }
        // 类的成员函数,有一个隐藏的默认参数,是this指针
        // 线程入口函数,没有this指针,如何操作私有成员呢??
        static void *thr_start(void *arg) {
            ThreadPool *p = (ThreadPool *) arg;
            //不断的从任务队列中取出任务,执行任务的Run接口就可以
            //每一个任务节点中包含了要处理的数据,以及如何处理的函数
            while(1) {
                pthread_mutex_lock(&p->_mutex);
                while(p->_queue.empty()) {
                    pthread_cond_wait(&p->_cond, &p->_mutex);
                }
                ThreadTask task;
                task = p->_queue.front();
                p->_queue.pop();
                pthread_mutex_unlock(&p->_mutex);
                task.Run();//任务的处理要放在解锁之外,因为当前的所保护的时队列的操作
            }
            return NULL;
        }
    private:
        int _thr_max; // 线程池中线程的最大数量--根据这个初始化创建指定数量的线程
        std::queue<ThreadTask> _queue;
        pthread_mutex_t _mutex;//保护队列操作的互斥锁
        pthread_cond_t _cond;//实现从队列中获取节点的同步条件变量
};

线程池 main 程序:

#include <unistd.h>
#include "threadpool.hpp"

void test_func(int data)
{
    int sec = (data % 3) + 1;
    printf("tid:%p -- get data:%d , sleep:%d\n", pthread_self(), data, sec);
    sleep(sec);
}
void tmp_func(int data) {
    printf("tid:%p -- tmp_func\n", pthread_self());
    sleep(1);
}
int main()
{
    ThreadPool pool;
    for(int i = 0; i < 10; i++) {
        ThreadTask task;
        if (i % 2 == 0) {
            task.SetTask(i, test_func);
        }else {
            task.SetTask(i, tmp_func);
        }
        pool.TaskPush(task);
    }

    sleep(1000);
    return 0;
}

线程安全的单例模式

单例模式:是一种典型常用的一种设计模式,一份资源只能被申请加载一次 / 单例模式的方法创建的类在当前进程中只有一个实例。

实现方式

  • 饿汉方式:资源的程序初始化的时候就去加载,后面使用的话就直接使用。使用的时候比较流畅,有可能会加载用不上的资源,并且会导致程序初始化的时间比较慢。(使用static就可以将一个成员变量设置为静态变量,则所有对象共用一份资源,并且在程序初始化时就会申请资源,不涉及线程安全)。
  • 懒汉模式:资源在使用的时候发现还没有加载,则申请加载。程序初始化比较快,第一次运行某个模块的时候就会比较慢,因为这时候去加载相应资源。

实现过程中需注意的细节

  • 使用 static,保证所有对象使用同一份资源;
  • 使用 volatile,放置编译器过度优化;
  • 实现线程安全,保证资源判断以及申请过程是安全的;
  • 外部二次判断,以及避免资源已经加载成功每次获取都要加锁解锁,带来的锁冲突;
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值