当前位置: 首页 > news >正文

【Linux】POSIX信号量与、基于环形队列实现的生产者消费者模型

目录

一、POSIX信号量概述

信号量的基本概念

信号量在临界区的作用

与互斥锁的比较

信号量的原理

信号量的优势

二、信号量的操作

1、初始化信号量:sem_init

2、信号量申请(P操作):sem_wait

3、信号量的释放(V操作):sem_post

4、信号量的销毁:sem_destroy

信号量使用代码示例: 

三、基于环形队列实现的生产者消费者模型

四、完整代码

五、生产者消费者模型两种实现方法的对比 

阻塞队列与条件变量的模型

环形队列与信号量的模型

为什么环形队列与信号量需要两个锁

总结


一、POSIX信号量概述

POSIX信号量是POSIX标准(Portable Operating System Interface,可移植操作系统接口)定义的一种进程/线程间同步机制。它提供了一种跨平台的方法来实现进程间的同步和互斥。POSIX信号量可以用于控制对共享资源的访问,确保在任何时刻只有一个进程可以访问该资源,或者限制对资源的并发访问数量。

信号量的基本概念

  1. 信号量定义

    • 信号量是一个计数器,用于管理对有限资源的访问。
    • 它通过维护一个计数值来表示资源的数量或可用的资源数。
  2. 操作

    • P操作(等待/减少操作):当一个线程试图获取资源时,它会执行P操作(也叫sem_wait),这会减少信号量的计数。如果计数值已经为零,线程会被阻塞,直到有资源可用。
    • V操作(信号/增加操作):当一个线程释放资源时,它会执行V操作(也叫sem_post),这会增加信号量的计数。如果有线程在等待资源,这会唤醒其中一个线程。
    • 阻塞与唤醒:POSIX 信号量通常有一个等待队列。当信号量的值为 0 或者低于某个阈值时,进程(或线程)在请求信号量时会被阻塞,并被加入到信号量的等待队列中。信号量操作系统内部会维护这个队列,以确保进程可以在信号量的值变为正数时被唤醒。

信号量在临界区的作用

  1. 控制访问

            在进入临界区之前,线程首先会通过执行P操作来申请信号量。这种操作检查信号量的计数,如果计数为零,线程会被阻塞,直到其他线程释放资源。
  2. 计数器机制

            信号量的计数器表示可用的资源数。当计数器大于零时,表示有足够的资源可用;当计数器为零时,表示资源已满或不可用,线程需要等待。
  3. 资源预定

            线程在进入临界区之前,先进行信号量的P操作(申请资源)。这种机制确保了临界区中的资源不会被过多的线程访问,从而避免了竞态条件和资源冲突。
  4. 退出临界区

            在离开临界区时,线程会执行V操作(释放资源)。这会增加信号量的计数,并可能唤醒等待资源的其他线程。

【注意:P和V操作必须保证是原子性的,这意味着在执行PV操作时,不会有其他线程干扰。】

与互斥锁的比较

  • 互斥锁:主要用于保护临界区,确保同一时刻只有一个线程能访问临界区。互斥锁不管理资源的计数,仅用于保证互斥。
  • 信号量:不仅提供互斥保护,还可以管理资源的数量。信号量适用于需要控制多个线程对有限资源访问的场景。

信号量的原理

  1. 我们将可能会被多个执行流同时访问的资源叫做临界资源,临界资源需要进行保护否则会出现数据不一致等问题。

  2. 当我们仅用一个互斥锁对临界资源进行保护时,相当于我们将这块临界资源看作一个整体,同一时刻只允许一个执行流对这块临界资源进行访问。

  3. 但实际我们可以将这块临界资源再分割为多个区域,当多个执行流需要访问临界资源时,如果这些执行流访问的是临界资源的不同区域,那么我们可以让这些执行流同时访问临界资源的不同区域,此时不会出现数据不一致等问题。

信号量的优势

信号量(信号灯)本质是一个计数器,是描述临界资源中资源数目的计数器,信号量能够更细粒度的对临界资源进行管理。

在基于阻塞队列的生产者消费者模型章节中,我们在访问阻塞队列时,同一时刻只会有一个线程成功竞争到互斥锁然后进入临界区当中。而只有进入临界区后,该执行流才能去判断阻塞队列中有多少剩余空间或剩余数据。因此我们不仅需要对阻塞队列中的数据条件进行循环判断,还需要在阻塞队列为空或为满时在条件变量处进行等待。

使用信号量其实是对临界资源的预定机制。在进入临界区之前,每个线程都会先去申请临界区资源。这就使得线程不需要进入临界区,就可以预先完成临界资源的分配!同时,信号量也使线程对临界资源的操作更细致、更高效!

