【Linux】多线程 -> 线程同步与基于BlockingQueue的生产者消费者模型

线程同步

条件变量

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。

例如:一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

同步概念

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题。

竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解。

条件变量函数

初始化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict

attr);

参数:

cond:要初始化的条件变量

attr:NULL

销毁

int pthread_cond_destroy(pthread_cond_t *cond);

等待条件满足

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

参数:

cond:要在这个条件变量上等待

mutex:互斥量,后面详细解释

唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒一批线程。

int pthread_cond_signal(pthread_cond_t *cond);//唤醒一个线程。

示例代码:

makefile:

testCond:testCond.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f testCond

testCond.cc:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>

int tickets = 1000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void *start_routine(void *args)
{
    std::string name = static_cast<const char *>(args);
    while (true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex);
        // 判断暂时省略
        std::cout << name << "->" << tickets << std::endl;
        tickets--;
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    // 通过条件变量控制线程的执行
    pthread_t t[4];
    for (int i = 0; i < 4; i++)
    {
        char *name = new char[64];
        snprintf(name, 64, "thread %d", i + 1);
        pthread_create(t + i, nullptr, start_routine, (void *)name);
    }
    while (true)
    {
        sleep(1);
        //pthread_cond_broadcast(&cond); // 唤醒一批线程
        pthread_cond_signal(&cond);//唤醒一个线程
        std::cout << "main thread wakeup one thread... " << std::endl;
    }
    for (const auto &i : t)
    {
        pthread_join(i, nullptr);
    }

    return 0;
}

pthread_cond_signal:唤醒一个线程。      pthread_cond_broadcast:唤醒一批线程。

这些线程会持续等待一个条件变量的信号。主线程每隔 1 秒就会发送一个条件变量信号,唤醒其中一个等待的线程。被唤醒的线程会输出当前剩余的票数并将票数减 1。

可以看到,由于条件变量的存在,输出结果变得有顺序性。

  • 为什么 pthread_cond_wait 需要互斥量?

1. 保证条件检查和等待操作的原子性

在多线程环境中,线程需要先检查某个条件是否满足,如果不满足则进入等待状态。这个检查条件和进入等待的操作必须是原子的,否则可能会出现竞态条件

例如,在生产者 - 消费者模型中,消费者线程需要检查缓冲区是否为空,如果为空则等待。假设没有互斥量保护,可能会出现以下情况:

  • 消费者线程检查到缓冲区为空,准备进入等待状态。

  • 在消费者线程真正进入等待状态之前,生产者线程往缓冲区中添加了数据,并发出了条件变量的通知。

  • 消费者线程此时才进入等待状态,由于之前通知已经发出,消费者线程可能会一直等待下去,导致程序出现错误。

使用互斥量可以保证条件检查和进入等待状态这两个操作的原子性。当线程调用pthread_cond_wait时,它会先释放互斥量,然后进入等待状态;当被唤醒时,又会重新获取互斥量。这样就避免了上述竞态条件的发生。

2. 保护共享资源和条件变量

条件变量通常与共享资源相关联,线程在检查条件和修改共享资源时需要保证线程安全。互斥量可以用来保护这些共享资源,确保同一时间只有一个线程能够访问和修改它们

在调用pthread_cond_wait之前,线程需要先获取互斥量,这样可以保证在检查条件和进入等待状态时,其他线程不会同时修改共享资源和条件变量。当线程被唤醒后,再次获取互斥量,又可以保证在处理共享资源时的线程安全

生产者消费者模型

  • 为何要使用生产者消费者模型?

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

“321”原则(便于记忆)

  • 3种关系:生产者和生产者(互斥关系)、消费者和消费者(互斥关系)、生产者和消费者(互斥、同步)。
  • 2种角色:生产者线程和消费者线程。
  • 1种交易场所:一段特定结构的缓冲区。

优点

  1. 解耦:生产线程和消费线程解耦。
  2. 支持忙闲不均:生产和消费的一段时间的忙闲不均。
  3. 提高效率:支持并发。

基于BlockingQueue的生产消费模型

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)。

下面我们以单生产者,单消费者为例:

makefile:

MainCp:MainCp.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f MainCp

BlockQueue.hpp:

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

const int gmaxcap = 5;

