「Linux」400行纯C语言代码带你「手撕线程池」

news2024/11/15 15:34:50

线程池的基本概念

不管线程池是什么东西!但是我们必须知道线程池被搞出来的目的就是:提高程序执行效率而设计出来的;

了解了线程池的目的后:我们就可以开始理解线程池:

首先回答一个问题:为什么会有线程池?

呃呃,我这么问就很奇怪,因为线程池是什么我都没说,怎么会知道为什么会有线程池呢?所以我打算带大家去思考一个场景:

当我们的程序中:有一批任务到来时候(通常该任务都是从网络来的),我们就会创建一堆线程去处理这一批任务;

虽然说创建线程的成本开销并不大,但是这里有个问题:当我们任务来到时候,你才去创建线程去处理这个任务,你不觉得这样很慢吗?

是否我们可以换个思路:假如我们有一种手段:使得任务一到来,就可以马上有线程去处理这批任务,这样是不是相对于前面等线程来到,再创建线程去处理时候快得多;

所以说:线程池就是基于上面的思路设计的;线程池就是:预先创建好一大批线程,同时线程池维护一个队列,来存放到来的任务,当队列中一旦有任务时候,预先创建好的一大批线程就可以并发处理这一批任务了;


我们抽象出一个模型:

任务派发者是谁? 是生产者;

任务存储的队列是什么?是一个容器,数组,链表,只要是可以存放产品(数据)的东西即可;

拿任务去处理的是谁?是消费者;

所以说:线程池本质就是一个生产者消费者的模型;

而我们线程池只需要关注两个点:一个存放任务的队列,和消费队列任务的消费者即可;而生产者暂时不用关注,因为生产者是你外部搞出任务丢给线程池去使用;那么什么时候可以关心生产者呢?

也就是当我们去使用线程池的时候咯;这不就是妥妥的生产者消费者模型嘛!

线程池实现的基本思路:

在各个编程语言的语种中都有线程池的概念,并且很多语言中直接提供了线程池,作为程序猿直接使用就可以了,下面给大家介绍一下线程池的实现原理:

线程池的组成主要分为 3 个部分,这三部分配合工作就可以得到一个完整的线程池:

任务队列,存储需要处理的任务,由工作的线程来处理这些任务

通过线程池提供的 API 函数,将一个待处理的任务添加到任务队列,或者从任务队列中删除;

已处理的任务会被从任务队列中删除;

线程池的使用者,也就是调用线程池函数往任务队列中添加任务的线程就是生产者线程;

工作的线程(任务队列任务的消费者) ,N个

线程池中维护了一定数量的工作线程,他们的作用是是不停的读任务队列,从里边取出任务并处理

工作的线程相当于是任务队列的消费者角色;

如果任务队列为空,工作的线程将会被阻塞 (使用条件变量 / 信号量阻塞);

如果阻塞之后有了新的任务,由生产者将阻塞解除,工作线程开始工作;

管理者线程(不处理任务队列中的任务),1个

它的任务是周期性的对任务队列中的任务数量以及处于忙状态的工作线程个数进行检测;

当任务过多的时候,可以适当的创建一些新的工作线程;

当任务过少的时候,可以适当的销毁一些工作的线程;

相关视频推荐

100行代码手写线程池,人人都能实现的(自备linux环境)

手把手C++实现线程池及线程池性能优化分析

池式组件为性能飙升提供技术保障-线程池,内存池,异步请求池,数据库连接池,无锁队列的ringbuffer

学习地址:C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂

需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

 

线程池的代码

1.任务队列的任务结构体

对于任务队列:

里面存放的都是函数指针,该函数指针指向的就是处理任务的函数;

同时还要维护一个任务函数的形参;

typedef struct Task
{
    void (*function)(void *args); //任务的函数指针
    void *args; //任务函数的形参
} Task;

2. 线程池的定义

线程池里面最重要的是:

一个任务队列;
多个消费者线程IDs;
一个管理者线程ID;
管理线程池的锁;
管理任务队列是否为满和空的条件变量;

还有一些其他的辅助成员变量;