通过维护计数器,信号量可以准确地反映当前可用的资源数量。这使得线程在请求和释放资源时,能有效地控制资源的使用情况,避免了资源过度使用或饥饿状态。信号量通过原子操作来管理资源的计数,这可以减少竞争条件和锁争用,尤其是在高并发的环境中。

总的来说,信号量作为对临界资源的预定机制,通过精确控制资源的分配和使用,可以提高多线程程序的效率和稳定性。

二、信号量的操作

1、初始化信号量:sem_init

sem_init 是 POSIX 标准中用于初始化信号量的函数。它用于设置一个信号量对象的初始状态,包括其初始计数值和属性设置。

函数原型

int sem_init(sem_t *sem, int pshared, unsigned int value);

参数说明

  1. sem (sem_t *sem)

    • 这是指向一个 sem_t 类型的信号量对象的指针。这个对象在 sem_init 调用之前需要被分配空间(通常是通过静态分配或动态分配内存)。
  2. pshared (int pshared)

    • 这个参数决定了信号量的共享范围。
    • 如果 pshared 为 0,则信号量是线程间共享的,即同一个进程内的多个线程可以访问它。
    • 如果 pshared 非 0,则信号量是进程间共享的,多个进程可以访问这个信号量。此时,sem 指向的信号量对象必须存储在共享内存区域中(例如,通过 mmap 分配的共享内存)。
  3. value (unsigned int value)

    • 这是信号量的初始值,即信号量计数器的初始值。它表示资源的初始数量或可用数量。

返回值

  • 成功:返回 0。
  • 失败:返回 -1,并设置 errno 以指示错误类型。常见的错误包括 EINVAL(参数无效)或 ENOMEM(内存不足)。

2、信号量申请(P操作):sem_wait

sem_wait 是 POSIX 标准中用于执行信号量的等待操作的函数。它用于请求信号量,通常在多线程或多进程环境中用于实现同步和互斥。 

函数原型

int sem_wait(sem_t *sem);

参数说明

  • sem (sem_t *sem)
    • 这是指向一个已经初始化的信号量对象的指针。信号量对象必须在调用 sem_wait 之前通过 sem_init 或其他方式初始化。

返回值

  • 成功:返回 0。
  • 失败:返回 -1,并设置 errno 以指示错误类型。常见的错误包括 EINTR(操作被信号中断)或 EINVAL(无效的信号量描述符)。

功能和行为

  • 等待信号量:sem_wait 函数会尝试将信号量的计数器减少 1。这个操作是原子性的,即在执行期间不会被中断。

  • 阻塞和挂起

    • 如果信号量的计数器大于 0,sem_wait 会将计数器减 1,并立即返回,允许线程继续执行。
    • 如果信号量的计数器为 0,调用 sem_wait 的线程会被阻塞,直到信号量的计数器变得大于 0。此时,线程会被唤醒,并且信号量计数器会减少 1。
  • 原子性sem_wait 的操作是原子性的,这意味着在 sem_wait 执行过程中,不会有其他线程或进程修改同一个信号量的状态,从而避免竞态条件。

3、信号量的释放(V操作):sem_post

sem_post 是 POSIX 标准中用于释放信号量的函数。它的作用是将信号量的计数器增加 1,从而可能唤醒因等待信号量而被阻塞的线程或进程。

函数原型

int sem_post(sem_t *sem);

参数说明

  • sem (sem_t *sem)
    • 这是指向一个已经初始化的信号量对象的指针。信号量对象必须在调用 sem_post 之前通过 sem_init 或其他方式初始化。

返回值

  • 成功:返回 0。
  • 失败:返回 -1,并设置 errno 以指示错误类型。常见的错误包括 EINVAL(无效的信号量描述符)。

功能和行为

  • 释放信号量

    • sem_post 函数将信号量的计数器增加 1。这个操作是原子性的,即在执行期间不会被中断。
  • 唤醒等待线程

    • 如果有线程因调用 sem_wait 而被阻塞(即信号量计数器为 0),sem_post 会增加信号量的计数器,并唤醒其中一个被阻塞的线程或进程,使其可以继续执行。
  • 计数器上限

    • 在多数实现中,信号量的计数器没有上限,但实际限制取决于系统实现。在某些实现中,信号量计数器的最大值可能受到系统限制,因此要注意避免计数器溢出。

注意sem_post 通常与 sem_wait 配对使用,确保在释放信号量时之前已经有线程因 sem_wait 被阻塞。否则,如果没有线程在等待信号量,sem_post 将增加计数器但不会产生实际的同步效果。

4、信号量的销毁:sem_destroy

sem_destroy 是 POSIX 标准中用于销毁信号量的函数。它的作用是释放与信号量对象相关的资源。在多线程或多进程编程中,使用信号量后,应确保在不再需要它们时调用 sem_destroy 来避免资源泄漏。 

