线程池(Linux +C)

参考  手写线程池 - C语言版 | 爱编程的大丙 (subingwen.cn)

目录

1.为什么需要线程池?

1)线程问题:

2)如何解决线程问题(线程池的优势):

2.线程池是什么?

1)线程的基本组成:

(1)任务队列(负责保存要处理的任务,并将任务交给工作线程去处理)

(2)工作者线程(任务队列的消费者、执行人员,动态创建N个)

(3)管理者线程(管理整个线程池,1个)

3.线程池怎么实现?(linux/C语言版本)

1)单个任务元素结构体

2)线程池结构体(任务队列+管理线程+工作线程)

3)API声明

4)创建线程池

(1)主要操作:

(2)说明:

5)工作线程执行函数worker

(1)主要操作:

(2)说明:

6)管理者任务

(1)主要操作:

(2)说明:

7)单线程退出函数

(1)主要操作:

(2)说明

8)线程池添加函数

(1)主要操作:

(2)说明:

9)获取线程池中工作的线程个数、存活的线程个数

10)线程销毁函数

(1)主要操作:

(2)说明:

4.线程池怎么用?


1.为什么需要线程池?

1)线程问题:

(1)如果只使用线程创建函数,在不断有新的任务进来的时候,需要不断的创建任务;任务在结束之后,为了避免占用资源,需要销毁线程任务。导致频繁操作,占用程序运行时间。

(2)在线程创建之后,如果任务已经运行完毕,线程不去销毁,则会白白浪费资源。

2)如何解决线程问题(线程池的优势):

(1)使用线程池之后,将任务和线程分离,建立一定数量的线程,使线程重复利用(不销毁),不断将任务添加到线程中。解决线程频繁创建、销毁问题。提供系统执行效率。

(2)根据任务数量增减,自动添加或者减少线程,使得线程维持在最优数量,节约系统资源。

2.线程池是什么?

1)线程的基本组成:

(1)任务队列(负责保存要处理的任务,并将任务交给工作线程去处理)

(a)通过线程池提供的api函数,将待处理的任务添加到任务队列,或者从任务队列删除;

(b)已经处理的任务会被从任务队列删除;

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

(2)工作者线程(任务队列的消费者、执行人员,动态创建N个)

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

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

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

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

(3)管理者线程(管理整个线程池,1个)

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

(b)根据任务数量的多少,增减线程数量;

3.线程池怎么实现?(linux/C语言版本)

1)单个任务元素结构体

// 任务结构体
typedef struct Task
{
    void (*function)(void* arg);
    void* arg;
}Task;

2)线程池结构体(任务队列+管理线程+工作线程)

// 线程池结构体
 struct ThreadPool
{
    Task* taskQ;        // 任务队列  后面利用堆新建数组
    int queueCapacity;  // 容量
    int queueSize;      // 当前任务个数
    int queueFront;     // 队头 -> 取数据
    int queueRear;      // 队尾 -> 放数据

    pthread_t managerID;    // 管理者线程ID
    pthread_t *threadIDs;   // 工作的线程ID   后面利用堆新建数组
    int minNum;             // 最小线程数量
    int maxNum;             // 最大线程数量
    int busyNum;            // 忙的线程的个数
    int liveNum;            // 存活的线程的个数
    int exitNum;            // 要销毁的线程个数
    pthread_mutex_t mutexPool;  // 锁整个的线程池
    pthread_mutex_t mutexBusy;  // 锁busyNum变量
    pthread_cond_t notFull;     // 任务队列是不是满了
    pthread_cond_t notEmpty;    // 任务队列是不是空了

    int shutdown;           // 是不是要销毁线程池, 销毁为1, 不销毁为0
};

3)API声明

typedef struct ThreadPool ThreadPool;

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

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

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

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

// 获取线程池中活着的线程的个数
int threadPoolAliveNum(ThreadPool* pool);

//
// 工作的线程(消费者线程)任务函数
void* worker(void* arg);
// 管理者线程任务函数
void* manager(void* arg);
// 单个线程退出
void threadExit(ThreadPool* pool);

4)创建线程池

(1)主要操作:

(a)创建线程池对象:ThreadPool* pool = (ThreadPool*)malloc(sizeof(ThreadPool));

(b)创建工作者线程队列(pthread_t):只是为了便于管理线程,并没有创建线程。

        pool->threadIDs = (pthread_t*)malloc(sizeof(pthread_t) * max);

