Linux_线程同步生产者消费者模型

news2025/1/31 13:52:24

同步的相关概念

  • 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
  • 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。

同步的意义
互斥保证安全性,安全不一定合理或高效!!同步主要是在保证安全的前提下,让系统变得更加合理和高效!

条件变量

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。条件变量是一个用来进行线程同步的特性,内部要维护线程队列。

条件变量函数

pthread_cond_init函数

// 初始化局部的条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t
*restrict attr);

// 使用宏值初始化(全局),类比pthread_mutex_init
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  • cond 是指向条件变量对象的指针,该对象将被初始化。
  • attr 是指向条件变量属性的指针,用于指定条件变量的属性。如果此参数为 NULL,则使用默认属性。在大多数应用中,通常传递 NULL

函数成功时返回 0;出错时返回错误码。

pthread_cond_wait函数

// 使线程在条件变量上等待,直到该条件变量被另一个线程的信号
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict
mutex);
  • cond 是指向条件变量对象的指针。
  • mutex 是指向互斥锁对象的指针,该互斥锁必须在调用 pthread_cond_wait 之前被当前线程持有(即锁定状态)。

pthread_cond_signal函数

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

pthread_cond_broadcast函数 

// 唤醒所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);

一个Demo样例

// 初始化全局的锁和条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void* active(void* args)
{
    std::string name = static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&mutex);
        // 让每一个线程去条件变量那里去等待
        pthread_cond_wait(&cond, &mutex);    
        printf("%s is active!\n", name.c_str());
        
        pthread_mutex_unlock(&mutex);
    }
}
int main()
{
    pthread_t tid1, tid2, tid3;
    pthread_create(&tid1, nullptr, active, (void*)"thread-1");
    pthread_create(&tid2, nullptr, active, (void*)"thread-2");
    pthread_create(&tid3, nullptr, active, (void*)"thread-3");

    while(true)
    {
        printf("main wakeup thread...\n");
        // 我唤醒一个线程,一个线程去做相应的工作
        pthread_cond_signal(&cond);
        sleep(1);
    }

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);
    return 0;    
}

将上面的pthread_cond_signal换成下面这句代码,线程就会全部被唤醒。

pthread_cond_broadcast(&cond);

生产者消费者模型

什么是生产者消费者模型

生产者消费者模型(Producer-Consumer Model)是一种经典的多线程同步问题,用于描述两个或多个线程之间共享有限资源的场景。在这个模型中,生产者负责生成数据并将其放入缓冲区(Buffer),消费者则从缓冲区中取出数据并消费。生产者和消费者通过缓冲区进行通信,但它们的执行速度可能不同,因此需要通过同步机制来协调它们的行为。

为什么要使用生产者消费者模型

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

生产者消费者模型的优点:解耦、支持并发、支持忙闲不均

1分钟记住生产者消费者模型

生产者消费者模型可简单理解为321原则

  • 三种关系(生产和生产(互斥),消费和消费(互斥),生产和消费(互斥&&同步))
  • 两种角色(生产线程,消费线程)
  • 一个缓冲区(一段内存空间,如阻塞队列)

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

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

Blocking Queue的Demo代码

设计思路:

(1)定义阻塞队列

阻塞队列是生产者和消费者之间的通信桥梁。它需要支持以下操作:

  • 入队(put):生产者调用,将数据放入队列。如果队列已满,则阻塞生产者线程,直到有空间可用。
  • 出队(take):消费者调用,从队列中取出数据。如果队列为空,则阻塞消费者线程,直到有数据可用。

(2)生产者逻辑

生产者负责生成数据并将其放入阻塞队列。

  1. 生成数据。

  2. 调用阻塞队列的put方法将数据放入队列。

  3. 如果队列已满,生产者线程会被阻塞,直到有空间可用。

(3)消费者逻辑

消费者负责从阻塞队列中取出数据并消费。

  1. 调用阻塞队列的take方法从队列中取出数据。

  2. 如果队列为空,消费者线程会被阻塞,直到有数据可用。

  3. 消费数据

// BlockQueue.hpp
#pragma once
#include <iostream>
#include <queue>