函数原型

int sem_destroy(sem_t *sem);

参数说明

  • sem (sem_t *sem)
    • 这是指向一个已经初始化的信号量对象的指针。该信号量必须在调用 sem_destroy 之前通过 sem_init 进行初始化,并且在调用 sem_destroy 时不应有线程正在等待或使用该信号量。

返回值

  • 成功:返回 0。
  • 失败:返回 -1,并设置 errno 以指示错误类型。常见的错误包括:
    • EINVAL:信号量不是有效的信号量或未被初始化。
    • EBUSY:信号量正在被其他线程使用(即,已经有线程在等待或已经获取了该信号量)。

功能和行为

  • 释放资源

    • sem_destroy 会释放与信号量相关的系统资源。这是重要的一步,尤其在使用动态分配的信号量时,以防止内存泄漏。
  • 适用场景

    • 在信号量不再需要使用时调用,例如在程序结束前或线程完成工作后。

注意事项

  1. 调用时机

    只有在所有与信号量相关的线程已退出或不再使用该信号量时,才能成功地调用 sem_destroy。如果有线程仍在等待该信号量,调用将失败。
  2. 避免重复销毁

    不应多次调用 sem_destroy 来销毁同一个信号量,这可能导致未定义的行为。
  3. 初始化和销毁配对

    每个通过 sem_init 初始化的信号量都应以 sem_destroy 完成销毁,以确保资源的正确管理。

信号量使用代码示例: 

#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <pthread.h>
#include <errno.h>sem_t sem;void* worker(void* arg) {// 等待信号量if (sem_wait(&sem) != 0) {perror("sem_wait failed");pthread_exit(NULL);}// 进入临界区printf("Thread %ld acquired the semaphore.\n", (long)arg);// 模拟工作sleep(2);// 释放信号量if (sem_post(&sem) != 0) {perror("sem_post failed");pthread_exit(NULL);}printf("Thread %ld released the semaphore.\n", (long)arg);pthread_exit(NULL);
}int main() {pthread_t threads[2];int i;// 初始化信号量,初始值为 1if (sem_init(&sem, 0, 1) != 0) {perror("sem_init failed");exit(EXIT_FAILURE);}// 创建线程for (i = 0; i < 2; i++) {pthread_create(&threads[i], NULL, worker, (void*)(long)i);}// 等待线程完成for (i = 0; i < 2; i++) {pthread_join(threads[i], NULL);}// 销毁信号量sem_destroy(&sem);return 0;
}

三、基于环形队列实现的生产者消费者模型

对于环形队列,此前已经在数据结构中做过详细讲解,大家可移步观看。

在数据结构章节中,我们是如何判断环形队列是否为空或者为满呢?——是通过队首坐标和队尾坐标是否相差“1”来判断的。这也就意味着在这种做法下我们需要牺牲一个空间。

实际上,还有一种做法可以对环形队列的状态进行判断——使用一个计数器来实时记录环形队列中元素的个数。当像队列中添加数据时,计数器 +1;当队列弹出数据时,计数器 -1。当计数器为0或为我们所设定的队列最大容量时,代表环形队列为空或已满。

如此,我们就不必使用队首和队尾索引间的关系来对队列的状态进行判断。

而信号量的本质就是计数器,自然也能做上述工作!同时,信号量还可以承担为线程分配资源的功能!

生产者如何向环形队列中添加数据,消费者又如何从队列中提取数据呢?

队列的原则是“先进先出”,当我们向队列中添加元素时,应该从队尾开始添加。而提取元素时,应从队首开始提取。

我们使用数组模拟环形队列,由消费者代码控制队首索引:

//生产者承担添加元素的操作,形参为需要添加的对象的引用void Push(const T& in){//1、申请一个代表空间资源的信号量,该信号量的值 -1 —— P操作P(&_space_nums);//2、去竞争临界区的互斥锁pthread_mutex_lock(&_productor_mutex);//3、向队尾添加元素,并让队尾索引指向下一个位置_ring_queue[_end] = in;_end = (_end + 1) % _max_capacity;//4、解锁pthread_mutex_unlock(&_productor_mutex);//5、此时生产者已经向环形队列中添加了一个数据//这意味着空间资源数量 -1, 数据资源数量 +1,此时对代表数据资源的信号量进行 +1 操作 —— V操作V(&_data_nums);}

由生产者代码来控制队尾索引:

//消费者承担提取元素的任务,形参为指向所提取的元素的指针,是输出型参数void Pop(T* out){//1、申请一个代表数据资源的信号量,该信号量的值 -1 —— P操作P(&_data_nums);//2、去竞争临界区的互斥锁pthread_mutex_lock(&_consumer_mutex);//3、从队首提取元素,并让队首索引指向下一个位置*out = _ring_queue[_front];_front = (_front + 1) % _max_capacity;//4、解锁pthread_mutex_unlock(&_consumer_mutex);//5、此时消费者已经从环形队列中提取了一个数据//这意味着数据资源数量 -1, 空间资源数量 +1,此时对代表空间资源的信号量进行 +1 操作 —— V操作V(&_space_nums);}