(c)创建任务队列(task类型):pool->taskQ = (Task*)malloc(sizeof(Task) * queueSize);

(d)创建管理者线程:pthread_create(&pool->managerID, NULL, manager, pool);

(e)创建工作者线程:按照最小数量minNum进行for循环;
           pthread_create(&pool->threadIDs[i], NULL, worker, pool);

(f)初始化互斥量/条件变量:mutexPool、mutexBusy、notEmpty、notFull

(g)其余工作:初始化一些控制变量

(h)鲁棒性工作:检查指针,及时释放内存

(2)说明:

(a)为什么使用do while ?可以使用break,最后统一销毁资源;

(b)memset,后续根据指针是不是0 判断是不是有线程闲置

ThreadPool* threadPoolCreate(int min, int max, int queueSize)
{
    //实例化线程池对象
    ThreadPool* pool = (ThreadPool*)malloc(sizeof(ThreadPool));
    do 
    {
        //判断pool有没有指向有效内存
        if (pool == NULL)
        {
            printf("malloc threadpool fail...\n");
            break;
        }
        //创建工作者线程队列,按照最多线程数量 创建
        pool->threadIDs = (pthread_t*)malloc(sizeof(pthread_t) * max);
        //判断threadIDs有没有指向有效内存
        if (pool->threadIDs == NULL)
        {
            printf("malloc threadIDs fail...\n");
            break;
        }
        
        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->mutexBusy, NULL) != 0 ||
            pthread_cond_init(&pool->notEmpty, NULL) != 0 ||
            pthread_cond_init(&pool->notFull, NULL) != 0)
        {
            printf("mutex or condition init fail...\n");
            break;
        }
        创建任务队列
        pool->taskQ = (Task*)malloc(sizeof(Task) * queueSize);
        pool->queueCapacity = queueSize;
        pool->queueSize = 0;
        pool->queueFront = 0;
        pool->queueRear = 0;
        
        pool->shutdown = 0;

        //创建管理者线程
        pthread_create(&pool->managerID, NULL, manager, pool);
        //创建工作者线程
        for (int i = 0; i < min; ++i)
        {
            pthread_create(&pool->threadIDs[i], NULL, worker, pool);
        }
        return pool;
    } while (0);

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

    return NULL;
}

5)工作线程执行函数worker

(1)主要操作:

(a)判断任务队列是否为空。假如为空,阻塞线程。并进一步判断,是否要销毁线程池。

(b)如果任务队列不为空,取出任务,执行任务回调函数。

(2)说明:

(a)线程池属于公共资源,在访问线程池的时候要加锁;访问结束之后解锁。

(b)while (pool->queueSize == 0 && !pool->shutdown)  。在pthread_cond_wait这个地方,为什么采用while而不是if。关于该点,进行解释。

        首先,我们需要了解pthread_cond_wait的执行流程:

        -》pthread_cond_wait()函数会将当前线程挂起,使它处于休眠状态。
        -》在当前线程被挂起之前,函数会自动调用pthread_mutex_lock()函数将关联的互斥锁上锁,保证条件变量的独占访问。
        -》函数会将当前线程加入条件变量的等待队列中,并释放关联的互斥锁。
        -》当前线程进入休眠状态,等待其他线程在该条件变量上发出信号或广播。
        -》当另一个线程在该条件变量上发出信号或广播时,pthread_cond_wait()函数会唤醒一个等待在该条件变量上的线程。
        -》唤醒的线程重新获得关联的互斥锁的所有权,并继续执行。
        -》pthread_cond_wait()函数返回。

        其次,我们需要了解pthread_cond_signal的作用:至少唤醒一个线程。

        综合上面两点,设想以下场景。

初始临界变量x = 0;

线程1:如果x< 1,阻塞。否则,继续执行,并设置x = 0;

线程2:如果x< 1,阻塞。否则,继续执行,并设置x = 0;

