首页 > 编程语言 >线程安全的队列:使用Monitor模式和C++11多线程库

线程安全的队列:使用Monitor模式和C++11多线程库

时间:2023-09-08 11:38:37浏览次数:50  
标签:11 std Monitor 队列 auto 线程 lock 多线程


线程安全的队列:使用Monitor模式和C++11多线程库

引言

在多线程编程中,数据共享是一个关键的问题。如果多个线程需要访问同一个数据结构,不正确的管理会导致数据不一致甚至程序崩溃。本文将介绍如何使用C++11的多线程库和Monitor模式来实现一个线程安全的队列。

Monitor模式

Monitor模式是一种同步原语,用于封装对共享资源的访问。在C++中,我们可以通过组合使用std::mutexstd::condition_variable来实现Monitor模式。

// 定义一个通用的Monitor模板类
template <typename T>
class Monitor {
public:
    // 嵌套结构体,用于管理互斥锁和条件变量
    struct UnlockAndNotify {
        std::mutex d_mutex;  // 互斥锁
        std::condition_variable d_condition;  // 条件变量
        
        // 锁定互斥锁
        void lock() { d_mutex.lock(); }
        
        // 解锁互斥锁,并通知一个等待的线程
        void unlock() { d_mutex.unlock(); d_condition.notify_one(); }
    };

private:
    // 由于可能在const成员函数中需要修改这两个成员,因此将它们声明为mutable
    mutable UnlockAndNotify d_combined;  // UnlockAndNotify实例
    mutable T d_data;  // 存储的数据

public:
    // 用于生产者的锁定函数
    // 返回一个包含数据引用和锁的tuple
    std::tuple<T&, std::unique_lock<UnlockAndNotify>> makeProducerLock() const {
        return { d_data, std::unique_lock{d_combined} };
    }

    // 用于消费者的锁定函数
    // 只有当满足某个条件(PRED predicate)时,才返回数据和锁
    template <typename PRED>
    std::tuple<T&, std::unique_lock<std::mutex>> makeConsumerLockWhen(PRED predicate) const {
        std::unique_lock lock{d_combined.d_mutex};  // 获取锁
        // 等待条件满足
        d_combined.d_condition.wait(lock, [this, predicate]{ return predicate(d_data); });
        return { d_data, std::move(lock) };  // 返回数据和锁
    }
};

线程安全的队列实现

我们定义了一个名为ThreadQueue的模板类,它使用一个Monitor实例来封装其内部的std::deque

// 定义一个模板类 ThreadQueue
template <typename T>
class ThreadQueue {
    // 使用 Monitor 模板类封装一个 std::deque
    // 以保证其线程安全性
    Monitor<std::deque<T>> d_monitor;

public:
    // 添加一个元素到队列中
    void add(T number) {
        // 使用 Monitor 的 makeProducerLock 方法获取一个唯一锁和队列引用
        // 这确保了在添加元素时队列不会被其他线程修改
        auto[numberQueue, lock] = d_monitor.makeProducerLock();
        
        // 在获取到锁的情况下,将元素添加到队列的末尾
        numberQueue.push_back(number);
    }

    // 从队列中移除并返回一个元素
    T remove() {
        // 使用 Monitor 的 makeConsumerLockWhen 方法在满足某个条件(队列非空)时
        // 获取一个唯一锁和队列引用
        auto[numberQueue, lock] = d_monitor.makeConsumerLockWhen([](auto& numberQueue) { return !numberQueue.empty(); });
        
        // 在获取到锁和确认队列非空的情况下,从队列前端移除一个元素
        const auto number = numberQueue.front();
        numberQueue.pop_front();
        
        // 返回被移除的元素
        return number;
    }
};

添加元素

add函数中,我们首先使用makeProducerLock方法获取一个锁和队列的引用。然后,我们在获取锁的情况下,安全地将元素添加到队列中。

移除元素

remove函数中,我们使用makeConsumerLockWhen方法。该方法会等待队列非空的条件成立,然后才获取锁和队列的引用。

测试

class Dice {
public:
    int operator()(){ return rand(); }
private:
    std::function<int()> rand = std::bind(std::uniform_int_distribution<>(1, 6), 
                                          std::default_random_engine());
};

int main(){
    
    std::cout << '\n';
    
    constexpr auto NumberThreads = 100;
    
    ThreadQueue<int> safeQueue;                     

    auto addLambda = [&safeQueue](int val){ safeQueue.add(val);         
                                            std::cout << val << " "
                                            << std::this_thread::get_id() << "; "; 
                                          }; 
    auto getLambda = [&safeQueue]{ safeQueue.remove(); };  

    std::vector<std::thread> addThreads(NumberThreads);
    Dice dice;
    for (auto& thr: addThreads) thr = std::thread(addLambda, dice());

    std::vector<std::thread> getThreads(NumberThreads);
    for (auto& thr: getThreads) thr = std::thread(getLambda);

    for (auto& thr: addThreads) thr.join();
    for (auto& thr: getThreads) thr.join();
    
    std::cout << "\n\n";
     
}