struct ThreadPool
{
    Task *taskQ; //任务队列
    /*对于一个任务队列:我们需要知道以下信息*/
    int queueCapacity; //队列的容量
    int queueSize;     //当前任务的个数
    int queueFront;    //队头取任务
    int queueRear;     //队尾放任务

    /*有了任务队列后,还要有管理任务队列的线程和从任务队列拿任务的线程*/
    pthread_t managerID; //管理者线程
    /*设置为指针的目的:工作线程有多个*/
    pthread_t *threadIDs; //工作线程(也就是消费者)

    /*对于工作线程我们要知道以下这几个消息方便管理*/
    int minNum;  //最少的工作线程数
    int maxNum;  //最多的工作线程数
    int busyNum; //正在工作的线程数,也就是正在获取任务处理的线程
    int liveNum; //存货的工作线程数(也就是被唤醒的线程,却没有资格去获取任务的线程)
    int exitNum; //销毁的工作线程数(因为可能工作线程存在,但是却不工作,我们需要杀掉一些不必要的线程)

    /*  由于任务队列为临界资源:
        工作线程(消费者)可能有多个会同时竞争该资源
        同时多生产者线程之间(也就是往任务队列放任务的线程)也会竞争该资源
        所以我们要保证互斥访问线程池的任务队列
    */
    pthread_mutex_t mutexpool;    //锁整个线程池
    pthread_mutex_t mutexbusyNum; //锁增在工作线程的数量
    /*由于任务队列满,或者为空:
      生产者和消费者都需要阻塞
      所以需要条件变量,来保证
    */
    pthread_cond_t notFull;  //判断线程池是否为满
    pthread_cond_t notEmpty; //判断线程池是否为空

    /*辅助成员主要判断该线程池是否还在工作*/
    int shutdown; //判断是否需要销毁线程池,是0不销毁,是1销毁
};

线程池的头文件声明

#pragma once
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <malloc.h>
#include<stdio.h>

typedef struct ThreadPool ThreadPool; //线程池结构体,这里声明的原因是结构体定义在线程池源文件中

//创建线程池并初始化
ThreadPool* threadPoolCreate(int min,int max,int queueSize);

//销毁线程池
int threadPoolDestroy(ThreadPool* pool);

//给线程池添加任务
void threadPoolAdd(ThreadPool* pool,void(*functions)(void*),void* args);

//获取线程池工作线程的个数
int threadBusyNum (ThreadPool* pool);

//获取线程池存活的线程的个数
int threadLiveNum (ThreadPool* pool);
//工作线程
void* worker (void* args);
//管理线程
void* manager (void* args);

//线程退出函数
void threadExit(ThreadPool* pool);

线程池的源文件

#include"thread_pool.h"

const int WORK_THREAD_NUMBER = 2; //管理者线程要添加的工作线程个数,和销毁的线程个数
/*
线程池:首先要有个任务队列,在C语言中,
任务队列是需要自己定义的,C++中可以直接使用容器queue
*/
//任务队列存放的任务就是一个函数指针
typedef struct Task
{
    void (*function)(void *args);
    void *args;
} Task;

//再搞出一个线程池

struct ThreadPool
{
    Task *taskQ; //任务队列
    /*对于一个任务队列:我们需要知道以下信息*/
    int queueCapacity; //队列的容量
    int queueSize;     //当前任务的个数
    int queueFront;    //队头取任务
    int queueRear;     //队尾放任务

    /*有了任务队列后,还要有管理任务队列的线程和从任务队列拿任务的线程*/
    pthread_t managerID; //管理者线程
    /*设置为指针的目的:工作线程有多个*/
    pthread_t *threadIDs; //工作线程(也就是消费者)

    /*对于工作线程我们要知道以下这几个消息方便管理*/
    int minNum;  //最少的工作线程数
    int maxNum;  //最多的工作线程数
    int busyNum; //正在工作的线程数,也就是正在获取任务处理的线程
    int liveNum; //存货的工作线程数(也就是被唤醒的线程,却没有资格去获取任务的线程)
    int exitNum; //销毁的工作线程数(因为可能工作线程存在,但是却不工作,我们需要杀掉一些不必要的线程)

