<生产者、消费者问题>——《Linux》

news2024/11/24 7:56:13

 目录

1. 生产者消费者模型

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

1.2 生产者消费者模型优点

2.基于BlockingQueue的生产者消费者模型

2.1 BlockingQueue

2.2 C++ queue模拟阻塞队列的生产消费模型

3.POSIX信号量

4.基于环形队列的生产消费模型

后记:●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!

                                                                           ——By 作者:新晓·故知


1. 生产者消费者模型

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

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

1.2 生产者消费者模型优点

  • 解耦
  • 支持并发
  • 支持忙闲不均

2.基于BlockingQueue的生产者消费者模型

2.1 BlockingQueue

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

2.2 C++ queue模拟阻塞队列的生产消费模型

代码:
为了便于理解,我们以单生产者,单消费者,来进行讲解。

#include <iostream>
#include <queue>
#include <stdlib.h>
#include <pthread.h>
#define NUM 8
class BlockQueue
{
private:
    std::queue<int> q;
    int cap;
    pthread_mutex_t lock;
    pthread_cond_t full;
    pthread_cond_t empty;

private:
    void LockQueue()
    {
        pthread_mutex_lock(&lock);
    }
    void UnLockQueue()
    {
        pthread_mutex_unlock(&lock);
    }
    void ProductWait()
    {
        pthread_cond_wait(&full, &lock);
    }
    void ConsumeWait()
    {
        pthread_cond_wait(&empty, &lock);
    }
    void NotifyProduct()
    {
        pthread_cond_signal(&full);
    }
    void NotifyConsume()
    {
        pthread_cond_signal(&empty);
    }
    bool IsEmpty()
    {
        return (q.size() == 0 ? true : false);
    }
    bool IsFull()
    {
        return (q.size() == cap ? true : false);
    }

public:
    BlockQueue(int _cap = NUM) : cap(_cap)
    {
        pthread_mutex_init(&lock, NULL);
        pthread_cond_init(&full, NULL);
        pthread_cond_init(&empty, NULL);
    }
    void PushData(const int &data)
    {
        LockQueue();
        while (IsFull())
        {
            NotifyConsume();
            std::cout << "queue full, notify consume data, product stop." << std::endl;
            ProductWait();
        }
        q.push(data);
        // NotifyConsume();
        UnLockQueue();
    }
    void PopData(int &data)
    {
        LockQueue();
        while (IsEmpty())
        {
            NotifyProduct();
            std::cout << "queue empty, notify product data, consume stop." << std::endl;
            ConsumeWait();
        }
        data = q.front();
        q.pop();
        // NotifyProduct();
        UnLockQueue();
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&lock);
        pthread_cond_destroy(&full);
        pthread_cond_destroy(&empty);
    }
};
void *consumer(void *arg)
{
    BlockQueue *bqp = (BlockQueue *)arg;
    int data;
    for (;;)
    {
        bqp->PopData(data);
        std::cout << "Consume data done : " << data << std::endl;
    }
}
// more faster
void *producter(void *arg)
{
    BlockQueue *bqp = (BlockQueue *)arg;
    srand((unsigned long)time(NULL));
    for (;;)
    {
        int data = rand() % 1024;
        bqp->PushData(data);
        std::cout << "Prodoct data done: " << data << std::endl;
        // sleep(1);
    }
}
int main()
{
    BlockQueue bq;
    pthread_t c, p;
    pthread_create(&c, NULL, consumer, (void *)&bq);
    pthread_create(&p, NULL, producter, (void *)&bq);
    pthread_join(c, NULL);
    pthread_join(p, NULL);
    return 0;
}

模拟实现:

BlockQueue.hpp:

#pragma once
#include <iostream>
#include <queue>
#include <cstdlib>
#include <unistd.h>
#include <pthread.h>
using namespace std;

// 实现新需求: 我只想保存最新的5个任务,如果来了任务,老的任务,我想让他直接被丢弃(自行选择实现)

const uint32_t gDefaultCap = 5;
template <class T>
class BlockQueue
{
public:
    BlockQueue(uint32_t cap = gDefaultCap) : cap_(cap)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&conCond_, nullptr);
        pthread_cond_init(&proCond_, nullptr);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&conCond_);
        pthread_cond_destroy(&proCond_);
    }