线程3:设置x = 1,调用pthread_cond_signal唤醒线程。

        情况1,采用if语句:线程1收到了线程唤醒,线程2收到了线程唤醒,线程1抢到了互斥锁,他正常进行,并把x设置为0;然后线程2因为收到了线程唤醒,他也开始执行。问题出现了,线程1不应该执行因为他的判断条件是 x<1的情况下,应该进行阻塞。可是他却开始执行了。

        情况2,采用while语句:线程1收到了线程唤醒,线程2收到了线程唤醒,线程1抢到了互斥锁,因为之前被阻塞了,while语句不满足,他又检查了一次while条件,发现不用阻塞了,他正常进行,并把x设置为0。然后线程2因为收到了线程唤醒,他也开始执行。因为之前也被阻塞了,while语句不满足,他有检查一次条件,发现x被修改了,x=0,需要被阻塞了。所以他没法往下执行了。所以接着被阻塞了。这种情况,就可以保证,条件变量只能唤醒一个线程。防止“伪唤醒”。

        回到本例子中,情况一样。线程池会唤醒多个工作线程,让他们去执行任务。但是有可能任务数<线程数。如果使用while循环,就能保证每个任务对应一个线程。如果使用if判断,有可能唤醒多余的线程,并引发程序崩溃。

void* worker(void* arg)
{
    ThreadPool* pool = (ThreadPool*)arg;

    while (1)
    {
        pthread_mutex_lock(&pool->mutexPool);
        // 当前任务队列是否为空
        while (pool->queueSize == 0 && !pool->shutdown)
        {
            // 阻塞工作线程
            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 (pool->shutdown)
        {
            pthread_mutex_unlock(&pool->mutexPool);
            threadExit(pool);
        }

        // 从任务队列中取出一个任务
        Task task;
        task.function = pool->taskQ[pool->queueFront].function;
        task.arg = pool->taskQ[pool->queueFront].arg;
        // 移动头结点 数组变成了循环队列
        pool->queueFront = (pool->queueFront + 1) % pool->queueCapacity;
        pool->queueSize--;
        //告诉生产者,可以添加任务了
        pthread_cond_signal(&pool->notFull);
        //解锁
        pthread_mutex_unlock(&pool->mutexPool);
        //工作线程数量+1
        printf("thread %ld start working...\n", pthread_self());
        pthread_mutex_lock(&pool->mutexBusy);
        pool->busyNum++;
        pthread_mutex_unlock(&pool->mutexBusy);
        //函数任务调用
        task.function(task.arg);
        //释放函数堆内存
        free(task.arg);
        task.arg = NULL;
        //工作线程数量-1
        printf("thread %ld end working...\n", pthread_self());
        pthread_mutex_lock(&pool->mutexBusy);
        pool->busyNum--;
        pthread_mutex_unlock(&pool->mutexBusy);
    }
    return NULL;
}

6)管理者任务

(1)主要操作:

(a)添加任务线程;

(b)销毁任务线程;

(2)说明:

销毁任务线程的思路:发出条件变量信号,唤醒所有的工作线程;并将shutdown参数,告诉工作线程,现在要关闭。让所有的线程自杀。

void* manager(void* arg)
{
    ThreadPool* pool = (ThreadPool*)arg;
    while (!pool->shutdown)
    {
        // 每隔3s检测一次
        sleep(3);

        // 取出线程池中任务的数量和当前线程的数量
        pthread_mutex_lock(&pool->mutexPool);
        int queueSize = pool->queueSize;
        int liveNum = pool->liveNum;
        pthread_mutex_unlock(&pool->mutexPool);

        // 取出忙的线程的数量
        pthread_mutex_lock(&pool->mutexBusy);
        int busyNum = pool->busyNum;
        pthread_mutex_unlock(&pool->mutexBusy);

        // 添加线程
        // 任务的个数>存活的线程个数 && 存活的线程数<最大线程数
        if (queueSize > liveNum && liveNum < pool->maxNum)
        {
            pthread_mutex_lock(&pool->mutexPool);
            int counter = 0;
            for (int i = 0; i < pool->maxNum && counter < NUMBER
                && pool->liveNum < pool->maxNum; ++i)
            {
                if (pool->threadIDs[i] == 0)
                {
                    pthread_create(&pool->threadIDs[i], NULL, worker, pool);
                    counter++;
                    pool->liveNum++;
                }
            }
            pthread_mutex_unlock(&pool->mutexPool);
        }
        // 销毁线程
        // 忙的线程*2 < 存活的线程数 && 存活的线程>最小线程数
        if (busyNum * 2 < liveNum && liveNum > pool->minNum)
        {
            pthread_mutex_lock(&pool->mutexPool);
            pool->exitNum = NUMBER;
            pthread_mutex_unlock(&pool->mutexPool);
            // 让工作的线程自杀
            for (int i = 0; i < NUMBER; ++i)
            {
                pthread_cond_signal(&pool->notEmpty);
            }
        }
    }
    return NULL;
}