    /*  由于任务队列为临界资源:
        工作线程(消费者)可能有多个会同时竞争该资源
        同时多生产者线程之间(也就是往任务队列放任务的线程)也会竞争该资源
        所以我们要保证互斥访问线程池的任务队列
    */
    pthread_mutex_t mutexpool;    //锁整个线程池
    pthread_mutex_t mutexbusyNum; //锁增在工作线程的数量
    /*由于任务队列满,或者为空:
      生产者和消费者都需要阻塞
      所以需要条件变量,来保证
    */
    pthread_cond_t notFull;  //判断线程池是否为满
    pthread_cond_t notEmpty; //判断线程池是否为空

    /*辅助成员主要判断该线程池是否还在工作*/
    int shutdown; //判断是否需要销毁线程池,是0不销毁,是1销毁
};

//************************************************************************************************

/*由于我们的线程池被创建出来时候,就必须保证存在的,
    所以我们返回值要设计为指针类型,不能是赋值拷贝的形式
    并且如何考虑线程池需要传入什么参数初始化呢?
*/
ThreadPool *threadPoolCreate(int min, int max, int queueSize)
{
    //先搞出一个线程池
    ThreadPool *pool = (ThreadPool *)malloc(sizeof(ThreadPool));
    do // do while(0)的设计是为了,假设开辟线程池,消费者线程IDs,任务队列空间失败,可以直接跳出循环统一处理释放空间
    {
        if (pool == NULL)
        {
            printf("malloc threadPool is failed\n");
            break;
        }
        //搞出线程池后开始初始化里面的数据成员

        //首先先搞出消费者线程出来
        pool->threadIDs = (pthread_t *)malloc(sizeof(pthread_t) * max);
        if (pool->threadIDs == NULL)
        {
            printf("malloc threadIDs is failed\n");
            /*如果没有do while(0)的设计,这里直接返回,那么前面的pool内存池的空间没有被释放,这就会内存泄漏了*/
            // return NULL;

            //基于上面的注释考虑,这里设计break;退出dowhile(0)然后处理
            break;
        }
        //初始化消费者线程ID
        /*这么做的目的是:在管理者线程中可以通过判断线程ID是否为0,来说明该消费者线程是否被占用*/
        memset(pool->threadIDs, 0, sizeof(pthread_t) * max);
        //初始化线程池的其他成员属性
        pool->minNum = min;
        pool->maxNum = max;
        pool->busyNum = 0;
        pool->liveNum = min;
        pool->exitNum = 0;
        //初始化锁和条件变量
        if (pthread_mutex_init(&pool->mutexpool, NULL) != 0 ||
            pthread_mutex_init(&pool->mutexpool, NULL) != 0 ||
            pthread_cond_init(&pool->notEmpty, NULL) != 0 ||
            pthread_cond_init(&pool->notFull, NULL) != 0)
        {
            perror("mutex or condition failed:");
        }
        //初始化任务队列
        pool->taskQ = (Task *)malloc(sizeof(Task) * queueSize);
        if (pool->taskQ == NULL)
        {
            printf("malloc taskQ is failed\n");
            break;
        }
        pool->queueCapacity = queueSize;
        pool->queueSize = 0;
        pool->queueFront = 0;
        pool->queueRear = 0;
        //刚开始不关闭线程池
        pool->shutdown = 0;

        //创建管理者线程和消费者线程
        pthread_create(&pool->managerID, NULL, manager, (void *)pool);
        int i = 0;
        for (; i < min; ++i)
        {
            /*消费线程需要消费的是任务,
            也就是taskQ,而taskQ又是pool的一个成员属性
            所以传参时候,我们传入pool就可以获得taskQ了
            */
            pthread_create(&pool->threadIDs[i], NULL, worker, (void *)pool);
        }

        //创建成功初始化后,那么就可以把线程池返回去了
        return pool;
    } while (0);
    //如果break出来,那么就是异常的开辟空间失败,要释放资源
    if (pool)
        free(pool);
    if (pool && pool->threadIDs)
        free(pool->threadIDs);
    if (pool && pool->taskQ)
        free(pool->taskQ);

    return NULL;
}

//判断任务队列是否为空
static int taskQIsEmpty(ThreadPool *pool)
{
    return pool->queueSize == 0;
}
//判断线程池是否还工作
static int isShutDown(ThreadPool *pool)
{
    return pool->shutdown == 1 ? 1 : 0;
}