生产者关注的是空间资源,消费者关注的是数据资源

对于生产者和消费者来说,它们关注的资源是不同的:

  • 生产者关注的是环形队列当中是否有空间(blank),只要有空间生产者就可以进行生产。
  • 消费者关注的是环形队列当中是否有数据(data),只要有数据消费者就可以进行消费。

blank_sem和data_sem的初始值设置

现在我们用信号量来描述环形队列当中的空间资源(blank_sem)和数据资源(data_sem),在我们初始信号量时给它们设置的初始值是不同的:

  • blank_sem的初始值我们应该设置为环形队列的容量,因为刚开始时环形队列当中全是空间。
  • data_sem的初始值我们应该设置为0,因为刚开始时环形队列当中没有数据。
template <typename T>
class RingQueue
{
private:sem_t _data_nums;//数组资源信号量sem_t _space_nums;//空间资源信号量pthread_mutex_t _productor_mutex;//生产操作临界区的互斥锁pthread_mutex_t _consumer_mutex;//消费操作临界区的互斥锁int _max_capacity; //环形队列的最大容量std::vector<T> _ring_queue;//数组模拟环形队列int _front;//队首索引,由消费者来维护int _end;//队尾索引,由生产者来维护
public:RingQueue(int max_capicity = MAXCAPACITY):_max_capacity(max_capicity), _ring_queue(max_capicity){_front = _end = 0;sem_init(&_data_nums, 0, 0);//数据资源信号量初始化为0sem_init(&_space_nums, 0, _max_capacity);//空间资源信号量初始化为环形队列的最大容量pthread_mutex_init(&_productor_mutex, nullptr);pthread_mutex_init(&_consumer_mutex, nullptr);}
}

生产者申请空间资源,释放数据资源

对于生产者来说,生产者每次生产数据前都需要先申请blank_sem:

  • 如果blank_sem的值不为0,则信号量申请成功,此时生产者可以进行生产操作。
  • 如果blank_sem的值为0,则信号量申请失败,此时生产者需要在blank_sem的等待队列下进行阻塞等待,直到环形队列当中有新的空间后再被唤醒。

当生产者生产完数据后,应该释放data_sem:

  • 虽然生产者在进行生产前是对blank_sem进行的P操作,但是当生产者生产完数据,应该对data_sem进行V操作而不是blank_sem。
  • 生产者在生产数据前申请到的是blank位置,当生产者生产完数据后,该位置当中存储的是生产者生产的数据,在该数据被消费者消费之前,该位置不再是blank位置,而应该是data位置。
  • 当生产者生产完数据后,意味着环形队列当中多了一个data位置,因此我们应该对data_sem进行V操作。

消费者申请数据资源,释放空间资源

对于消费者来说,消费者每次消费数据前都需要先申请data_sem:

  • 如果data_sem的值不为0,则信号量申请成功,此时消费者可以进行消费操作。
  • 如果data_sem的值为0,则信号量申请失败,此时消费者需要在data_sem的等待队列下进行阻塞等待,直到环形队列当中有新的数据后再被唤醒。

当消费者消费完数据后,应该释放blank_sem:

  • 虽然消费者在进行消费前是对data_sem进行的P操作,但是当消费者消费完数据,应该对blank_sem进行V操作而不是data_sem。
  • 消费者在消费数据前申请到的是data位置,当消费者消费完数据后,该位置当中的数据已经被消费过了,再次被消费就没有意义了,为了让生产者后续可以在该位置生产新的数据,我们应该将该位置算作blank位置,而不是data位置。
  • 当消费者消费完数据后,意味着环形队列当中多了一个blank位置,因此我们应该对blank_sem进行V操作。

必须遵守的两个规则

在基于环形队列的生产者和消费者模型当中,生产者和消费者必须遵守如下两个规则。

第一个规则:生产者和消费者不能对同一个位置进行访问。

生产者和消费者在访问环形队列时:

  • 如果生产者和消费者访问的是环形队列当中的同一个位置,那么此时生产者和消费者就相当于同时对这一块临界资源进行了访问,这当然是不允许的。
  • 而如果生产者和消费者访问的是环形队列当中的不同位置,那么此时生产者和消费者是可以同时进行生产和消费的,此时不会出现数据不一致等问题。

如下图:

第二个规则:无论是生产者还是消费者,都不应该将对方套一个圈以上。