7)单线程退出函数

(1)主要操作:

前面判断线程是不是在工作,主要是判断线程ID是不是空。如果线程ID是空,表明为空线程;如果线程不为空,则为忙线程。因此,在退出删除线程的时候,需要修改线程队列中对应的线程ID,将其改为ID=0;便于后面的继续使用。

(2)说明

void threadExit(ThreadPool* pool)
{
    pthread_t tid = pthread_self();
    for (int i = 0; i < pool->maxNum; ++i)
    {
        if (pool->threadIDs[i] == tid)
        {
            pool->threadIDs[i] = 0;
            printf("threadExit() called, %ld exiting...\n", tid);
            break;
        }
    }
    pthread_exit(NULL);
}

8)线程池添加函数

(1)主要操作:

(a)如果线程池中的工作线程全部被占用,阻塞添加任务,即阻塞生产者(该线程就是调用线程池的线程,一般是主线程);
(b)如果工作线程释放了notFull条件变量,则说明工作线程有空余,唤醒线程添加函数。

(2)说明:

void threadPoolAdd(ThreadPool* pool, void(*func)(void*), void* arg)
{
    pthread_mutex_lock(&pool->mutexPool);
    while (pool->queueSize == pool->queueCapacity && !pool->shutdown)
    {
        // 阻塞生产者线程
        pthread_cond_wait(&pool->notFull, &pool->mutexPool);
    }
    if (pool->shutdown)
    {
        pthread_mutex_unlock(&pool->mutexPool);
        return;
    }
    // 添加任务
    pool->taskQ[pool->queueRear].function = func;
    pool->taskQ[pool->queueRear].arg = arg;
    pool->queueRear = (pool->queueRear + 1) % pool->queueCapacity;
    pool->queueSize++;

    pthread_cond_signal(&pool->notEmpty);
    pthread_mutex_unlock(&pool->mutexPool);
}

9)获取线程池中工作的线程个数、存活的线程个数

int threadPoolBusyNum(ThreadPool* pool)
{
    pthread_mutex_lock(&pool->mutexBusy);
    int busyNum = pool->busyNum;
    pthread_mutex_unlock(&pool->mutexBusy);
    return busyNum;
}

int threadPoolAliveNum(ThreadPool* pool)
{
    pthread_mutex_lock(&pool->mutexPool);
    int aliveNum = pool->liveNum;
    pthread_mutex_unlock(&pool->mutexPool);
    return aliveNum;
}

10)线程销毁函数

(1)主要操作:

        赋值操作pool->shutdown = 1;此时管理者线程就会检测到,退出while循环;

        阻塞回收管理者线程。pthread_join(pool->managerID, NULL);

        唤醒所有的消费者线程。让消费者线程自杀。

        释放线程池中的堆内存。

        销毁互斥锁和条件变量。

(2)说明:

        (a)关于pthread_join:当A线程调用线程B并 pthread_join() 时,A线程会处于阻塞状态,直到B线程结束后,A线程才会继续执行下去。当 pthread_join() 函数返回后,被调用线程才算真正意义上的结束,它的内存空间也会被释放(如果被调用线程是非分离的)。

        (b)管理者线程采用pthread_join;消费者线程采用 pthread_exit。之所以消费者线程自动退出的目的是,不让管理者线程承担过多的工作,管理者线程应该只是负责统一的管理,

int threadPoolDestroy(ThreadPool* pool)
{
    if (pool == NULL)
    {
        return -1;
    }

    // 关闭线程池
    pool->shutdown = 1;
    // 阻塞回收管理者线程
    pthread_join(pool->managerID, NULL);
    // 唤醒阻塞的消费者线程
    for (int i = 0; i < pool->liveNum; ++i)
    {
        pthread_cond_signal(&pool->notEmpty);
    }
    // 释放堆内存
    if (pool->taskQ)
    {
        free(pool->taskQ);
    }
    if (pool->threadIDs)
    {
        free(pool->threadIDs);
    }

    pthread_mutex_destroy(&pool->mutexPool);
    pthread_mutex_destroy(&pool->mutexBusy);
    pthread_cond_destroy(&pool->notEmpty);
    pthread_cond_destroy(&pool->notFull);

    free(pool);
    pool = NULL;

    return 0;
}