//消费者线程
void *worker(void *args)
{
    ThreadPool *pool = (ThreadPool *)args;
    /*设计为死循环是:消费者要不断从任务队列拿任务来处理*/
    while (1)
    {
        pthread_mutex_lock(&pool->mutexpool);
        //消费数据之前,要判断任务队列是否为空,空就需要挂起该线程
        while (taskQIsEmpty(pool) && !isShutDown(pool))
        {
            pthread_cond_wait(&pool->notEmpty, &pool->mutexpool);

            //线程被唤醒后,判断是否需要销毁该线程,因为有线程是多余的
            if (pool->exitNum > 0)
            {
                pool->exitNum--;
                if (pool->liveNum > pool->minNum)
                {
                    pool->liveNum--;
                    pthread_mutex_unlock(&pool->mutexpool); //退出线程前解锁,防止死锁问题
                    threadExit(pool);
                }
            }
        }
        //还需要判断线程池是否关闭了,关闭了就退出消费者线程即可
        if (isShutDown(pool))
        {
            pthread_mutex_unlock(&pool->mutexpool);
            threadExit(pool);
        }
        //开始消费者拿任务
        Task task;                                              //保存任务的变量
        task.function = pool->taskQ[pool->queueFront].function; //获取到任务队列的任务,就是一个函数指针
        task.args = pool->taskQ[pool->queueFront].args;           //获取任务队列任务的函数指针参数

        //控制任务队列的指针移动
        pool->queueFront++;
        pool->queueFront %= pool->queueCapacity;
        pool->queueSize--;

        pthread_mutex_unlock(&pool->mutexpool);
         //唤醒生产者
        pthread_cond_signal(&pool->notFull);

        //拿到任务后就是处理任务

        // 1.处理任务前,先处理busyNum
        pthread_mutex_lock(&pool->mutexbusyNum);
        pool->busyNum++;
        pthread_mutex_unlock(&pool->mutexbusyNum);

        // 2. 这里处理任务就是调用任务函数
        task.function(task.args);
        //任务处理完就释放参数的空间
        free(task.args);
        task.args = NULL;

        printf("thread %ld ending working ... \n", pthread_self());
        // 3.处理完任务对其busyNum操作
        pthread_mutex_lock(&pool->mutexbusyNum);
        pool->busyNum--;
        pthread_mutex_unlock(&pool->mutexbusyNum);
    }
}
//管理者线程
/*
主要是管理创建线程和销毁线程

*/
void *manager(void *args)
{
    ThreadPool *pool = (ThreadPool *)args;
    //只要线程池没关闭,那么管理者线程就一直工作
    while (!isShutDown(pool))
    {
        //自己定制的检查策略:我设置每个三秒检测
        sleep(3);

        //取出线程池任务的数量和消费者的工作线程数量
        pthread_mutex_lock(&pool->mutexpool);
        int queueSize = pool->queueSize;
        int liveNum = pool->liveNum;
        pthread_mutex_unlock(&pool->mutexpool);

        //获取忙的消费者线程数量
        pthread_mutex_lock(&pool->mutexbusyNum);
        int busyNum = pool->busyNum;
        pthread_mutex_unlock(&pool->mutexbusyNum);

        //开始管理线程
        // 1.添加消费者线程
        /*制定添加规则(也是自己设定的)
            任务的个数 > 存活的线程个数 && 存活的线程个数 < 最大的线程个数
        */
        if (queueSize > liveNum && liveNum < pool->maxNum)
        {
            pthread_mutex_lock(&pool->mutexpool); //这个锁主要是操作了liveNum这个资源

            int counter = 0; // counter表示要添加的消费者线程数量
            //遍历 消费者线程IDs数组,看看哪个位置可以放入新添加的线程
            int i = 0;
            for (; i < pool->maxNum &&
                   counter < WORK_THREAD_NUMBER &&
                   pool->liveNum < pool->maxNum;
                 i++)
            {
                //为0表示消费者线程数组的位置可以放入线程ID
                if (pool->threadIDs[i] == 0)
                {
                    pthread_create(&pool->threadIDs[i], NULL, worker, pool);
                    counter++;
                    liveNum++;
                }
            }
            pthread_mutex_unlock(&pool->mutexpool);
        }

        //由于线程过多,可能要进行销毁
        // 2. 销毁消费者线程
        /*
             销毁线程的策略:
              存活的线程数量>忙的线程数量*2 && 存活线程数量>最小线程数量
        */
        if (liveNum > busyNum * 2 && liveNum > pool->minNum)
        {
            pthread_mutex_lock(&pool->mutexpool);
            pool->exitNum = WORK_THREAD_NUMBER;
            pthread_mutex_unlock(&pool->mutexpool);

            //让工作者线程去自杀
            /*如何让他自杀呢?
              由于线程池有多余的消费者线程不工作
              我们可以通过唤醒消费者线程,让他去自己消亡
            */
            int i = 0;
            for (; i < WORK_THREAD_NUMBER; i++)
            {
                pthread_cond_signal(&pool->notEmpty);
            }
        }
    }
}