static const int gcap = 10;
template <typename T>
class BlockQueue
{
private:
    bool IsFull() { return _q.size() == _cap; }
    bool IsEmpty() { return _q.empty(); }
public:
    BlockQueue(int cap = gcap)
        : _cap(cap)
        , _cwait_num(0)
        , _pwait_num(0)
    {
        // 基于RAII设计初始化和销毁
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_productor_cond, nullptr);
        pthread_cond_init(&_consumer_cond, nullptr);
    }
    void Put(const T &in) // 生产者
    {
        pthread_mutex_lock(&_mutex);
        // 为了防止伪唤醒,我们这里使用while进行判断
        // 伪唤醒是指将线程唤醒之后,线程没有资源可以访问,然后继续去等待
        while (IsFull())
        {
            _pwait_num++; // 若空间满了,就让等待的生产者数量++
            // 若满了,则生产者不能生产,必须在条件变量下等待
            // 临界区里面抱着锁等待是不被允许的!!!所以pthread_cond_wait
            // 被调用的时候:除了指明自己在那个条件变量下等待,还会释放自己抱着的锁
            pthread_cond_wait(&_productor_cond, &_mutex);
            // 返回时,线程被唤醒 && 重新申请并持有锁(它会在临界区内醒来!)
            // 才能继续往下运行
            _pwait_num--; // 生产者被唤醒,数量--
        }
        // while(IsFull())条件不满足 || 线程被唤醒, 退出循环
        _q.push(in);
        // 肯定有数据,唤醒消费者
        if (_cwait_num)
            pthread_cond_signal(&_consumer_cond);
        pthread_mutex_unlock(&_mutex);
    }
    void Take(T *out) // 消费者
    {
        pthread_mutex_lock(&_mutex);
        while (IsEmpty())
        {
            _cwait_num++; // 若数据空了,就让等待的消费者数量++
            pthread_cond_wait(&_consumer_cond, &_mutex);
            _cwait_num--; // 消费者被唤醒,数量--
        }
        *out = _q.front();
        _q.pop();
        // 肯定有空间,唤醒生产者
        if (_pwait_num)
            pthread_cond_signal(&_productor_cond);
        pthread_mutex_unlock(&_mutex);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_productor_cond);
        pthread_cond_destroy(&_consumer_cond);
    }

private:
    std::queue<T> _q;               // 使用普通队列作为保存数据的容器
    int _cap;                       // bq最大容量
    pthread_mutex_t _mutex;         // 互斥
    pthread_cond_t _productor_cond; // 生产者条件变量
    pthread_cond_t _consumer_cond;  // 消费者条件变量

    int _cwait_num;                 // 在条件变量下等待的消费者数量
    int _pwait_num;                 // 在条件变量下等待的生产者数量
};
// Main.cc
#include "BlockQueue.hpp"
#include <pthread.h>
#include <unistd.h>

void* Consumer(void* args)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int>*>(args);
    while(true)
    {
        sleep(2);

        int data;
        // 1.从bq中拿到数据
        bq->Take(&data);
        // 2.做各种处理
        printf("Consumer, 消费了一个数据: %d\n", data);
    }
}
void* Productor(void* args)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int>*>(args);
    // 1.从外部获取数据
    int data = 10;
    while(true)
    {
        // 2.生产到bq中
        printf("Productor, 生产了一个数据: %d\n", data);
        bq->Put(data);
        data++;
    }
}
int main()
{
    BlockQueue<int> *bq = new BlockQueue<int>(4);// 共享资源
    // 单生产,单消费
    pthread_t c, p;
    // 最后一个参数传递bq,保证同一个线程看到同一个队列
    pthread_create(&c, nullptr, Consumer, bq);
    pthread_create(&p, nullptr, Productor, bq);

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

    delete bq;
    return 0;
}

让消费者睡眠2秒,生产者正常运行

让生产者睡眠2秒,消费者正常运行

ps. 上面代码是单生产单消费模式的,若想改为多生产多消费的,直接添加线程就可以。因为在生产者消费者模型的321原则中,21已经有了,3中的其中一种关系(生产和消费)已经有了,只需要补充完剩下的两种就可以。而我们设计的生产者与生产者或者消费者与消费者之间本身就只有一把锁,所以它们天然的就存在互斥关系!

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

认识POSIX信号量以及接口

信号量本身就是一个计数器,它用来描述你所申请资源数目的多少

信号量原理:一元信号量(或二元信号量)主要用于实现资源的互斥访问。当一个线程(或进程)获取了信号量(将其值从1减为0),它便获得了对某个共享资源的独占访问权,其他试图获取该信号量的线程将被阻塞,直到信号量被释放(其值从0加回1)。

