线程池(Linux +C)

news2025/2/22 20:26:52

参考  手写线程池 - 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;
}

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

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

相关文章

某夕夕商家告诉你:这样寄快递居然这么省钱(便宜寄全国)

在当下很多时候寄快递成为了困扰很多人的问题&#xff0c;比如很多时候都会面临运费贵的问题&#xff0c;而且寄快递的效率也得不到保障&#xff0c;即使投诉快递员最终也是无济于事。其实在目前来看寄快递并没有这么难&#xff0c;闪侠惠递就能够有效的寄快递&#xff0c;而且…

试验数字化平台WDP 助力车企数据管理加速度

一 现状 随着现代测控技术的提高&#xff0c;数据结构变得越来越复杂多样&#xff0c;数据量也在日益增大。又因试验条件的限制&#xff0c;大多数企业的数据管理方式主要是通过各类电子文档将试验数据保存在每个工程师的移动电脑中&#xff0c;再进行汇总存储和共享。这种落后…

OpenHarmony 设备启动Logo和启动视频替换指南

前言 OpenHarmony源码版本&#xff1a;4.0release 开发板&#xff1a;DAYU / rk3568 一、Logo替换 替换其中的logo.bmp 和 logo_kernel.bmp文件 注意事项&#xff1a; 1、图片的分辨率需要和设备匹配 2、如果是非首次编译&#xff08;存在缓存&#xff09;需要将out目录删…

【Backbone】TransNeXt:最新ViT模型(原理+常用神经网络汇总)

文章目录 一、近几年神经网络 Backbone 回顾1.Densenet 与 Resnet2.CBP3.SENet4.GCNet5.DANet6.PANet 与 FPN7.ASPP8.SPP-net9.PSP-net10.ECA-Net 二、TransNeXt&#xff08;2023&#xff09;1.提出问题2.Aggregated Pixel-focused Attention2.1 Pixel-focused Attention&#…

如何一个月内发表一篇中文核心 干货分享

发论文经验教学 干货分享&#xff1a;如何在一个月内发表一篇中文核心 经验分享 干货分享_哔哩哔哩_bilibili

元宇宙红色展厅VR虚拟展馆提高受训者的参与感

生活在和平年代的新一代青少年&#xff0c;可能对革命先烈英勇事迹难以有很深的体会&#xff0c;无法切实感受到中国共产党无畏牺牲、誓死保家卫国的红色精神&#xff0c;因此借助VR虚拟现实制作技术&#xff0c;让参观者们走近革命先烈中&#xff0c;感受老一辈无产阶级革命家…

C语言-字符串变量

字符串变量 char* s “Hello, world!”&#xff1b; s是一个指针&#xff0c;初始化为指向一个字符串常量 由于这个常量所在的地方&#xff0c;所以实际上s是const char* s&#xff0c;但是由于历史的原因&#xff0c;编译器接受不带const的写法但是试图对s所指的字符串做写…

龙迅#LT8311X3 USB中继器应用描述!

1. 概述 LT8311X3是一款USB 2.0高速信号中继器&#xff0c;用于补偿ISI引起的高速信号衰减。通过外部下拉电阻器选择的编程补偿增益有助于提高 USB 2.0 高速信号质量并通过 CTS 测试。 2. 特点 • 兼容 USB 2.0、OTG 2.0 和 BC 1.2• 支持 HS、FS、LS 信令 • 自动检测和补偿 U…

【MATLAB源码-第95期】基于matlab的协作通信中(AF模式)中继选择算法对比。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 1. 最大最小中继选择 (Max-Min Relay Selection)&#xff1a;这种算法选择能够提供最大最小信号强度的中继。它首先计算所有可用中继的信号强度&#xff0c;然后选择那些在最差信道条件下仍能保持最高信号强度的中继。其目的…

Vue3引入markdown编辑器--Bytemd

字节跳动开源了一款markdown编辑器&#xff0c;bytemd&#xff0c;项目地址&#xff1a;GitHub - bytedance/bytemd: ByteMD v1 repository 安装 npm i bytemd/vue-next 引入方式如下&#xff0c;再main.js中引入样式 import bytemd/dist/index.css 直接封装一个Markdown编…