//线程退出函数
void threadExit(ThreadPool *pool)
{
    pthread_t tid = pthread_self();
    int i = 0;
    //遍历消费者线程的线程个数,找到退出线程的ID
    for (; i < pool->maxNum; i++)
    {
        if (pool->threadIDs[i] == tid)
        {
            pool->threadIDs[i] = 0;
            printf("threadExit()消费者线程 :%ld exit...\n", tid);
            break;
        }
    }
    pthread_exit(NULL);
}
static int taskQisFull(ThreadPool* pool)
{
    return pool->queueCapacity == pool->queueSize;
}
//给线程池添加任务
void threadPoolAdd(ThreadPool* pool,void(*function)(void*),void* args)
{
    pthread_mutex_lock(&pool->mutexpool); 
    //生产者线程:任务队列满要阻塞自己
    while(taskQisFull(pool) && !isShutDown(pool))
    {
        pthread_cond_wait(&pool->notFull,&pool->mutexpool);
    }
    if(isShutDown(pool))
    {
        pthread_mutex_unlock(&pool->mutexpool);
        return ;
    }

    //添加任务
    pool->taskQ[pool->queueRear].function = function;
    pool->taskQ[pool->queueRear].args = args;

    pool->queueRear++;
    pool->queueRear %= pool->queueCapacity;
    pool->queueSize++;
 
    pthread_mutex_unlock(&pool->mutexpool); 
    //唤醒work线程:
    pthread_cond_signal(&pool->notEmpty);
}

//获取线程池工作线程的个数
int threadBusyNum (ThreadPool* pool)
{
    pthread_mutex_lock(&pool->mutexbusyNum);
    int busyNum = pool->busyNum;
    pthread_mutex_unlock(&pool->mutexbusyNum);
    return busyNum;

}

//获取线程池存活的线程的个数
int threadLiveNum (ThreadPool* pool)
{
    pthread_mutex_lock(&pool->mutexpool);
    int liveNum = pool->liveNum;
    pthread_mutex_unlock(&pool->mutexpool);
    return liveNum;
}

//销毁线程池
int threadPoolDestroy(ThreadPool* pool)
{
    if(pool == NULL)
    {
        return -1;
    }
    //关闭线程池
    pool->shutdown = 1;


    //唤醒阻塞的消费者
    //存活的线程有多少就唤醒多少
    int i = 0;
    for(;i < pool->liveNum;i++)
    {
        pthread_cond_signal(&pool->notEmpty);
    }
    pthread_join(pool->managerID,NULL);

    //释放资源
    if(pool->taskQ )
        free(pool->taskQ);
    if(pool->threadIDs)
        free(pool->threadIDs);

    pthread_mutex_destroy(&pool->mutexbusyNum);
    pthread_mutex_destroy(&pool->mutexpool);
    pthread_cond_destroy(&pool->notFull);
    pthread_cond_destroy(&pool->notEmpty);

    free(pool);
    pool = NULL;

    return 0;

}

线程池测试代码

#include"thread_pool.h"