信号量对公共资源使用时可以整体使用,也可以不整体使用。整体使用就是把整个资源看作一份。当二元信号量的值为1时,表示资源未被占用,线程可以获取信号量并进入临界区;当信号量的值为0时,表示资源已被占用,其他线程必须等待。(如上面的阻塞队列)。

POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。但POSIX可以用于线程间同步

信号量的两个重要操作

1. P操作(Wait操作)

P操作(也称为sem_wait)用于等待信号量,其目的是减少信号量的值,并根据信号量的当前值决定是否阻塞当前线程或进程。

2. V操作(Post操作)

V操作(也称为sem_post)用于释放信号量,其目的是增加信号量的值,并可能唤醒等待该信号量的线程或进程。

 sem_init函数
// 初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);

参数

  • sem:指向要初始化的信号量对象的指针。
  • pshared:控制信号量的作用域。如果pshared的值非0,则信号量在进程间共享;如果为0,则信号量仅在调用进程内的线程间共享
  • value:信号量的初始值。这个值必须是非负的。

返回值

成功时,sem_init 返回0。失败时,返回-1,并设置errno以指示错误

sem_destroy函数
// 销毁信号量
int sem_destroy(sem_t *sem);

参数

  • sem:指向要销毁的信号量对象的指针。

返回值

  • 成功时,sem_destroy 返回 0。失败时,返回 -1,并设置 errno 以指示错误。
sem_wait函数
// 等待信号量
int sem_wait(sem_t *sem); //P()

参数

  • sem:指向要操作的信号量对象的指针。

返回值

  • 成功时返回0。失败时返回-1,并设置errno以指示错误。

该函数用于信号量的P操作,从信号量的值中减去1,如果信号量的值变为0,则当前线程将被阻塞,直到信号量的值被其他线程通过 sem_post 增加。

sem_post函数
// 发布信号量
int sem_post(sem_t *sem);//V()

参数

  • sem:指向要操作的信号量的指针。

返回值

  • 成功时,sem_post返回0。
  • 失败时,返回-1,并设置errno以指示错误。可能的错误包括EINVAL,表示传入的信号量不是一个有效的信号量。

该函数用于信号量的V操作,将信号量的值增加1;唤醒等待线程:如果有线程因为调用sem_wait函数而在该信号量上等待(即信号量的值为0且线程被阻塞),那么sem_post函数可能会唤醒其中一个等待的线程。具体唤醒哪个线程取决于操作系统的线程调度策略。

环形队列的Demo代码

这里环形队列我们使用数组模拟,用模运算来模拟环状特性。

设计思路:

1. 环形队列的结构

环形队列是一个固定大小的数组,通过两个指针(headtail)来表示队列的头部和尾部。队列的头部用于消费者读取数据,队列的尾部用于生产者写入数据。

2. 生产者和消费者线程

2.1 生产者线程

  • 使用信号量P(sem_wait函数)操作进行空间的预定,若没有空间则进行等待

  • 加锁

  • 写入数据:将数据写入 tail 指向的位置。

  • 更新 tail:将 tail 向前移动一位:tail = (tail + 1) % size

  • 解锁

  • 释放一个数据,即消费者有新数据可用

2.2 消费者线程

  • 使用信号量V(sem_post函数)操作进行数据的预定,若没有数据则进行等待

  • 加锁

  • 读取数据:从 head 指向的位置读取数据。

  • 更新 head:将 head 向前移动一位:head = (head + 1) % size

  • 解锁

  • 释放一个空间,即生产者有空闲位置可用

3. 同步机制

为了确保生产者和消费者之间的同步,需要使用互斥锁(mutex)和信号量(semaphore)来控制对共享资源的访问。

  • 互斥锁:用于保护生产者线程和消费者线程,确保同一时间只有一个线程可以修改队列。

  • 信号量:

    • sem_wait:表示等待信号量,从信号量的值中减去1

    • sem_post:表示发布信号量,从信号量的值中加1, 并唤醒等待的信号量

// Sem.hpp
#pragma once
#include <semaphore.h>