public:
    //生产接口
    void push(const T &in) // const &: 纯输入
    {
        // 加锁
        // 判断->是否适合生产->bq是否为满->程序员视角的条件->1. 满(不生产) 2. 不满(生产)
        // if(满) 不生产,休眠
        // else if(不满) 生产,唤醒消费者
        // 解锁

        lockQueue();
        while (isFull()) // ifFull就是我们在临界区中设定的条件
        {
            // before: 当我等待的时候,会自动释放mutex_
            proBlockWait(); //阻塞等待,等待被唤醒。 被唤醒 != 条件被满足(概率虽然很小),被唤醒 && 条件被满足
            //解决伪唤醒(使用while)
            // after: 当我醒来的时候,我是在临界区里醒来的!!
        }
        // 条件满足,可以生产
        pushCore(in); //生产完成
        // wakeupCon(); // 唤醒消费者
        unlockQueue();
        wakeupCon(); // 唤醒消费者
    }
    //消费接口
    T pop()
    {
        // 加锁
        // 判断->是否适合消费->bq是否为空->程序员视角的条件->1. 空(不消费) 2. 有(消费)
        // if(空) 不消费,休眠
        // else if(有) 消费,唤醒生产者
        // 解锁
        lockQueue();
        while (isEmpty())
        {
            conBlockwait(); //阻塞等待,等待被唤醒,?
        }
        // 条件满足,可以消费
        T tmp = popCore();
        unlockQueue();
        wakeupPro(); // 唤醒生产者

        return tmp;
    }

private:
    void lockQueue()
    {
        pthread_mutex_lock(&mutex_);
    }
    void unlockQueue()
    {
        pthread_mutex_unlock(&mutex_);
    }
    bool isEmpty()
    {
        return bq_.empty();
    }
    bool isFull()
    {
        return bq_.size() == cap_;
    }
    void proBlockWait() // 生产者一定是在临界区中的!
    {
        // 1. 在阻塞线程的时候,会自动释放mutex_锁
        pthread_cond_wait(&proCond_, &mutex_);
    }

    void conBlockwait() //阻塞等待,等待被唤醒
    {
        // 1. 在阻塞线程的时候,会自动释放mutex_锁
        pthread_cond_wait(&conCond_, &mutex_);
        // 2. 当阻塞结束,返回的时候,pthread_cond_wait,会自动帮你重新获得mutex_,然后才返回
        // 为什么我们上节课,写的代码,批量退出线程的时候,发现无法退出?  
        //唤醒时, 调用pthread_cond_wait(&conCond_, &mutex_);,重新去竞争这个锁,多个线程去竞争一个,且那个拥有锁的线程退出没有释放锁,最后导致其他线程阻塞。
    }

    void wakeupPro() // 唤醒生产者
    {
        pthread_cond_signal(&proCond_);
    }
    void wakeupCon() // 唤醒消费者
    {
        pthread_cond_signal(&conCond_);
    }
    void pushCore(const T &in)
    {
        bq_.push(in); //生产完成
    }
    T popCore()
    {
        T tmp = bq_.front();
        bq_.pop();
        return tmp;
    }

private:
    uint32_t cap_;           //容量
    queue<T> bq_;            // blockqueue
    pthread_mutex_t mutex_;  //保护阻塞队列的互斥锁
    pthread_cond_t conCond_; // 让消费者等待的条件变量
    pthread_cond_t proCond_; // 让生产者等待的条件变量
};

BlockQueueTest.cc: 

#include "Task.hpp"
#include "BlockQueue.hpp"
#include <ctime>

const std::string ops = "+-*/%";
// 并发,并不是在临界区中并发(一般),而是生产前(before blockqueue),消费后(after blockqueue)对应的并发
void *consumer(void *args)
{
    BlockQueue<Task> *bqp = static_cast<BlockQueue<Task> *>(args);
    while (true)
    {
        Task t = bqp->pop(); // 消费任务
        int result = t();    //处理任务 --- 任务也是要花时间的!
        int one, two;
        char op;
        t.get(&one, &two, &op);
        cout << "consumer[" << pthread_self() << "] " << (unsigned long)time(nullptr) << " 消费了一个任务: " << one << op << two << "=" << result << endl;
    }
}
void *productor(void *args)
{
    BlockQueue<Task> *bqp = static_cast<BlockQueue<Task> *>(args);
    while (true)
    {
        // 1. 制作任务 --- 要不要花时间?? -- 网络,磁盘,用户
        int one = rand() % 50;
        int two = rand() % 20;
        char op = ops[rand() % ops.size()];
        Task t(one, two, op);
        // 2. 生产任务
        bqp->push(t);
        cout << "producter[" << pthread_self() << "] " << (unsigned long)time(nullptr) << " 生产了一个任务: " << one << op << two << "=?" << endl;
        sleep(1);
    }
}