文章由ChatGPT-4模型协助完成。

参考:Thread-Safe Queue: Two Serious Errors


标签:11,std,Monitor,队列,auto,线程,lock,多线程
From: https://blog.51cto.com/u_16062556/7407960

相关文章

  • 【230908-11】六对数函数图像比较
    【图像】【代码】<!DOCTYPEhtml><htmllang="utf-8"><metahttp-equiv="Content-Type"content="text/html;charset=utf-8"/><head><title>六对数函数比较</title><styletype="text/css"&......
  • CF1103C 题解
    2023-09-0514:52:07solution找路径很好找,我们随便跑个dfs树找个深度\(\ge\frac{n}{k}\)的路径输出即可。可是怎么找\(k\)个长度不是\(3\)的倍数的环呢?既然我们跑了dfs树,那么就没有横叉边,对于叶子节点非树边只有返祖边,然后一看这个很奇怪的条件——保证每个点度数大......
  • 集美大学计算2111张艺勇
    这个作业属于哪个课程计算2111这个作业要求在哪里工程概论开篇(第一次作业)这个作业的目标Github和Git工具的使用,Markdown文档编写,工程概论课程初步了解自我介绍我来自集美计算机工程学院,曾任科创委员、文体委员、辅导员助理、党建助手,现任班助,院就业会主......
  • qt程序调用cuda-11.7,cmake编译时,提示:"CMakeCUDACompilerId.cu" failed. Compiler:
    报错显示:Running/home/wc/software/cmake-3.26.3-linux-x86_64/bin/cmake/home/wc/work/junke_src/missile-sim'-GCodeBlocks-UnixMakefiles'in/home/wc/work/junke_src/build/debug.CMakeErrorat/home/wc/software/cmake-3.26.3-linux-x86_64/share/cmak......
  • jiangyuchen12码风 截至 2022-12-27 11:09
    最后一条码风改之前的记录那么多人的博客都有TA的码风,我也写一下吧头文件一般使用万能头文件,因为绝对看不到[Error]'***'doesnotnameatype之类的错误常量有,一般是K,N,看题目变量名字变量输入&输出一般用cin,cout大括号写题是这样while(1){……}具体......
  • std list多线程使用
    #include<iostream>#include<list>#include<thread>#include<mutex>#include<condition_variable>#include<unistd.h>std::list<int>my_list;std::mutexmy_mutex;std::condition_variablemy_cond;voidadd_ele......
  • 外汇110网:交易止损里的“门道”:计划止损与突发止损
    止损是投资者必备的交易技巧。一般来说,最后的亏损是因为没有执行盈亏比3:1设定的止损而产生计划外超额止损,而这种损失大多就是来自所谓的突发性止损。那么,计划止损与突发止损这两种止损方式究竟有什么不同,我们又应该采取什么策略去处理和应对呢? 计划止损 单从表面意思看,计划止损就......
  • 【售价68元】天嵌T113核心板上新
    核心板参数表:CPU架构全志Cortex-A7主频800MHz内存128MBDDR3存储8GB eMMc5.1(4G/16G/32G可选)操作系统Linux电源输入3.1V~5V尺寸38*38mm接口方式邮票孔层数4层无铅工艺LVDS/RGB/DSI支持双路8位LVDS,1080P;RGB,18bit,1080P;支持24bit3、MIPI_DSI,4通道,1080P;LVDS、RGB和MIPI_D......
  • 天嵌T113核心板上新
    天嵌T113核心板上新、核心板参数表:CPU架构全志Cortex-A7主频800MHz内存128MBDDR3存储8GBeMMc5.1(4G/16G/32G可选)操作系统Linux电源输入3.1V~5V尺寸38*38mm接口方式邮票孔层数4层无铅工艺LVDS/RGB/DSI支持双路8位LVDS,1080P;RGB,18bit,1080P;支持24bit3、MIPI_DSI,4通道,1080P......
  • Heritrix的多线程ToeThread和ToePool
    4、Heritrix的多线程ToeThread和ToePool要想更有效更快捷地抓取网页内容,则必须采用多线程。Heritirx提供了一个标准的线程池ToeThread,用于管理所有的抓取线程。org.archive.crawler.frameworkClassToePooljava.lang.Objectjava.lang.ThreadGrouporg.archi......