  • 生产者从消费者的位置开始一直按顺时针方向进行生产,如果生产者生产的速度比消费者消费的速度快,那么当生产者绕着消费者生产了一圈数据后再次遇到消费者,此时生产者就不应该再继续生产了,因为再生产就会覆盖还未被消费者消费的数据。
  • 同理,消费者从生产者的位置开始一直按顺时针方向进行消费,如果消费者消费的速度比生产者生产的速度快,那么当消费者绕着生产者消费了一圈数据后再次遇到生产者,此时消费者就不应该再继续消费了,因为再消费就会消费到缓冲区中保存的废弃数据。

如下图:

四、完整代码

#include <vector>
#include <pthread.h>
#include <semaphore.h>const int MAXCAPACITY = 6; template <typename T>
class RingQueue
{
private:sem_t _data_nums;//数组资源信号量sem_t _space_nums;//空间资源信号量pthread_mutex_t _productor_mutex;//生产操作临界区的互斥锁pthread_mutex_t _consumer_mutex;//消费操作临界区的互斥锁int _max_capacity; //环形队列的最大容量std::vector<T> _ring_queue;//数组模拟环形队列int _front;//队首索引,由消费者来维护int _end;//队尾索引,由生产者来维护
public:RingQueue(int max_capicity = MAXCAPACITY):_max_capacity(max_capicity), _ring_queue(max_capicity){_front = _end = 0;sem_init(&_data_nums, 0, 0);//数据资源信号量初始化为0sem_init(&_space_nums, 0, _max_capacity);//空间资源信号量初始化为环形队列的最大容量pthread_mutex_init(&_productor_mutex, nullptr);pthread_mutex_init(&_consumer_mutex, nullptr);}void P(sem_t* sem){sem_wait(sem);}void V(sem_t* sem){sem_post(sem);}//生产者承担添加元素的操作,形参为需要添加的对象的引用void Push(const T& in){//1、申请一个代表空间资源的信号量,该信号量的值 -1 —— P操作P(&_space_nums);//2、去竞争临界区的互斥锁pthread_mutex_lock(&_productor_mutex);//3、向队尾添加元素,并让队尾索引指向下一个位置_ring_queue[_end] = in;_end = (_end + 1) % _max_capacity;//4、解锁pthread_mutex_unlock(&_productor_mutex);//5、此时生产者已经向环形队列中添加了一个数据//这意味着空间资源数量 -1, 数据资源数量 +1,此时对代表数据资源的信号量进行 +1 操作 —— V操作V(&_data_nums);}//消费者承担提取元素的任务,形参为指向所提取的元素的指针,是输出型参数void Pop(T* out){//1、申请一个代表数据资源的信号量,该信号量的值 -1 —— P操作P(&_data_nums);//2、去竞争临界区的互斥锁pthread_mutex_lock(&_consumer_mutex);//3、从队首提取元素,并让队首索引指向下一个位置*out = _ring_queue[_front];_front = (_front + 1) % _max_capacity;//4、解锁pthread_mutex_unlock(&_consumer_mutex);//5、此时消费者已经从环形队列中提取了一个数据//这意味着数据资源数量 -1, 空间资源数量 +1,此时对代表空间资源的信号量进行 +1 操作 —— V操作V(&_space_nums);}~RingQueue(){sem_destroy(&_data_nums);sem_destroy(&_space_nums);pthread_mutex_destroy(&_productor_mutex);pthread_mutex_destroy(&_consumer_mutex);}
};

五、生产者消费者模型两种实现方法的对比 

阻塞队列与条件变量的模型

在基于阻塞队列的实现中,生产者和消费者之间使用条件变量进行同步,而整个队列由一个互斥锁保护。这种方法的特点是将队列和同步机制视为一个整体进行管理:

  • 单一互斥锁:使用一把锁来保护整个队列,确保生产者和消费者在访问队列时不会同时进行操作,避免数据竞争和不一致性。
  • 条件变量:用来实现生产者和消费者之间的协调。生产者在队列满时等待,消费者在队列空时等待。条件变量的等待和通知都依赖于持有的互斥锁。

优点

  • 简单:由于只需要一把锁,代码实现和管理都相对简单。
  • 一致性:锁保护了整个队列,确保了在任何时刻,队列的状态是一致的。

环形队列与信号量的模型

环形队列与信号量的实现方法通常涉及到两个独立的锁(或信号量),一个用于管理队列的访问,另一个用于管理队列的空闲和满状态:

  • 两个独立的锁/信号量
    • 互斥锁:用于保护对队列的访问,确保生产者和消费者在对队列进行操作时是互斥的——即保护线程对临界资源操作的原子性。
    • 信号量:用来表示队列中的空位和数据项数量。生产者和消费者通过信号量进行同步,但信号量与互斥锁是分开的。

优点