int main()
{
    srand((unsigned long)time(nullptr) ^ getpid());
    // 定义一个阻塞队列
    // 创建两个线程,productor, consumer
    // productor -----  consumer
    // BlockQueue<int> bq;
    // bq.push(10);
    // int a = bq.pop();
    // cout << a << endl;
    // 既然可以使用int类型的数据,我们也可以使用自己封装的类型,包括任务
    // BlockQueue<int> bq;
    BlockQueue<Task> bq;

    pthread_t c, p;
    pthread_create(&c, nullptr, consumer, &bq);
    pthread_create(&p, nullptr, productor, &bq);

    pthread_join(c, nullptr);
    pthread_join(p, nullptr);
    return 0;
}

Task.hpp:

#pragma once
#include <iostream>
#include <string>

class Task
{
public:
    Task() 
    :elemOne_(0)
    ,elemTwo_(0)
    ,operator_('0')
    {}
    Task(int one, int two, char op) 
    :elemOne_(one)
    ,elemTwo_(two)
    ,operator_(op)
    {}
    int operator() ()
    {
        return run();
    }
    int run()
    {
        int result = 0;
        switch (operator_)
        {
        case '+':
            result = elemOne_ + elemTwo_;
            break;
        case '-':
            result = elemOne_ - elemTwo_;
            break;
        case '*':
            result = elemOne_ * elemTwo_;
            break;
        case '/':
        {
            if (elemTwo_ == 0)
            {
                std::cout << "div zero, abort" << std::endl;
                result = -1;
            }
            else
            {
                result = elemOne_ / elemTwo_;
            }
        }

        break;
        case '%':
        {
            if (elemTwo_ == 0)
            {
                std::cout << "mod zero, abort" << std::endl;
                result = -1;
            }
            else
            {
                result = elemOne_ % elemTwo_;
            }
        }
        break;
        default:
            std::cout << "非法操作: " << operator_ << std::endl;
            break;
        }
        return result;
    }
    int get(int *e1, int *e2, char *op)
    {
        *e1 = elemOne_;
        *e2 = elemTwo_;
        *op = operator_;
    }
private:
    int elemOne_;
    int elemTwo_;
    char operator_;
};

makefile: 

CC=g++
FLAGS=-std=c++11
LD=-lpthread
bin=blockQueue
src=BlockQueueTest.cc

$(bin):$(src)
	$(CC) -o $@ $^ $(LD) $(FLAGS)
.PHONY:clean
clean:
	rm -f $(bin)

3.POSIX信号量

POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
 pshared:0表示线程间共享,非零表示进程间共享
 value:信号量初始值
销毁信号量
int sem_destroy(sem_t *sem);
等待信号量
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem);
发布信号量
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);
上面的生产者-消费者的例子是基于queue的,其空间可以动态分配,现在基于固定大小的环形队列重写这个程序 (POSIX信号量)
信号量是一个计数器,描述临界资源数量的计数器。二元信号量==互斥锁

4.基于环形队列的生产消费模型

  • 环形队列采用数组模拟,用模运算来模拟环状特性

  • 环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态
  •  但是我们现在有信号量这个计数器,就很简单的进行多线程间的同步过程

 模拟实现:

#include <iostream>
#include <vector>
#include <stdlib.h>
#include <semaphore.h>
#include <unistd.h>
#include <pthread.h>
#define NUM 16
class RingQueue
{
private:
    std::vector<int> q;
    int cap;
    sem_t data_sem;
    sem_t space_sem;
    int consume_step;
    int product_step;

public:
    RingQueue(int _cap = NUM) : q(_cap), cap(_cap)
    {
        sem_init(&data_sem, 0, 0);
        sem_init(&space_sem, 0, cap);
        consume_step = 0;
        product_step = 0;
    }
    void PutData(const int &data)
    {
        sem_wait(&space_sem); // P
        q[consume_step] = data;
        consume_step++;
        consume_step %= cap;
        sem_post(&data_sem); // V
    }
    void GetData(int &data)
    {
        sem_wait(&data_sem);
        data = q[product_step];
        product_step++;
        product_step %= cap;
        sem_post(&space_sem);
    }
    ~RingQueue()
    {
        sem_destroy(&data_sem);
        sem_destroy(&space_sem);
    }
};
void *consumer(void *arg)
{
    RingQueue *rqp = (RingQueue *)arg;
    int data;
    for (;;)
    {
        rqp->GetData(data);
        std::cout << "Consume data done : " << data << std::endl;
        sleep(1);
    }
}
// more faster
void *producter(void *arg)
{
    RingQueue *rqp = (RingQueue *)arg;
    srand((unsigned long)time(NULL));
    for (;;)
    {
        int data = rand() % 1024;
        rqp->PutData(data);
        std::cout << "Prodoct data done: " << data << std::endl;
        // sleep(1);
    }
}
int main()
{
    RingQueue rq;
    pthread_t c, p;
    pthread_create(&c, NULL, consumer, (void *)&rq);
    pthread_create(&p, NULL, producter, (void *)&rq);
    pthread_join(c, NULL);
    pthread_join(p, NULL);
}