int defaultvalue = 1;
class Sem
{
public:
    Sem(int value = defaultvalue)
    {
        int n = ::sem_init(&_sem, 0, value);
        (void)n;
    }
    void P()
    {
        int n = ::sem_wait(&_sem);
        (void)n;
    }
    void V()
    {
        int n = ::sem_post(&_sem);
        (void)n;
    }
    ~Sem()
    {
        int n = ::sem_destroy(&_sem);
        (void)n;
    }

private:
    sem_t _sem;        // 信号量
    int _init_value;
};
// RingBuffer.hpp
#pragma once
#include <iostream>
#include <vector>
#include <pthread.h>
#include "Sem.hpp"

template <typename T>
class RingBuffer
{
public:
    RingBuffer(int size)
        : _ring(size),    // 给vector初始化一个大小
          _size(size),
          _tail(0),       // 生产者的默认位置
          _head(0),       // 消费者的默认位置
          _datasem(0),    // 默认数据为0
          _spacesem(size) // 默认空间为满
    {
        int n = ::pthread_mutex_init(&_p_lock, nullptr);
        (void)n;
        int m = ::pthread_mutex_init(&_c_lock, nullptr);
        (void)m;
    }
    // 生产者
    void Equeue(const T &in)
    {
        _spacesem.P(); // 预定一个空间
        // 上锁
        pthread_mutex_lock(&_p_lock);
        _ring[_tail] = in; // 生产
        _tail++;
        _tail %= _size; // 维持环形特性
        pthread_mutex_unlock(&_p_lock);

        _datasem.V(); // 释放一个数据
    }
    // 消费者
    void Pop(T *out)
    {
        _datasem.P(); // 预定一个数据
        // 上锁
        pthread_mutex_lock(&_c_lock);
        *out = _ring[_head]; // 取出数据
        _head++;
        _head %= _size;
        pthread_mutex_unlock(&_c_lock);

        _spacesem.V(); // 释放一个空间
    }
    ~RingBuffer()
    {
        int n = ::pthread_mutex_destroy(&_p_lock);
        (void)n;
        int m = ::pthread_mutex_destroy(&_c_lock);
        (void)m;
    }

private:
    std::vector<T> _ring;    // 环, 临界资源
    int _size;               // 总容量
    int _tail;               // 生产者位置
    int _head;               // 消费位置

    Sem _datasem;            // 数据信号量
    Sem _spacesem;           // 空间信号量
    pthread_mutex_t _p_lock; // 生产者锁
    pthread_mutex_t _c_lock; // 消费者锁
};
// Main.cc
#include "RingBuffer.hpp"
#include <string>
#include <unistd.h>

// 生产
void *Consumer(void *args)
{
    RingBuffer<int> *ring_buffer = static_cast<RingBuffer<int> *>(args);
    int data = 0;
    while (true)
    {
        // sleep(1);
        ring_buffer->Equeue(data);
    
        std::cout << "生产了一个数据: " << data << std::endl;
        data++;
    }
    return nullptr;
}
// 消费
void *Productor(void *args)
{
    RingBuffer<int> *ring_buffer = static_cast<RingBuffer<int> *>(args);

    while (true)
    {
        sleep(2);
        int data;
        ring_buffer->Pop(&data);
        std::cout << "消费了一个数据: " << data << std::endl;
    }
    return nullptr;
}

int main()
{
    RingBuffer<int> *ring_buffer = new RingBuffer<int>(5); // 共享资源 -> 临界资源
    pthread_t c1, p1, c2, c3, p2;
    pthread_create(&c1, nullptr, Consumer, ring_buffer);
    pthread_create(&c2, nullptr, Consumer, ring_buffer);
    pthread_create(&c3, nullptr, Consumer, ring_buffer);
    pthread_create(&p1, nullptr, Productor, ring_buffer);
    pthread_create(&p2, nullptr, Productor, ring_buffer);

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

    delete ring_buffer;
    return 0;
}

相关细节:

1.为了完成多生产和多消费的模型,消费者和生产者的同步和互斥问题我们的环形队列中已经解决。除此之外,我们还需要维护生产者和生产者 &&消费者和消费者之间的互斥关系。由于环形队列的下标也是属于临界资源的,如果不维持关系内部的互斥关系,是一定会破坏环形队列结构的。所以势必要引入生产者互斥锁和消费者互斥锁。

2.先加锁 or 先申请信号量