template <class T>
class BlockQueue
{
public:
    BlockQueue(const int &maxcap = gmaxcap) : _maxcap(maxcap)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_pcond, nullptr);
        pthread_cond_init(&_ccond, nullptr);
    }
    void push(const T &in) // 输入型参数,const &;输出型参数 *;输入输出型参数 &;
    {
        pthread_mutex_lock(&_mutex);
        // 1.判断
        while (is_full())
        // if(is_full())//细节2:充当判断的语法必须是while,不能是if,因为在被唤醒时,有可能存在异常或伪唤醒。
        {
            // 细节1:pthread_cond_wait是在临界区啊。
            // pthread_cond_wait的第二个参数,必须是我们正在使用的互斥锁。
            // a.pthread_cond_wait:该函数调用的时候,会以原子性的方式,将锁释放,并将自己挂起。
            // b.pthread_cond_wait: 该函数在被唤醒返回的时候,会自动的重新获取你传入的锁。
            pthread_cond_wait(&_pcond, &_mutex); // 因为生产条件不满足,无法生产,生产者进行等待。
        }
        // 2.走到这里,一定是没有满的。
        _q.push(in);
        // 3.一定能保证阻塞队列里有数据。
        // 细节3:pthread_cond_signal:可以放在临界区内部,也可以放在外部。
        pthread_cond_signal(&_ccond); // 唤醒消费者消费。这里可以有一定的策略。
        pthread_mutex_unlock(&_mutex);
        // pthread_cond_siganl(&_ccond);
    }
    void pop(T *out)
    {
        pthread_mutex_lock(&_mutex);
        // 1.判断
        while (is_empty())
        // if(is_empty())
        {
            pthread_cond_wait(&_ccond, &_mutex); // 因为消费条件不满足,无法消费,消费者进行等待。
        }
        // 2.走到这里,一定是不为空的。
        *out = _q.front();
        _q.pop();
        // 3.一定能保证阻塞队列里至少有一个空位置。
        pthread_cond_signal(&_pcond); // 唤醒生产者生产。这里可以有一定的策略。
        pthread_mutex_unlock(&_mutex);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_pcond);
        pthread_cond_destroy(&_ccond);
    }

private:
    bool is_empty()
    {
        return _q.empty();
    }
    bool is_full()
    {
        return _q.size() == _maxcap;
    }

private:
    std::queue<T> _q;
    int _maxcap; // 队列中元素的上限
    pthread_mutex_t _mutex;
    pthread_cond_t _pcond; // 生产者对应的条件变量
    pthread_cond_t _ccond; // 消费者对应的条件变量
};

 MainCp.cc:

#include "BlockQueue.hpp"
#include <ctime>
#include <sys/types.h>
#include <unistd.h>

void *consumer(void *bq_)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(bq_);
    while (true)
    {
        // 生产活动
        int data;
        bq->pop(&data);
        std::cout << "消费数据:" << data << std::endl;
    }
    return nullptr;
}
void *productor(void *bq_)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(bq_);
    while (true)
    {
        // 生产活动
        int data = rand() % 10 + 1; // 这里我们先用一个随机数构建一个数据。
        bq->push(data);
        std::cout << "生产数据:" << data << std::endl;
    }
    return nullptr;
}

int main()
{
    srand((unsigned long)time(nullptr) ^ getpid());
    BlockQueue<int> *bq = new BlockQueue<int>();
    pthread_t c, p;
    // 生产消费要看到同一份资源,也就是阻塞队列
    pthread_create(&c, nullptr, consumer, bq);
    pthread_create(&p, nullptr, productor, bq);

    pthread_join(c, nullptr);
    pthread_join(p, nullptr);

    delete bq;

    return 0;
}

如果不加控制,生产消费就会疯狂的执行,没有顺序。

  • 你怎么证明它是一个阻塞队列呢?

让生产者每隔一秒生产一个,消费者一直消费。那么最终的预期结果就是生产一个,消费一个;生产一个,消费一个。

void *consumer(void *bq_)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(bq_);
    while (true)
    {
        // 生产活动
        int data;
        bq->pop(&data);
        std::cout << "消费数据:" << data << std::endl;
    }
    return nullptr;
}
void *productor(void *bq_)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(bq_);
    while (true)
    {
        // 生产活动
        int data = rand() % 10 + 1; // 这里我们先用一个随机数构建一个数据。
        bq->push(data);
        std::cout << "生产数据:" << data << std::endl;
        sleep(1);
    }
    return nullptr;
}