RingQueue.hpp:

 

#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <semaphore.h>
using namespace std;

const int gCap = 10;
template <class T>
class RingQueue
{
public:
    RingQueue(int cap = gCap)
    :ringqueue_(cap)
    ,pIndex_(0)
    ,cIndex_(0)
    {
        // 生产
        sem_init(&roomSem_, 0, ringqueue_.size());
        // 消费
        sem_init(&dataSem_, 0, 0);

        pthread_mutex_init(&pmutex_ ,nullptr);
        pthread_mutex_init(&cmutex_ ,nullptr);
    }
    // 生产
    void push(const T &in)
    {
        sem_wait(&roomSem_); //无法被多次的申请
        pthread_mutex_lock(&pmutex_);

        ringqueue_[pIndex_] = in; //生产的过程
        pIndex_++;   // 写入位置后移
        pIndex_ %= ringqueue_.size(); // 更新下标,保证环形特征

        pthread_mutex_unlock(&pmutex_);
        sem_post(&dataSem_);
    }
    // 消费
    T pop()
    {
        sem_wait(&dataSem_);
        pthread_mutex_lock(&cmutex_);

        T temp = ringqueue_[cIndex_];
        cIndex_++;
        cIndex_ %= ringqueue_.size();// 更新下标,保证环形特征

        pthread_mutex_unlock(&cmutex_);
        sem_post(&roomSem_);

        return temp;
    }
    ~RingQueue()
    {
        sem_destroy(&roomSem_);
        sem_destroy(&dataSem_);

        pthread_mutex_destroy(&pmutex_);
        pthread_mutex_destroy(&cmutex_);
    }
private:
    vector<T> ringqueue_; // 唤醒队列
    sem_t roomSem_;       // 衡量空间计数器,productor
    sem_t dataSem_;       // 衡量数据计数器,consumer
    uint32_t pIndex_;     // 当前生产者写入的位置, 如果是多线程,pIndex_也是临界资源
    uint32_t cIndex_;     // 当前消费者读取的位置,如果是多线程,cIndex_也是临界资源

    pthread_mutex_t pmutex_;
    pthread_mutex_t cmutex_;
};

RingQueueTest.cc:

#include "RingQueue.hpp"
#include <ctime>
#include <unistd.h>

// 我们是单生产者,单消费者
// 多生产者,多消费者??代码怎么改?
// 为什么呢???多生产者,多消费者?
// 不要只关心把数据或者任务,从ringqueue 放拿的过程,获取数据或者任务,处理数据或者任务,也是需要花时间的!

void *productor(void *args)
{
    RingQueue<int> *rqp = static_cast<RingQueue<int> *>(args);
    while(true)
    {
        int data = rand()%10;
        rqp->push(data);
        cout << "pthread[" << pthread_self() << "]" << " 生产了一个数据: " << data << endl;
        sleep(1);
    }
}

void *consumer(void *args)
{
    RingQueue<int> *rqp = static_cast<RingQueue<int> *>(args);
    while(true)
    {
        //sleep(10);
        int data = rqp->pop();
        cout << "pthread[" << pthread_self() << "]" << " 消费了一个数据: " << data << endl;
    }
}

int main()
{
    srand((unsigned long)time(nullptr)^getpid());

    RingQueue<int> rq;

    pthread_t c1,c2,c3, p1,p2,p3;
    pthread_create(&p1, nullptr, productor, &rq);
    pthread_create(&p2, nullptr, productor, &rq);
    pthread_create(&p3, nullptr, productor, &rq);
    pthread_create(&c1, nullptr, consumer, &rq);
    pthread_create(&c2, nullptr, consumer, &rq);
    pthread_create(&c3, nullptr, consumer, &rq);

    pthread_join(c1, nullptr);
    pthread_join(c2, nullptr);
    pthread_join(c3, nullptr);
    pthread_join(p1, nullptr);
    pthread_join(p2, nullptr);
    pthread_join(p3, nullptr);

    return 0;
}

 makefile:

 