一定是先申请信号量,在加锁。先申请信号量,会让所有线程先瓜分好信号量,在其它线程进行等待锁的时候,此时资源已经申请好了,可以直接进入临界区;如果先加锁,在申请信号量,所有线程都会在锁上等待,等待完成之后在去申请信号量,这样解决问题的实际效率会变低。

3.使用信号量实现生产者消费者模型,对资源进行使用,申请时,为什么不判断一下条件是否满足?

信号量本身就是判断条件! 信号量:是一个计数器,是资源的预订机制。预订:可以理解为在外部,可以不判断资源是否满足,就可以知道内部资源的情况! 

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

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

相关文章

Github 2025-01-30 Go开源项目日报 Top10

根据Github Trendings的统计,今日(2025-01-30统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Go项目10Ollama: 本地大型语言模型设置与运行 创建周期:248 天开发语言:Go协议类型:MIT LicenseStar数量:42421 个Fork数量:2724 次关注人…

【Rust自学】17.2. 使用trait对象来存储不同值的类型

喜欢的话别忘了点赞、收藏加关注哦&#xff08;加关注即可阅读全文&#xff09;&#xff0c;对接下来的教程有兴趣的可以关注专栏。谢谢喵&#xff01;(&#xff65;ω&#xff65;) 17.2.1. 需求 这篇文章以一个例子来介绍如何在Rust中使用trait对象来存储不同值的类型。 …

VLLM性能调优

1. 抢占 显存不够的时候&#xff0c;某些request会被抢占。其KV cache被清除&#xff0c;腾退给其他request&#xff0c;下次调度到它&#xff0c;重新计算KV cache。 报这条消息&#xff0c;说明已被抢占&#xff1a; WARNING 05-09 00:49:33 scheduler.py:1057 Sequence gr…

Java线程认识和Object的一些方法

本文目标&#xff1a; 要对Java线程有整体了解&#xff0c;深入认识到里面的一些方法和Object对象方法的区别。认识到Java对象的ObjectMonitor&#xff0c;这有助于后面的Synchronized和锁的认识。利用Synchronized wait/notify 完成一道经典的多线程题目&#xff1a;实现ABC…

数据库管理-第287期 Oracle DB 23.7新特性一览(20250124)

数据库管理287期 2025-01-24 数据库管理-第287期 Oracle DB 23.7新特性一览&#xff08;20250124&#xff09;1 AI向量搜索&#xff1a;算术和聚合运算2 更改Compatible至23.6.0&#xff0c;以使用23.6或更高版本中的新AI向量搜索功能3 Cloud Developer包4 DBMS_DEVELOPER.GET_…

【MySQL】MySQL客户端连接用 localhost和127.0.0.1的区别

# systemctl status mysqld # ss -tan | grep 3306 # mysql -V localhost与127.0.0.1的区别是什么&#xff1f; 相信有人会说是本地IP&#xff0c;曾有人说&#xff0c;用127.0.0.1比localhost好&#xff0c;可以减少一次解析。 看来这个入门问题还有人不清楚&#xff0c;其实…

MySQL(高级特性篇) 14 章——MySQL事务日志

事务有4种特性&#xff1a;原子性、一致性、隔离性和持久性 事务的隔离性由锁机制实现事务的原子性、一致性和持久性由事务的redo日志和undo日志来保证&#xff08;1&#xff09;REDO LOG称为重做日志&#xff0c;用来保证事务的持久性&#xff08;2&#xff09;UNDO LOG称为回…

【Block总结】HWD,小波下采样,适用分类、分割、目标检测等任务|即插即用

论文信息 Haar wavelet downsampling (HWD) 是一项针对语义分割的创新模块&#xff0c;旨在通过减少特征图的空间分辨率来提高深度卷积神经网络&#xff08;DCNNs&#xff09;的性能。该论文的主要贡献在于提出了一种新的下采样方法&#xff0c;能够在下采样阶段有效地减少信息…

【解决方案】MuMu模拟器移植系统进度条卡住98%无法打开

之前在Vmware虚拟机里配置了mumu模拟器&#xff0c;现在想要移植到宿主机中 1、虚拟机中的MuMu模拟器12-1是目标系统&#xff0c;对应的目录如下 C:\Program Files\Netease\MuMu Player 12\vms\MuMuPlayer-12.0-1 2、Vmware-虚拟机-设置-选项&#xff0c;启用共享文件夹 3、复…

力扣面试150 快乐数 循环链表找环 链表抽象 哈希

Problem: 202. 快乐数 &#x1f469;‍&#x1f3eb; 参考题解 Code public class Solution {public int squareSum(int n) {int sum 0;while(n > 0){int digit n % 10;sum digit * digit;n / 10;}return sum;}public boolean isHappy(int n) {int slow n, fast squa…

安卓(android)实现注册界面【Android移动开发基础案例教程(第2版)黑马程序员】

一、实验目的&#xff08;如果代码有错漏&#xff0c;可查看源码&#xff09; 1.掌握LinearLayout、RelativeLayout、FrameLayout等布局的综合使用。 2.掌握ImageView、TextView、EditText、CheckBox、Button、RadioGroup、RadioButton、ListView、RecyclerView等控件在项目中的…

SpringSecurity:There is no PasswordEncoder mapped for the id “null“

文章目录 一、情景说明二、分析三、解决 一、情景说明 在整合SpringSecurity功能的时候 我先是去实现认证功能 也就是&#xff0c;去数据库比对用户名和密码 相关的类&#xff1a; UserDetailsServiceImpl implements UserDetailsService 用于SpringSecurity查询数据库 Logi…

微服务入门(go)

微服务入门&#xff08;go&#xff09; 和单体服务对比&#xff1a;里面的服务仅仅用于某个特定的业务 一、领域驱动设计&#xff08;DDD&#xff09; 基本概念 领域和子域 领域&#xff1a;有范围的界限&#xff08;边界&#xff09; 子域&#xff1a;划分的小范围 核心域…

996引擎 - NPC-动态创建NPC

996引擎 - NPC-动态创建NPC 创建脚本服务端脚本客户端脚本添加自定义音效添加音效文件修改配置参考资料有个小问题,创建NPC时没有控制朝向的参数。所以。。。自己考虑怎么找补吧。 多重影分身 创建脚本 服务端脚本 Mir200\Envir\Market_Def\test\test001-3.lua -- NPC八门名…

基于MinIO的对象存储增删改查

MinIO是一个高性能的分布式对象存储服务。Python的minio库可操作MinIO&#xff0c;包括创建/列出存储桶、上传/下载/删除文件及列出文件。 查看帮助信息 minio.exe --help minio.exe server --help …

观察者模式和订阅发布模式的关系

有人把观察者模式等同于发布订阅模式&#xff0c;也有人认为这两种模式存在差异&#xff0c;本质上就是调度的方法不同。 发布订阅模式: 观察者模式: 相比较&#xff0c;发布订阅将发布者和观察者之间解耦。&#xff08;发布订阅有调度中心处理&#xff09;

(2025 年最新)MacOS Redis Desktop Manager中文版下载,附详细图文

MacOS Redis Desktop Manager中文版下载 大家好&#xff0c;今天给大家带来一款非常实用的 Redis 可视化工具——Redis Desktop Manager&#xff08;简称 RDM&#xff09;。相信很多开发者都用过 Redis 数据库&#xff0c;但如果你想要更高效、更方便地管理 Redis 数据&#x…

Baklib引领内容管理平台新时代优化创作流程与团队协作

内容概要 在迅速变化的数字化时代&#xff0c;内容管理平台已成为各种行业中不可或缺的工具。通过系统化的管理&#xff0c;用户能够有效地组织、存储和共享信息&#xff0c;从而提升工作效率和创意表达。Baklib作为一款新兴的内容管理平台&#xff0c;以其独特的优势和创新功…

Java实现.env文件读取敏感数据

文章目录 1.common-env-starter模块1.目录结构2.DotenvEnvironmentPostProcessor.java 在${xxx}解析之前执行&#xff0c;提前读取配置3.EnvProperties.java 这里的path只是为了代码提示4.EnvAutoConfiguration.java Env模块自动配置类5.spring.factories 自动配置和注册Enviro…

房屋租赁系统在数字化时代中如何重塑租赁服务与提升市场竞争力

内容概要 在当今快速发展的数字化时代&#xff0c;房屋租赁系统的作用愈发重要。随着市场需求的变化&#xff0c;租赁服务正面临着新的挑战与机遇。房屋租赁系统不仅仅是一个简单的管理工具&#xff0c;更是一个能够提升用户体验和市场竞争力的重要平台。其核心功能包括合同管…