1.vue学习笔记(vue简介+API风格+开发前的准备)

1.介绍 1.一款用于构建用户页面的JavaScript框架 2.基于HTML、CSS、JavaScript 3.官方文档&#xff1a;cn.vuejs.org2.渐进式框架 1.注重灵活性/可被逐步集成 根据需求场景&#xff1a;1.无需构建步骤&#xff0c;渐进式增强静态的HTML2.在任何页面中作为Web Components嵌入&…

生命在于折腾——Android Studio网络设置(MAC)

一、前言 在macos上面&#xff0c;能使用的android模拟器是真不如windows多&#xff0c;各个厂家似乎抛弃了macos的安卓模拟器&#xff0c;当然&#xff0c;我使用的mac是2019款16寸的inter芯片&#xff0c;之前使用arm架构M2芯片的时候&#xff0c;更是可怕&#xff0c;不过q…

博客文章SEO:提升博客排名和吸引更多读者的方法来啦!

互联网发展到现在&#xff0c;搜索引擎优化&#xff08;SEO&#xff09;一直发挥着不可替代的作用。搜索引擎的流量往往更加定向&#xff0c;来自搜索引擎的流量转化率更高&#xff0c;可以帮助企业更好地实现销售和推广目标。因此&#xff0c;通过合理的SEO策略&#xff0c;你…

【FreeRTOS】消息队列——简介、常用API函数、注意事项、项目实现

在嵌入式系统开发中&#xff0c;任务间的通信是非常常见的需求。FreeRTOS提供了多种任务间通信的机制&#xff0c;其中之一就是消息队列。消息队列是一种非常灵活和高效的方式&#xff0c;用于在不同的任务之间传递数据。通过消息队列&#xff0c;任务可以异步地发送和接收消息…

泛型和Object的区别

什么时候使用 泛型&#xff1a;只要确定了用哪类对象&#xff0c;并且用到这个对象里的方法。选择泛型&#xff0c;泛型更加精确&#xff0c;只要用到Object的地方基本都能用泛型代替。Object类&#xff1a;Object是所有类的父类&#xff0c;更加笼统&#xff0c;且只能使用固…

找不到客户?交你一招

在当今社会中&#xff0c;获取潜在客户的信息对于企业的成功至关重要。然而&#xff0c;许多企业却面临着找不到潜在客户的难题。 企业想要成交的第一步就是寻找客户&#xff0c;将产品和目标客户进行匹配才能提高成交率。 因此企业首先需要明确自己的目标客户群体&#xff0…

GAN:WGAN-DIV

论文&#xff1a;https://arxiv.org/pdf/1712.01026.pdf 代码&#xff1a; 发表&#xff1a;2018 摘要 在计算机视觉的许多领域中&#xff0c;生成对抗性网络已经取得了巨大的成功&#xff0c;其中WGANs系列被认为是最先进的&#xff0c;主要是由于其理论贡献和竞争的定性表…

Leetcode—538.把二叉搜索树转换为累加树【中等】

2023每日刷题&#xff08;四十九&#xff09; Leetcode—538.把二叉搜索树转换为累加树 实现代码 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(…

Node包管理工具 - nvm、npm、yarn、cnpm、pnpm

转载说明 原文地址 简介 nvm : 可以实现一台电脑&#xff0c;拥有多个版本的Node npm : node package manager 下载Node后自带的一个包管理工具 yarn : npm 的升级版&#xff0c;更优秀 cnpm : 配置下载非官方地址的依赖&#xff08;淘宝、华为、腾讯镜像&#xff09; pnpm :…

Java jdk和jre的区别

jdk和jre的区别为&#xff1a;JRE是java运行时环境而JDK是java开发工具包&#xff0c;JDK包含JRE&#xff0c;但是JRE可以独立安装。 JDK&#xff1a; java development kit (java开发工具)&#xff0c;JDK 是用于开发 Java 程序的最小环境。 JRE&#xff1a; java runtime e…