CC=g++
FLAGS=-std=c++11
LD=-lpthread
bin=ringQueue
src=RingQueueTest.cc

$(bin):$(src)
	$(CC) -o $@ $^ $(LD) $(FLAGS)
.PHONY:clean
clean:
	rm -f $(bin)

 

后记:
●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!

                                                                           ——By 作者:新晓·故知

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

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

相关文章

windows中docker修改镜像与容器存放目录(不想放c盘)

查看状态 wsl --list -v 先退出docker deskop 查看状态 wsl --list -v 关闭wsl wsl --shutdown docker的镜像与容器存放目录在此处 新建一个你想要存放的目录&#xff0c;比如我存到H盘这个地方 分别将c盘那两个文件夹&#xff08;data和distro&#xff09;的内容打包压缩到H盘…

php使用redis进行消息发布订阅

php使用redis进行消息发布订阅前置条件消费者订阅subscribe.php生产者发送消息publish.php执行消费者订阅&#xff0c;开始阻塞获取消息执行生产者&#xff0c;开始发送消息查看消费者终端前置条件 已经安装了php的redis扩展 消费者订阅subscribe.php <?php ini_set(def…

【Java基础】Java日志—什么是日志?什么是Log4j?Log4j入门案例及配置

目录 一、什么是日志&#xff1f; 二、为什么会有日志&#xff1f; 四、入门案例&#xff1a;Log4j日志信息输出到控制台 步骤与实现&#xff1a; 步骤1&#xff1a;拷贝坐标 步骤2&#xff1a;拷贝配置文件 log4j.properties 步骤3&#xff1a;编写测试类 写到最后 &…

TensorRt(3)mnist示例中的C++ API

目前sample中mnist提供了至少caffe、onnx的预训练模型&#xff0c;在TensorRT经过优化生成engine后再进行infer&#xff0c;两种模型的加载处理略有不同&#xff0c;做出简单api处理说明。 最后尝试使用最少的代码来实现整个流程。 文章目录1、主要的C API 定义2、minst示例2.1…

云安全类型及预防方法

恶意软件是我们必须面对的现实&#xff0c;我们每天都需要与蠕虫、病毒、间谍软件和其他行恶意软件作斗争&#xff0c;而云恶意软件是我们需要面对的又一种类别。它已经发展十多年&#xff0c;早在2011年就托管在亚马逊简单存储服务存储桶中。云安全提供商Netskope报告称&#…

springboot够用就好系列-2.基于commandfast框架的应用开发

参考web的jsoncat框架&#xff0c;实现一个控制台IO的commandfast简易框架&#xff0c;并进行使用。 目录 程序效果 实现过程 样例代码 工程文件 参考资料 程序效果 截图1.查询当前时间和用户&#xff0c;查询磁盘空间 利用commandfast框架&#xff0c;实现的2个简单功能&…

95后阿里P7晒出工资单:狠补了两眼泪汪汪,真香...

最近一哥们跟我聊天装逼&#xff0c;说他最近从阿里跳槽了&#xff0c;我问他跳出来拿了多少&#xff1f;哥们表示很得意&#xff0c;说跳槽到新公司一个月后发了工资&#xff0c;月入5万多&#xff0c;表示很满足&#xff01;这样的高薪资着实让人羡慕&#xff0c;我猜这是税后…

Redis 核心原理串讲(上),从一条请求透视高性能的本质

文章目录Redis 核心原理总览&#xff08;全局篇&#xff09;前言一、请求二、数据结构1. 有哪些&#xff1f;2. 为什么节省内存又高效&#xff1f;三、网络模型1、四种常见IO模型1.1 同步阻塞1.2 同步非阻塞1.3 IO多路复用1.4 异步IO2、事件驱动2.1 引子2.2 事件驱动模型3、Rea…

【Windows】win10家庭版无法被远程桌面(mstsc)连接的解决方案

&#x1f41a;作者简介&#xff1a;花神庙码农&#xff08;专注于Linux、WLAN、TCP/IP、Python等技术方向&#xff09;&#x1f433;博客主页&#xff1a;花神庙码农 &#xff0c;地址&#xff1a;https://blog.csdn.net/qxhgd&#x1f310;系列专栏&#xff1a;善假于物&#…

前端知识学习