  • 细化的控制:信号量提供了对队列空闲和满状态的精细控制,使得生产者和消费者可以更高效地进行等待和通知,减少了不必要的等待时间。
  • 独立管理通过信号量,生产者和消费者在访问队列的逻辑上是分开的,互斥锁仅保护对队列的实际操作,而信号量负责同步和状态管理。

为什么环形队列与信号量需要两个锁

  1. 资源管理的独立性

            在环形队列的模型中,互斥锁保护队列的操作(如插入和删除),而信号量管理队列的状态(如队列空闲和满状态)。这两个机制的职责不同,因此需要独立的锁/信号量来实现更精细的控制。
  2. 提高效率

            使用信号量来管理空闲和满状态,可以减少生产者和消费者之间的等待时间。生产者和消费者可以在不同的信号量上进行操作,而不需要在操作队列时额外等待,从而提高了系统的效率。
  3. 避免复杂的锁竞争

            在使用信号量的情况下,生产者和消费者可以在互斥锁保护的范围之外进行状态检查和信号操作,从而减少了对互斥锁的竞争和锁持有的时间。

总结

  • 阻塞队列与条件变量的模型:一个互斥锁保护整个队列,条件变量用于协调生产者和消费者。这种方法简单且一致性好,适合于简单的生产者消费者问题。