让消费者每隔一秒消费一个,生产者一直生产。那么最终的预期结果就是消费一个,生产一个;消费一个,生产一个。

void *consumer(void *bq_)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(bq_);
    while (true)
    {
        // 生产活动
        int data;
        bq->pop(&data);
        std::cout << "消费数据:" << data << std::endl;
        sleep(1);
    }
    return nullptr;
}
void *productor(void *bq_)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(bq_);
    while (true)
    {
        // 生产活动
        int data = rand() % 10 + 1; // 这里我们先用一个随机数构建一个数据。
        bq->push(data);
        std::cout << "生产数据:" << data << std::endl;
    }
    return nullptr;
}

这就是基于阻塞队列的生产消费模型。

上面我们阻塞队列里放的就是一个整形数据,我们可以再完善一下。我们是可以直接在阻塞队列中放任务的。让生产者给消费者派发任务。

makefile:

MainCp:MainCp.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f MainCp

BlockQueue.hpp:

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

const int gmaxcap = 5;

template <class T>
class BlockQueue
{
public:
    BlockQueue(const int &maxcap = gmaxcap) : _maxcap(maxcap)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_pcond, nullptr);
        pthread_cond_init(&_ccond, nullptr);
    }
    void push(const T &in) // 输入型参数,const &;输出型参数 *;输入输出型参数 &;
    {
        pthread_mutex_lock(&_mutex);
        // 1.判断
        while (is_full())
        // if(is_full())//细节2:充当判断的语法必须是while,不能是if,因为在被唤醒时,有可能存在异常或伪唤醒。eg:一个生产者十个消费者,broadcast唤醒。
        {
            // 细节1:pthread_cond_wait是在临界区啊。
            // pthread_cond_wait的第二个参数,必须是我们正在使用的互斥锁。
            // a.pthread_cond_wait:该函数调用的时候,会以原子性的方式,将锁释放,并将自己挂起。
            // b.pthread_cond_wait: 该函数在被唤醒返回的时候,会自动的重新获取你传入的锁。
            pthread_cond_wait(&_pcond, &_mutex); // 因为生产条件不满足,无法生产,生产者进行等待。
        }
        // 2.走到这里,一定是没有满的。
        _q.push(in);
        // 3.一定能保证阻塞队列里有数据。
        // 细节3:pthread_cond_signal:可以放在临界区内部,也可以放在外部。
        pthread_cond_signal(&_ccond); // 唤醒消费者消费。这里可以有一定的策略。
        pthread_mutex_unlock(&_mutex);
        // pthread_cond_siganl(&_ccond);
    }
    void pop(T *out)
    {
        pthread_mutex_lock(&_mutex);
        // 1.判断
        while (is_empty())
        // if(is_empty())
        {
            pthread_cond_wait(&_ccond, &_mutex); // 因为消费条件不满足,无法消费,消费者进行等待。
        }
        // 2.走到这里,一定是不为空的。
        *out = _q.front();
        _q.pop();
        // 3.一定能保证阻塞队列里至少有一个空位置。
        pthread_cond_signal(&_pcond); // 唤醒生产者生产。这里可以有一定的策略。
        pthread_mutex_unlock(&_mutex);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_pcond);
        pthread_cond_destroy(&_ccond);
    }

private:
    bool is_empty()
    {
        return _q.empty();
    }
    bool is_full()
    {
        return _q.size() == _maxcap;
    }

private:
    std::queue<T> _q;
    int _maxcap; // 队列中元素的上限
    pthread_mutex_t _mutex;
    pthread_cond_t _pcond; // 生产者对应的条件变量
    pthread_cond_t _ccond; // 消费者对应的条件变量
};

Task.hpp:

#pragma once

#include <iostream>
#include <cstdio>
#include <functional>

class Task
{
    using func_t = std::function<int(int, int, char)>;
    // typedef std::function<int(int,int,char)>func_t;
public:
    Task()
    {
    }
    Task(int x, int y, char op, func_t func)
        : _x(x), _y(y), _op(op), _callback(func)
    {
    }
    std::string operator()()
    {
        int result = _callback(_x, _y, _op);
        char buffer[1024];
        snprintf(buffer, sizeof buffer, "%d %c %d = %d", _x, _op, _y, result);
        return buffer;
    }
    std::string toTaskString()
    {
        char buffer[1024];
        snprintf(buffer, sizeof buffer, "%d %c %d = ?", _x, _op, _y);
        return buffer;
    }

private:
    int _x;
    int _y;
    char _op;
    func_t _callback;
};