一、html的学习 1.1 html的基本结构 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title> </head> <body></body> </html>1. <!DOCTYPE html> 告诉浏…

网页版chatGPT,国内直接打开就用的chatgpt

先看效果&#xff1a; 文件就是一个网页文件&#xff0c;直接打开就可以网页使用了。 使用的前提是需要有chatGPT的账号去获取apikey,然后把拿到的apikey放在下面代码中 然后网页的代码如下&#xff1a; <script src"https://unpkg.com/vue3/dist/vue.global.js&qu…

MySQL事务日志 (redo log)

MySQL事务日志 &#xff08;redo log) 事务的隔离性由 锁机制 实现。 而事务的原子性、一致性和持久性由事务的 redo 日志和undo 日志来保证。 REDO LOG 称为 重做日志 &#xff0c;提供再写入操作&#xff0c;恢复提交事务修改的页操作&#xff0c;用来保证事务的持久性。 …

如何使用YonBuilder进行报表分析?

报表是基于业务元数据、业务模型、数据模型等数据来源展示与分析业务的重要工具&#xff0c;在YonBuilder中可以通过简单拖拽、选择&#xff0c;快速生成报表分析&#xff0c;提升报表开发效率。本期通过员工信息数据对YonBuilder中报表的基本配置进行介绍。 01、创建报表 首…

深入理解kafka-1

kafka快速入门1、kafka简介1.1 kafka是什么1.2 kafka基础架构1.3 kafka模块概述2、kafkka结构剖析2.1 kafka工作流程2.2 kafka文件存储2.2.1 顺序写2.2.2 分片&#xff0c;索引2.2.3 页缓存2.2.4 零拷贝2.3 broker集群2.3.1 Controller控制器及选举机制2.4 生产者2.4.1 生产者分…

MCU-51:定时器

目录一、定时器介绍1.1 定时器的功能1.2 定时器的结构1.3 定时器框图二、定时器控制2.1 工作模式寄存器TMOD2.2 控制寄存器TCON三、中断系统3.1 中断系统介绍3.2 中断程序流程3.3 STC89C52中断资源四、应用4.1 定时器控制LED闪烁4.2 基于定时器按键控制LED流水灯4.3 定时器时钟…

C进阶 :征服指针之指针与数组强化笔试题练习(1)

目录 &#x1f63c;&#x1f638;一.彻底明白 sizeof 操作符 &#xff0c;数组名&#xff0c;strlen 函数 &#x1f405;1.数组名的意义 &#x1f406;2. sizeof 详解 &#x1f40b;3.strlen详解 &#x1f996;3.数组名意义详细图解演示 &#x1f431;&#x1f640;二.关于…

使用JDBC+javafx写一个简单功能齐全的图书管理系统

目录 1、JDBC的使用 2、对应包和Java文件的层级关系及对应的含义 3、数据库 4、相关代码 1&#xff09;、bookmanager包 Ⅰ、main函数 Ⅱ、utils包 Ⅲ、bean包 Ⅳ、controller包 2)resources(为资源文件包&#xff0c;可以看链接文章了解) Ⅰ、book包 Ⅱ、 login包…

嘘!P站数据分析年报;各省市疫情感染进度条;爱奇艺推出元宇宙App;You推出AI聊天机器人;GitHub今日热榜 | ShowMeAI资讯日报

&#x1f440;日报合辑 | &#x1f3a1;AI应用与工具大全 | &#x1f514;公众号资料下载 | &#x1f369;韩信子 &#x1f4e2; 『The 2022 Year in Review』P站2022年度报告 Pornhub 发布了第 9 次年度报告&#xff0c;数据科学家们绘制了多张彩色可视化图表&#xff0c;回顾…

Spring注册Bean系列--方法3:@Import+@Bean

原文网址&#xff1a;Spring注册Bean系列--方法3&#xff1a;ImportBean_IT利刃出鞘的博客-CSDN博客 简介 本文介绍Spring注册Bean的方法&#xff1a;ImportBean。 注册Bean的方法我写了一个系列&#xff0c;见&#xff1a;Spring注册Bean(提供Bean)系列--方法大全_IT利刃出鞘…

Redis-SDS

本文你能得到&#xff1a; 1 SDS基本介绍 。 2 SDS与 C语言传统字符串的区别&#xff0c;为什么使用SDS。 3 SDS的结构和策略详解。 1 SDS 是什么&#xff1f;用来做什么&#xff1f; 1.1 ​ Redis没有直接使用C语言传统的字符串表示&#xff08;以空字符结尾的字符数组&a…