//任务处理函数
void taskFunction(void* args)
{
    int num = *(int*)args;
    printf("thread: %ld is working,number:%d\n",pthread_self(),num);
    sleep(1);
}
int main()
{
    //创建线程池
    ThreadPool* pool = threadPoolCreate(3,10,20);

    //往线程池里面放任务
    int i = 0;
    for(; i< 20; i++)
    {
        int *num = (int*)malloc(sizeof(int));
        *num = i+1;
        threadPoolAdd(pool,taskFunction,(void*)num);
    }

    sleep(10);

    threadPoolDestroy(pool);
    return 0;
}

测试线程池结果

由于我的测试代码:只搞了3个工作线程(消费者线程),任务队列大小为20,并且搞了20个任务队列进去,所以线程池就会有三个工作线程在抢夺任务工作!

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1784.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python文件的读取

python文件的读取1.文件的读取1.read() 读取整个文件2.readline() 每次读取一行文件3. readlines() 读取文件的所有行2.文件的写入1.以"x"方式打开文件2.以"a"方式打开文件3.以"w"方式打开文件3.文件的删除4.Excel表数据的读取1.直接读取2.通过p…

SQL Server2019配置always on高可用图文步骤

准备工作 首先需要准备好Windows Server上的故障转移群集&#xff0c;步骤可以参考上一篇。 https://blog.csdn.net/u012869793/article/details/127560270?spm1001.2014.3001.5501 然后服务器上安装好SqlServer&#xff0c;我这里安装的是2019。 正文 勾选启用Always ON可…

牛客竞赛每日俩题 - Day7

目录 经典01背包问题 二叉树遍历与构造&#xff08;考研重点&#xff09; 经典01背包问题 求正数数组的最小不可组成和_百度笔试题_牛客网 参考大佬题解&#xff1a; 动态规划&#xff1a;01背包问题(无物品价值)&#xff0c;思想相同&#xff0c;题目最终要求有些变化 min为…

【机器人定位引导中的机器视觉技术】

文章目录手眼标定原理手眼标定流程定位引导1、单相机抓取定位引导2、单相机纠偏定位引导3、上下相机对位引导随着工业生产中对自动化的要求越来越高&#xff0c;视觉技术已被广泛引入工业机器人行业&#xff0c;具备视觉的工业机器人能更快、更准、更灵活地完成定位抓取、对位组…

Linux系统 (三)- 权限介绍

~~~~前言命令行解释器 -- Command Line Interpreter ShellLinux操作系统命令行解释器对命令行解释器的初步认识命令行解释器的意义shell分类命令行解释器 CLI Shell图形界面 GUI ShellLinux权限Linux中用户分类su基本语法sudo基本语法配置操作权限管理权限是什么文件分类文件属…

【一起学数据结构与算法】计数排序、基数排序、桶排序(含菜鸟教程代码)

目录前言一、计数排序1.1 排序思想1.2 代码1.3 菜鸟教程官方代码(搬运)二、基数排序2.1 排序思想2.2 代码2.3 菜鸟教程官方代码(搬运)三、桶排序3.1 排序思想3.2 代码3.3 菜鸟教程官方代码(搬运)前言 之前我们学过了几种常见的排序&#xff0c;都是基于比较的排序&#xff0c;…

ES6中扩展对象的功能性

对象是JavaScript编程的核心&#xff0c;ECMAScript6为对象提供了许多简单易用且更加灵活的新特性。 ECMAScript 6在对象字面量的基础上做出了以下几个变更&#xff1a; 简化属性定义语法&#xff0c;使将当前作用域中的同名变量赋值给对象的语法更加简洁 function createPe…

【MySQL】MySQL基本操作详解

系列文章目录 第1篇&#xff1a;【MySQL】MySQL介绍及安装 第2篇&#xff1a;【MySQL】MySQL基本操作详解 文章目录 ✍1&#xff0c;数据库操作     &#x1f50d;1.1,查看数据库     &#x1f50d;1.2,创建数据库     &#x1f50d;1.3,选择数据库     &…

Kubernetes基础_02_Pod全解析

系列文章目录 文章目录系列文章目录前言一、Pod的生命周期Lifecycle二、Pod的重启策略RestartPolicy三、静态Pod四、Pod的健康检查总结前言 Pod是Kubernetes最小单位&#xff0c;当然一个Pod可以有多个Container&#xff0c;但是container是docker的元素&#xff0c;不是Kuber…