MainCp.cc:

#include "BlockQueue.hpp"
#include "Task.hpp"
#include <ctime>
#include <sys/types.h>
#include <unistd.h>

const std::string oper = "+-*/%";

int mymath(int x, int y, char op)
{
    int result = 0;
    switch (op)
    {
    case '+':
        result = x + y;
        break;
    case '-':
        result = x - y;
        break;
    case '*':
        result = x * y;
        break;
    case '/':
        {
            if (y == 0)
            {
                std::cerr << "div zero error!" << std::endl;
                result = -1;
            }
            else
            {
                result = x / y;
            }
        }
        break;
    case '%':
        {
            if (y == 0)
            {
                std::cerr << "mod zero error!" << std::endl;
                result = -1;
            }
            else
            {
                result = x % y;
            }
        }
        break;
    }
    return result;
}

void *consumer(void *bq_)
{
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(bq_);
    while (true)
    {
        // 消费活动
        Task t;
        bq->pop(&t);
        std::cout << "消费任务:" << t() << std::endl;
        //sleep(1);
    }
    return nullptr;
}
void *productor(void *bq_)
{
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(bq_);
    while (true)
    {
        // 生产活动
        int x = rand() % 100 + 1; // 这里我们先用一个随机数构建一个数据。
        int y = rand() % 10;
        int operCode = rand() % oper.size();
        Task t(x, y, oper[operCode], mymath);
        bq->push(t);

        std::cout << "生产任务:" << t.toTaskString() << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    srand((unsigned long)time(nullptr) ^ getpid());
    BlockQueue<Task> *bq = new BlockQueue<Task>();
    pthread_t c, p;
    // 生产消费要看到同一份资源,也就是阻塞队列
    pthread_create(&c, nullptr, consumer, bq);
    pthread_create(&p, nullptr, productor, bq);

    pthread_join(c, nullptr);
    pthread_join(p, nullptr);

    delete bq;

    return 0;
}

让生产者sleep1秒,看到的结果就是生产一个任务,消费一个任务。

让消费者sleep1秒,看到的结果就是消费一个任务,生产一个任务。

这样,我们就完成了一个线程给另一个线程派发任务:生产者给消费者派发任务。

  • 上面是单生产者,单消费者,如果我们直接改成多个生产者多个消费者可以吗?

MainCp.cc:

//
//...
//...
int main()
{
    srand((unsigned long)time(nullptr) ^ getpid());
    BlockQueue<Task> *bq = new BlockQueue<Task>();
    pthread_t c, c1, p, p1, p2;
    // 生产消费要看到同一份资源,也就是阻塞队列
    pthread_create(&p, nullptr, productor, bq);
    pthread_create(&p1, nullptr, productor, bq);
    pthread_create(&p2, nullptr, productor, bq);

    pthread_create(&c1, nullptr, consumer, bq);
    pthread_create(&c, nullptr, consumer, bq);

    pthread_join(c, nullptr);
    pthread_join(c1, nullptr);
    
    pthread_join(p, nullptr);
    pthread_join(p1, nullptr);
    pthread_join(p2, nullptr);
    delete bq;

    return 0;
}

可以看到是可以的。无论外部的线程再多,真正进入到阻塞队列里生产或消费的线程永远只有一个。

生产者要向blockqueue里放任务,消费者要向blockqueue里取任务。由于有锁的存在,这个(生产过程和消费过程)过程是串行的,也就是blockqueue里任何时刻只有一个执行流。那么:

  • 那么生产者消费者模型高效在哪里呢?创建多线程生产和消费的意义是什么呢?

1、对于生产者而言,它获取数据构建任务,是需要花时间的。

  • 如果这个任务的构建非常耗时,这个线程(生产者)在构建任务的同时,其他线程(生产者)可以并发的继续构建任务。

2、对于消费者而言,它拿到任务以后,是需要花时间处理这个任务的!

  • 如果这个任务的处理非常耗时,这个线程(消费者)在处理任务的同时,其他线程(消费者)可以并发的继续从阻塞队列里拿任务处理。

所以,高效并不体现在生产者把任务放进阻塞队列里高效,或者消费者从阻塞队列里拿任务高效。而是,体现在多个线程可以同时并发的构建或处理任务。

对于单生产单消费,它的并发性体现在,消费者从阻塞队列里拿任务和生产者构建任务,或者生产者往阻塞队列里放任务和消费者处理任务的过程是并发的。

总结:生产消费模型高效体现在,可以在生产前,和消费之后,让线程并行执行。

创建多线程生产和消费的意义:

多个线程可以并发生产,并发消费。

以上就是线程同步和基于阻塞队列的生产者消费者模型。

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

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

相关文章

ChatGPT各模型版本对比分析

文章目录 1. GPT-3.5&#xff08;2022年11月&#xff09;2. GPT-4&#xff08;2023年3月&#xff09;3. GPT-4o&#xff08;2024年5月&#xff09;4. GPT-4o mini&#xff08;2024年7月&#xff09;5. o1系列&#xff08;2024年9月至12月&#xff09;6. o3-mini&#xff08;202…

萌新学 Python 之自定义函数

函数主要用来封装功能&#xff0c;具有独立功能的代码块&#xff0c;可以提高代码重复利用率&#xff0c;便于模块管理 函数的定义&#xff1a; def 函数名(形参): 函数体&#xff0c;独立功能的代码 return ‘函数的返回值’ 函数注意事项&#xff1a; 1.函数的命名通常使…

【工作流】Spring Boot 项目与 Camunda 的整合

【工作流】Spring Boot 项目与 Camunda 的整合 【一】Camunda 和主流流程引擎的对比【二】概念介绍【1】Camunda 概念&#xff1a;【2】BPMN 概念 【三】环境准备【1】安装流程设计器CamundaModeler【画图工具】&#xff08;1&#xff09;下载安装 【2】CamundaModeler如何设计…

【Linux】基于UDP/TCP套接字编程与守护进程

目录 一、网路套接字编程 &#xff08;一&#xff09;基础概念 1、源IP地址与目的IP地址 2、端口号 3、TCP与UDP 4、网络字节序 &#xff08;二&#xff09;套接字编程接口 1、socket 常见API 2、sockaddr结构 &#xff08;三&#xff09;UDP套接字 1、UDP服务器创建…

【图像处理】:两幅图中相同区域的相似度比较

两幅图中相同区域的相似度比较 1.OpenCV和Python实现的两幅图相似度衡量方法1. 均方误差&#xff08;MSE&#xff09;2. 结构相似性指数&#xff08;SSIM&#xff09;图像协方差能显示结构特征的原因 3. 直方图相似度4. 特征点匹配5. 相关系数&#xff08;Pearson Correlation&…

[python脚本]论文1.(一)CPU/内存数据分析和分组

CPU 收集到的CPU数据&#xff0c;格式如下&#xff1a; 由于这里6个数据为一组来收集latency的数据以及各个分位值的数据&#xff0c;而本质上每一行都是一次完整的测试&#xff0c;因此这里将这个csv文件分为两个文件&#xff0c;第一个是和latency相关的&#xff0c;将6条数…

綫性與非綫性泛函分析與應用_1.例題(下)-半母本

第1章 實分析與函數論:快速回顧(下) 五、基數;有限集和無限集相關例題 例題1:集合基數的判斷 判斷集合和集合B=\{a,b,c,d,e\}的基數關係。 解析: 可以構造一個雙射,例如,,,,。 所以,兩個集合具有相同的基數。 例題2:可數集的證明 證明整數集是可數集。 解析: …

MQTT实现智能家居------3、源码分析(超详细)

一、连接服务器 1、初始化&#xff1a; mqtt_log_init();是一个空函数&#xff0c;自己定义宏 client mqtt_lease();//创建一个client结构体&#xff0c;从此以后client代表客户端 platform_memory_alloc();//是一个分配内存的总函数&#xff0c;可以适用于Linux、FreeRTos…

Qt常用控件之日历QCalendarWidget

日历QCalendarWidget QCalendarWidget 是一个日历控件。 QCalendarWidget属性 属性说明selectDate当前选中日期。minimumDate最小日期。maximumDate最大日期。firstDayOfWeek设置每周的第一天是周几&#xff08;影响日历的第一列是周几&#xff09;。gridVisible是否显示日历…

智慧废品回收小程序php+uniapp

废品回收小程序&#xff1a;数字化赋能环保&#xff0c;开启资源循环新时代 城市垃圾治理难题&#xff0c;废品回收小程序成破局关键 随着城市化进程加速与消费水平提升&#xff0c;我国生活垃圾总量逐年攀升&#xff0c;年均增速达5%-8%&#xff0c;其中超30%为可回收物。然…

SkyWalking集成Kafka实现日志异步采集经验总结

SkyWalking日志异步采集架构 【重点知识】 1、【Agent】kafka-reporter-plugin-x.x.x.jar包放plugins目录后必走kafka&#xff08;kafka没有正确配置就会报错&#xff09; 2、【Agent】异步如不开启数据压缩&#xff0c;日志数据较大&#xff0c;pod多、业务大时容易造成网络…

C++第十六讲:红黑树

C第十六讲&#xff1a;红黑树 1.什么是红黑树1.1红黑树的特点 2.MyRBTree实现2.1红黑树的结构2.2红黑树的插入2.2.1插入的总体逻辑2.2.2情况一&#xff1a;变色2.2.3情况二&#xff1a;单旋 变色2.2.4情况三&#xff1a;双旋 变色2.2.4插入代码总结 2.3红黑树的检查2.4完整代…

KubeKey一键安装部署k8s集群和KubeSphere详细教程

目录 一、KubeKey简介 二、k8s集群KubeSphere安装 集群规划 硬件要求 Kubernetes支持版本 操作系统要求 SSH免密登录 配置集群时钟 所有节点安装依赖 安装docker DNS要求 存储要求 下载 KubeKey 验证KubeKey 配置集群文件 安装集群 验证命令 登录页面 一、Ku…

Java 值传递

1 形参&实参 方法的定义可能会用到参数&#xff0c;参数在程序语言中分为&#xff1a; 实参&#xff1a;用于传递给函数/方法的参数&#xff0c;必须有确定的值。 形参&#xff1a;用于定义函数/方法&#xff0c;接收实参&#xff0c;不需要有确定的值。 String hello …

开源一款I2C电机驱动扩展板-FreakStudio多米诺系列

总线直流电机扩展板 原文链接&#xff1a; FreakStudio的博客 摘要 设计了一个I2C电机驱动板&#xff0c;通过I2C接口控制多个电机的转速和方向&#xff0c;支持刹车和减速功能。可连接16个扩展板&#xff0c;具有PWM输出、过流过热保护和可更换电机驱动芯片。支持按键控制…

论文笔记(七十二)Reward Centering(三)

Reward Centering&#xff08;三&#xff09; 文章概括摘要3 基于值的奖励中心化4 案例研究&#xff1a; 以奖励为中心的 Q-learning5 讨论、局限性与未来工作致谢 文章概括 引用&#xff1a; article{naik2024reward,title{Reward Centering},author{Naik, Abhishek and Wan…

内部知识库的核心模块是什么?

内容概要 现代企业内部知识库的架构设计遵循系统性、可扩展性与安全性三重原则&#xff0c;其核心模块的协同运作构成完整的知识资产运营体系。在知识存储层&#xff0c;基于结构化分类的存储管理采用多级目录架构&#xff08;如客户服务库、培训学习库&#xff09;&#xff0…

kafka基本知识

什么是 Kafka&#xff1f; Apache Kafka 是一个开源的分布式流处理平台&#xff0c;最初由 LinkedIn 开发&#xff0c;后来成为 Apache 软件基金会的一部分。Kafka 主要用于构建实时数据管道和流处理应用程序。它能够高效地处理大量的数据流&#xff0c;广泛应用于日志收集、数…

P8716 [蓝桥杯 2020 省 AB2] 回文日期

1 题目说明 2 题目分析 暴力不会超时&#xff0c;O(n)的时间复杂度&#xff0c; < 1 0 8 <10^8 <108。分析见代码&#xff1a; #include<iostream> #include<string> using namespace std;int m[13]{0,31,28,31,30,31,30,31,31,30,31,30,31};// 判断日期…

手机时钟精确到秒

这里以小米手机 MIUI 系统 举例 进入开发者模式 设置 - 我的设备 - 全部参数与信息 快速连续多次点击 MIUI 版本选框&#xff0c;即可进入开发者模式&#xff1b; 打开时间悬浮窗 设置 - 更多设置 - 开发者选项&#xff1b; 滑动至 输入 模块&#xff0c;开启时间悬浮窗&a…