  • 环形队列与信号量的模型:两个独立的锁/信号量分别管理队列的访问和状态。这种方法提供了更细粒度的控制,提高了效率,但实现和管理上相对复杂。

简而言之,在基于环形队列实现的模型中,生产者所关注的空间资源信号量和消费者所关注的数据资源信号量提供了对临界资源更精细化的控制。

在阻塞队列模型中,生产者和消费者每次对临界资源的操作都是对整个阻塞队列进行操作。 而在环形队列模型中,由于生产者和消费者管理着环形队列的不同索引,他们对临界资源的操作实际上是两种分开的不同的逻辑。

在阻塞队列中,互斥锁的作用主要是保护阻塞队列的线程安全。同样的,环形队列模型中的互斥锁也是为了保护环形队列的线程安全。不同的是,由于生产者和消费者的代码逻辑是分开独立的,所以生产者和消费者线程各自持有属于各自的互斥锁即可。这样不仅保证了临界资源操作的原子性,还减少了不必要的对互斥锁的竞争操作!

相关文章:

【Linux】POSIX信号量与、基于环形队列实现的生产者消费者模型

目录 一、POSIX信号量概述 信号量的基本概念 信号量在临界区的作用 与互斥锁的比较 信号量的原理 信号量的优势 二、信号量的操作 1、初始化信号量&#xff1a;sem_init 2、信号量申请&#xff08;P操作&#xff09;&#xff1a;sem_wait 3、信号量的释放&#xff08…...

Spring Boot-消息队列相关问题

Spring Boot 消息队列相关问题及解决方案 消息队列&#xff08;Message Queue, MQ&#xff09;在分布式系统中的应用越来越广泛&#xff0c;尤其是在解耦系统、异步通信、负载均衡等场景中起到了至关重要的作用。消息队列为不同的服务提供了一种异步通信的机制&#xff0c;使得…...

[数据集][目标检测]岩石种类检测数据集VOC+YOLO格式4766张9类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;4766 标注数量(xml文件个数)&#xff1a;4766 标注数量(txt文件个数)&#xff1a;4766 标注…...

图像分割基本知识

计算机视觉和图像处理 Tensorflow入门深度神经网络图像分类目标检测图像分割 图像分割 一、目标分割1.1 图像分割的定义1.2 任务类型1.2.1 任务描述1.2.2 任务类型 二、语义分割2.1 FCN网络2.1.1网络结构 2.2 Unet网络 三、UNet案例3.1 数据集获取3.1.1 设置相关信息3.1.2 图像…...

LIN总线CAPL函数——干扰LIN帧响应段(linInvertRespBit )

&#x1f345; 我是蚂蚁小兵&#xff0c;专注于车载诊断领域&#xff0c;尤其擅长于对CANoe工具的使用&#x1f345; 寻找组织 &#xff0c;答疑解惑&#xff0c;摸鱼聊天&#xff0c;博客源码&#xff0c;点击加入&#x1f449;【相亲相爱一家人】&#x1f345; 玩转CANoe&…...

【30天玩转python】网络编程基础

网络编程基础 网络编程是指编写能够在网络上进行通信的程序&#xff0c;通过网络进行数据的发送与接收。Python 提供了许多库和工具来进行网络编程&#xff0c;如 socket、urllib 和 requests。在这篇文章中&#xff0c;我们将介绍网络编程的基础知识&#xff0c;并演示如何使…...

【PCB工艺】如何实现PCB板层间的互连

系列文章目录 1.元件基础 2.电路设计 3.PCB设计 4.元件焊接 5.板子调试 6.程序设计 7.算法学习 8.编写exe 9.检测标准 10.项目举例 11.职业规划 文章目录 前言①、什么是通孔②、通孔是怎样产生的③、通孔种类④、盘中孔⑤、设计建议 前言 送给大学毕业后找不到奋斗方向的你…...

FastAPI--如何自定义Docs UI,包括多个APP、静态资源、元数据等

如何mount 一个FastAPI Application? “Mounting” means adding a completely “independent” application in a specific path, that then takes care of handling everything under that path, with the path operations declared in that sub-application. 示例代码 主…...

【FPGA XDMA AXI Bridge 模式】PCIe:BARs 和 AXI:BARs 含义解析

一. XDMA IP核两种模式 Xilinx的 DMA/Bridge Subsystem for PCI Express IP核中&#xff0c;支持普通的XDMA模式&#xff0c;但是这种模式只允许主机端发起PCIe 读写请求&#xff0c;FPGA内部无法主动发起读写请求&#xff0c;也即FPGA无法主动读写HOST的内存。 而该IP核的另…...

嵌入式-QT学习-小练习

1. 实现多窗口 2. 给按键增加图标 3. 动图展示 结果演示&#xff1a; Mul_Con main.cpp #include "widget.h"#include <QApplication>int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); }一、第一个窗口展示 …...

使用 Flask-Limiter 和 Nginx 实现接口访问次数限制

在现代 Web 应用中&#xff0c;针对敏感接口&#xff08;如短信验证码、登录接口等&#xff09;的访问次数限制至关重要。通过设置合理的限流策略&#xff0c;可以有效防止接口滥用&#xff0c;避免过多的资源消耗&#xff0c;并提升安全性。本文将通过 Nginx 和 Flask-Limiter…...

【数据结构】排序算法---冒泡排序

文章目录 1. 定义2. 算法步骤3. 动图演示4. 性质5. 算法分析6. 代码实现C语言PythonJavaCGo 结语 1. 定义 冒泡排序&#xff08;英语&#xff1a;Bubble sort&#xff09;是一种简单的排序算法。它重复地走访过要排序的数列&#xff0c;一次比较两个元素&#xff0c;如果它们的…...

mysql数据库中事务锁的机制

读锁又称为共享锁&#xff0c;简称S锁&#xff0c;共享锁就是多个事务对于同一数据可以共享一把锁&#xff0c;都能访问到数据&#xff0c;但是只能读不能修改。 写锁又称为排他锁&#xff0c;简称X锁&#xff0c;排他锁就是不能与其他所并存&#xff0c;如一个事务获取了一个…...

并发工具类-CountDownLatch

CountDownLatch 是 Java 中提供的一种非常有用的并发工具类&#xff0c;位于 java.util.concurrent 包中。它可以使一个或多个线程等待其他线程完成一组特定的操作后再继续执行。CountDownLatch 通过维护一个计数器来实现这一点&#xff0c;计数器的初始值由构造函数设定。每当…...

进程的重要函数

进程的重要函数: fork函数 了解fork函数 通过调用fork()函数&#xff0c;则会产生一个新的进程。调用fork()函数的进程叫做 父进程&#xff0c;产生的新进程则为子进程。 其编码过程: 1.函数功能: 函数头文件 #include <sys/types.h> #include <unistd.h> 函数…...

python 实现average median平均中位数算法

average median平均中位数算法介绍 平均&#xff08;Mean&#xff09;和中位数&#xff08;Median&#xff09;是统计学中常用的两个概念&#xff0c;用于描述一组数据的中心趋势&#xff0c;但它们并不是算法&#xff0c;而是数据处理的结果。不过&#xff0c;我可以解释如何…...

HTML概述

1. HTML概述 1.1 HTML定义 HTML超文本标记语言&#xff0c;其中超文本是链接&#xff0c;标记也叫标签&#xff08;即带尖括号的文本&#xff09;。 1.2 HTML基本骨架 HTML基本骨架是网页模板。 <html><head><title>网页的标题</title></head&…...

【FFT】信号处理——快速傅里叶变换【通俗易懂】

快速傅里叶变换&#xff08;Fast Fourier Transform, FFT&#xff09;是一种用于将信号从时间域转换到频率域的算法。 傅里叶变换的核心思想是&#xff1a;任何周期性信号都可以分解成多个不同频率的正弦波或余弦波的叠加。 简单来说&#xff0c;FFT可以帮助我们理解一个信号…...

电脑升级WIN11之后需要注意哪些东西

1.记事本&#xff0c;在前单位时&#xff0c;电脑升级后&#xff0c;记事本会需要手动更新&#xff0c;或手动安装 2.任务栏&#xff0c;WIN11默认任务栏在中间位置&#xff0c;想要调成WIN10一样的位置&#xff0c;分享两个方法 拖拽法&#xff08;适用于Windows 11 2022年1…...

GEE 教程:利用sentinel-5p数据进行长时序CO一氧化碳的监测分析并结合夜间灯光数据分析

目录 简介 数据 哨兵5号 NOAA/VIIRS/DNB/MONTHLY_V1/VCMCF 函数 ui.Chart.image.series(imageCollection, region, reducer, scale, xProperty) Arguments: Returns: ui.Chart 代码 结果 简介 利用sentinel-5p数据进行长时序CO一氧化碳的监测分析并结合夜间灯光数据…...

【教程】鸿蒙ARKTS 打造数据驾驶舱---前序

鸿蒙ARKTS 打造数据驾驶舱 ​ 前面2章我介绍了如何通过定义View绘制箭头以及圆形进度&#xff0c;初步了解了鸿蒙如何进行自定义View。接下来我将通过我最近在带的一个VUE的项目&#xff0c;简单实现了几个鸿蒙原生页面。帮助大家快速上手纯血鸿蒙开发. 本项目基于Api11Stage模…...

Html css样式总结

1.Html css样式总结 CSS 定义 中文名称&#xff1a;层叠样式表 。 英文全称&#xff1a;Cascading Style Sheets &#xff0c;简称CSS。在网页制作时采用CSS技术&#xff0c;可以有效地对页面的布局、字体、颜色、背景和其它效果实现更加精确的控制。 &#xff08;1&#xff09…...

决策树基础概论

1. 概述 在机器学习领域&#xff0c;决策树&#xff08;Decision Tree&#xff09; 是一种高度直观且广泛应用的算法。它通过一系列简单的是/否问题&#xff0c;将复杂的决策过程分解为一棵树状结构&#xff0c;使得分类或回归问题的解决过程直观明了。决策树的最大特点在于可…...

Spring Boot集成Akka Cluster快速入门Demo

1.什么是Akka Cluster&#xff1f; Akka Cluster将多个JVM连接整合在一起&#xff0c;实现消息地址的透明化和统一化使用管理&#xff0c;集成一体化的消息驱动系统。最终目的是将一个大型程序分割成若干子程序&#xff0c;部署到很多JVM上去实现程序的分布式并行运算&#xf…...

django学习入门系列之第十点《A 案例: 员工管理系统10》

文章目录 12 管理员操作12.4 密码加密12.5 获取对象&#xff08;防止id错误--编辑界面等&#xff09;12.6 编辑管理员12.7 重置密码 往期回顾 12 管理员操作 12.4 密码加密 密码不应该以明文的方式直接存储到数据库&#xff0c;应该加密才放进去 定义一个md5的方法&#xff…...

Unity实战案例全解析:PVZ 植物卡片状态分析

Siki学院2023的PVZ免费了&#xff0c;学一下也坏 卡片状态 卡片可以有三种状态&#xff1a; 1.阳光足够&#xff0c;&#xff08;且cd好了可以种植&#xff09; 2.阳光不够&#xff0c;&#xff08;cd&#xff1f;好了&#xff1a;没好 &#xff08;三目运算符&#xff09;&…...

判断变量是否为有限数字(非无穷大或NaN)math.isfinite() 判断变量是否为无穷大(正无穷大或负无穷大)math.isinf()

【小白从小学Python、C、Java】 【考研初试复试毕业设计】 【Python基础AI数据分析】 判断变量是否为有限数字&#xff08;非无穷大或NaN&#xff09; math.isfinite() 判断变量是否为无穷大&#xff08;正无穷大或负无穷大&#xff09; math.isinf() 请问关于以下代码表述错误…...

idea使用阿里云服务器运行jar包

说明&#xff1a;因为我用的阿里云服务器不是自己的&#xff0c;所以一些具体的操作可能不太全面。看到一个很完整的教程&#xff0c;供参考。 0. 打包项目 这里使用的是maven打包。 在pom.xml中添加以下模块。 <build><plugins><plugin><groupId>org…...

解决nginx代理SSE接口的响应没有流式返回

目录 现象原来的nginx配置解决 现象 前后端分离的项目&#xff0c;前端访问被nginx反向代理的后端SSE接口&#xff0c;预期是流式返回&#xff0c;但经常是很久不响应&#xff0c;一响应全部结果一下子都返回了。查看后端项目的日志&#xff0c;响应其实是流式产生的。推测是n…...

11 - TCPClient实验

在上一个章节的UDP通信测试中&#xff0c;尽管通信的实现过程相对简洁&#xff0c;但出现了通信数据丢包的问题。因此&#xff0c;本章节将基于之前建立的WIFI网络连接&#xff0c;构建一个基础的TCPClient连接机制。我们利用网络调试助手工具来发送数据&#xff0c;测试网络通…...