CTFHub | 布尔盲注

0x00 前言 CTFHub 专注网络安全、信息安全、白帽子技术的在线学习&#xff0c;实训平台。提供优质的赛事及学习服务&#xff0c;拥有完善的题目环境及配套 writeup &#xff0c;降低 CTF 学习入门门槛&#xff0c;快速帮助选手成长&#xff0c;跟随主流比赛潮流。 0x01 题目描述…

无限题库公众号系统搭建

无限题库公众号系统搭建 本平台优点&#xff1a; 多题库查题、独立后台、响应速度快、全网平台可查、功能最全&#xff01; 1.想要给自己的公众号获得查题接口&#xff0c;只需要两步&#xff01; 2.题库&#xff1a; 查题校园题库&#xff1a;查题校园题库后台&#xff08;…

《设计模式:可复用面向对象软件的基础》——行为模式(笔记)

文章目录五、行为模式5.1 CHAIN OF RESPONSIBILITY(职责链)1.意图2.动机3.适用性4.结构5.参与者6.协作7.效果8.实现9.代码示例10.相关模式5.2 COMMAND&#xff08;命令&#xff09;1.意图2.别名3.动机4.适用性5.结构6.参与者.7.协作8.效果9.实现10.代码示例11.相关模式5.3 INTE…

CSDN独家 | 全网首发 | Pytorch深度学习·理论篇(2023版)目录

很高兴和大家在这里分享我的最新专栏 Pytorch深度学习理论篇(2023版)&#xff0c;恭喜本博客浏览量达到两百万&#xff0c;CSDN内容合伙人&#xff0c;CSDN人工智能领域实力新星~ 0 Pytorch深度学习理论篇实战篇(2023版)大纲 1 Pytorch深度学习理论篇实战篇(2023版)专栏地址&…

嵌入式应用-详解移植并使用freetype显示文字

目录 前言 1. freetype和相关概念简介 2.freetype显示文字流程和主要函数 2.1 包含头文件及API头文件&#xff1a;ft2build.h 2.2 初始化&#xff1a; FT_InitFreetype 2.3 加载&#xff08;打开&#xff09;字体Face&#xff1a; FT_New_Face 2.4 设置字体大小&#x…

sqrt函数模拟实现的两种方法

起因:在leetcode刷题时&#xff0c;有一道题目考察了有关sqrt的原理的题目&#xff0c;当时就去查了网上的文章&#xff0c;结果发现&#xff0c;一开始的时候看的很懵&#xff0c;最后也是搞定了两种方法&#xff0c;今天我就以最简单的方式写下这两种方式的思路讲解&#xff…

Python批量获取高校基本信息

文章目录前言一、需求二、分析三、处理四、运行效果前言 为了更好的掌握数据处理的能力&#xff0c;因而开启Python网络爬虫系列小项目文章。 小项目小需求驱动&#xff0c;每篇文章会使用两种以上的方式&#xff08;Xpath、Bs4、PyQuery、正则等&#xff09;获取想要的数据。…

T31快启图像效果优化

T31快启图像效果优化 liwen01 20220821 (一)基础方法及概念 参考文档 《Ingenic_Zeratul_T31_快起效果调试说明_20200927_CN》 (1)起始EV参数 IPC 每次启动都是冷启动&#xff0c;画面有一个暗变亮的过程&#xff0c;称作为AE收敛过程(自动曝光收敛过程)。 为了加快AE收…

LeetCode50天刷题计划第二季(Day 23 — 重排链表(16.20- 17.00)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录前言一、题目重排链表示例提示二、思路三、代码前言 链表基础题加一 一、题目 重排链表 给定一个单链表 L 的头节点 head &#xff0c;单链表 L 表示为&#xff…

基于粒子群优化算法的时间调制非线性频偏FDA(Matlab代码实现)

&#x1f389;&#x1f389;&#x1f389;&#x1f389;欢迎您的到来&#x1f60a;&#x1f60a;&#x1f60a; &#x1f96c;博客主页&#xff1a;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 &#x1f4dd;床头铭&#xff1a;将来的我一定会感谢…