4.线程池怎么用?

#include "threadpool.h"
#include "stdio.h"
#include "pthread.h"
#include "unistd.h"
#include "malloc.h"


void taskFunc(void* arg)
{
    int num = *(int*)arg;
    printf("thread %ld is working, number = %d\n",
        pthread_self(), num);
    sleep(1);
}

int main()
{
    // 创建线程池
    ThreadPool* pool = threadPoolCreate(3, 10, 100);
    for (int i = 0; i < 100; ++i)
    {
        int* num = (int*)malloc(sizeof(int));
        *num = i + 100;
        threadPoolAdd(pool, taskFunc, num);
    }

    sleep(30);

    threadPoolDestroy(pool);
    return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

无公网IP环境Windows系统使用VNC远程连接Deepin桌面

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f3a5;系列专栏&#xff1a;《C语言》 《数据结构》 《Linux》《Cpolar》 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;…

Hadoop学习笔记(HDP)-Part.19 安装Kafka

目录 Part.01 关于HDP Part.02 核心组件原理 Part.03 资源规划 Part.04 基础环境配置 Part.05 Yum源配置 Part.06 安装OracleJDK Part.07 安装MySQL Part.08 部署Ambari集群 Part.09 安装OpenLDAP Part.10 创建集群 Part.11 安装Kerberos Part.12 安装HDFS Part.13 安装Ranger …

使用pandas制作图表

数据可视化对于数据分析的重要性不言而喻&#xff0c;一个优秀的图表有足以一眼就看出关键所在。pandas利用matplotlib实现绘图。能够提供各种各样的图表功能&#xff0c;包括: 单折线图多折线图柱状图叠加柱状图水平叠加柱状图直方图拆分直方图箱型图区域块图形散点图饼图多子…

Linux AMH服务器管理面板本地安装与远程访问

最近&#xff0c;我发现了一个超级强大的人工智能学习网站。它以通俗易懂的方式呈现复杂的概念&#xff0c;而且内容风趣幽默。我觉得它对大家可能会有所帮助&#xff0c;所以我在此分享。点击这里跳转到网站。 文章目录 1. Linux 安装AMH 面板2. 本地访问AMH 面板3. Linux安装…

MySQL老是卸载不干净,不会删除注册表,安装总是报错

给大家推荐一款非常使用的工具 geek点击官网下载。 安装完成主页就长这样&#xff1a; 右键点击你要删除的MySQL卸载即可。自动帮你清空注册表等信息。 谁用谁知道&#xff01;&#xff01;&#xff01; 用了感觉不错的话记得回来给我点赞加评论哦&#xff01;&#xff01;&…

科普小知识-3D 打印是什么?

3D 打印是什么&#xff1f;作为近年来备受关注的前沿科技&#xff0c;3D 打印技术正在不断改变着制造业、医疗领域、艺术设计等多个领域的面貌。其又被称为增材制造&#xff0c;是一种通过电脑设计&#xff0c;逐层堆叠材料来创建三维物体的技术。 3D 打印的基本原理 3D 打印…

数据库的索引

索引的特点 1&#xff09;加快查询的速度 2&#xff09;索引自身是一种数据结构&#xff0c;也要占用存储空间 3&#xff09;当我们需要进行增删改的时候&#xff0c;也要对索引进行更新&#xff08;也需要额外的空间开销&#xff09; sql操作 查看索引 show index from …

安装postgresql驱动及python使用pyodbc指定postgresql驱动调用postgresql

注&#xff1a;Python解释器版本(32位/64位)和postgresql驱动版本(32位/64位)需一致。 一、安装postgresql驱动 https://www.postgresql.org/ftp/odbc/versions/msi/ &#xff08;1&#xff09;32位&#xff1a; &#xff08;2&#xff09;64位&#xff1a; 双击安装。全程默…

如何让软文更具画面感,媒介盒子分享

写软文这种带有销售性质的文案时&#xff0c;总说要有画面感&#xff0c;要有想象空间。只有针对目标用户的感受的设计&#xff0c;要了解用户想的是什么&#xff0c;要用可视化的描述来影响用户的感受&#xff0c;今天媒介盒子就和大家分享&#xff1a;如何让软文更具画面感。…

axios调接口传参特殊字符丢失的问题(encodeURI 和 encodeURIComponent)

1、axios调接口特殊字符丢失的问题 项目开发过程中遇到一个接口传参&#xff0c;参数带特殊字符&#xff0c;axios调接口特殊字符丢失的问题 例如接口&#xff1a; get/user/detail/{name} name是个参数直接调接口的时候拼到接口上&#xff0c;get/user/detail/test123#$%&am…

IntelliJ IDEA图形安装教程

IntelliJ IDEA图形安装教程 之前开始Java程序&#xff0c;一直用的eclipse&#xff0c;觉得还可以。一直听说IntelliJ IDEA比eclipse好用很多&#xff0c;但因为比较懒&#xff0c;也没有学习使用。机缘巧合下&#xff0c;尝试用了下&#xff0c;顿时有种相见恨晚的感觉&#…

如何核销百川云网站监测兑换码

注册/登录百川云平台 在电脑端输入百川云网址“https://rivers.chaitin.cn/“&#xff0c;会出现以下界面&#xff1a; 2.点击右上角“立即注册”&#xff0c;使用微信扫一扫注册&#xff0c;注册成功之后&#xff0c;系统会自动跳转到百川云工作台。 免费开通“百川云网站监测…

什么是HTTPS加密协议? ️

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

FreeRTOS系统延时函数分析

一、概述 FreeRTOS提供了两个系统延时函数&#xff0c;相对延时函数vTaskDelay()和绝对延时函数vTaskDelayUntil()。相对延时是指每次延时都是从任务执行函数vTaskDelay()开始&#xff0c;延时指定的时间结束&#xff0c;绝对延时是指每隔指定的时间&#xff0c;执行一次调用vT…

水声功率放大器是什么(驱动水声换能器的原理是什么)

水声功率放大器是一种用于增大水声信号的电子设备。它是水声系统中的关键部件&#xff0c;通常用于驱动水声换能器&#xff0c;将低功率的电信号转换为高功率的水声信号。 水声功率放大器是声呐发射机的重要组成部分&#xff0c;用来提高输出功率,驱动换能器向水中辐射足够能量…

项目管理:为什么项目计划必不可少

项目管理计划定义了如何执行、监督和控制项目。项目计划让我们准确地知道在项目的每个阶段应该做什么&#xff0c;在哪里分配资源和时间&#xff0c;以及在事情超出计划或超出预算时要注意什么。 为了项目中获得成功&#xff0c;管理者需要在前期创建一个项目计划&#xff0c…

高效率:使用DBeaver连接spark-sql

提高运行效率一般采取底层使用spark引擎替换成hive引擎的方式提高效率&#xff0c;但替换引擎配置较为复杂考虑到兼容版本且容易出错&#xff0c;所以本篇将介绍使用DBeaver直接连接spark-sql快速操作hive数据库。 在spark目录下运行以下命令&#xff0c;创建一个SparkThirdSe…

免费通配符和免费多域名证书

免费通配符证书&#xff0c;其特点在于能够为一个主域名及其所有子域名提供加密保护。通常&#xff0c;通配符证书的主域名会以通配符&#xff08;*&#xff09;表示&#xff0c;比如*.example.com&#xff0c;这样就覆盖了blog.example.com、api.example.com等所有子域名。 免…

【高数:1 映射与函数】

【高数&#xff1a;1 映射与函数】 例2.1 绝对值函数例2.2 符号函数例2.3 反函数表示例2.4 双曲正弦sinh&#xff0c;双曲余弦cosh&#xff0c;双曲正切tanh 参考书籍&#xff1a;毕文斌, 毛悦悦. Python漫游数学王国[M]. 北京&#xff1a;清华大学出版社&#xff0c;2022. 例2…

【SQL开发实战技巧】系列(四十九):Oracle12C常用新特性☞表分区部分索引(Partial Indexes)

系列文章目录 【SQL开发实战技巧】系列&#xff08;一&#xff09;:关于SQL不得不说的那些事 【SQL开发实战技巧】系列&#xff08;二&#xff09;&#xff1a;简单单表查询 【SQL开发实战技巧】系列&#xff08;三&#xff09;&#xff1a;SQL排序的那些事 【SQL开发实战技巧…