【C++11多线程与并发编程】(3)解决生产者与消费者问题,写手与读者问题

本文详细介绍了C++11中的多线程并发编程实践,通过生产者-消费者问题和写手-读者问题的示例,展示了如何实现线程安全的库存管理和文件共享,以避免竞态条件和死锁。

C++11 多线程与并发编程(3)


C++11多线程与并发编程

练习题目1:生产者——消费者问题

假设有一个库存管理系统,有生产者和消费者两种角色。生产者负责生产产品并将其放入库存中,而消费者则负责从库存中取出产品并进行消费。

题目描述如下:

在一个库存管理系统中,有若干个库存位置,用于存放产品。生产者会生产产品,并将其放入库存中。消费者会从库存中取出产品进行消费。每个库存位置最多只能存放一个产品。

生产者和消费者的行为必须是线程安全的,以避免竞态条件和死锁。

请设计一个程序,实现以下功能:

  1. 生产者可以生产产品,并将其放入库存中。
  2. 消费者可以从库存中取出产品进行消费。
  3. 如果库存已满,生产者必须等待,直到有位置空出来。
  4. 如果库存为空,消费者必须等待,直到有产品可用。

解题点:

  1. 生产者和消费者是互斥:临界区一次仅供一位访问
  2. 生产者和消费者需要同步:生产了,消费者才有得用;消费者用了产生空盒,生产者生产了才有地方放。

以下是一种代码解法:

#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <iostream>
#include <chrono>
#include <atomic> // 使用atmomic变量来表示标志位

using namespace std;

class Storage
{
public:
    // 使仓库只有一个
    static Storage *getInstance()
    {
        call_once(flag, []()
                  { instance = new Storage(); });
        return instance;
    }

    void addProduct(int newgood)
    {
        storage_queue.push(newgood);
    }

    int getProduct()
    {

        int top = storage_queue.front();
        storage_queue.pop();
        return top;
    }

    bool isEmpty()
    {
        return storage_queue.empty();
    }

    // 如果仓库有十个就算满了
    bool isFull()
    {
        return storage_queue.size() == 10 ? true : false;
    }

private:
    Storage() : storage_queue() {}
    static Storage *instance; // 单例实例指针
    static once_flag flag;    // 标记初始化是否完成
    // 设置仓库队列
    queue<int> storage_queue;
};

Storage *Storage::instance = nullptr;
std::once_flag Storage::flag;

condition_variable ptoc_cv;          // 用于生产者通知消费者,咱有货了
condition_variable ctop_cv;          // 用于消费者通知生产者,咱有空位了
mutex mtx;                           // 互斥量
atomic<bool> producerStopped(false); // 标志位,用于表示生产者已停止生产

void Product(Storage *stg)
{
    for (int i = 0; i < 20; i++)
    {
        unique_lock<mutex> ulg(mtx);

        // 阻塞当前线程,并且需要在收到通知并且仓库不为空时,才唤醒当前线程
        ctop_cv.wait(ulg, [&stg]() { return !stg->isFull(); });

        stg->addProduct(i);
        ptoc_cv.notify_one();
    }

    producerStopped = true;
    cout << "All commodities have been produced!" << endl;
    ptoc_cv.notify_all();
}

void Comsumer(Storage *stg, vector<int> &custmer)
{
    while (true)
    {
        unique_lock<mutex> ulg(mtx);

        // 停止生产且库存为0,跳出循环
        if (producerStopped && stg->isEmpty())
        {
            return; // 使用 return 语句退出函数
        }
        ptoc_cv.wait(ulg, [&stg]() { return !stg->isEmpty() || producerStopped; });

        if (!stg->isEmpty()) {
            custmer.push_back(stg->getProduct());
            ctop_cv.notify_one();
        }
    }
}

int main()
{
    Storage *stg = Storage::getInstance();
    thread producter(Product, stg);
    vector<int> v1;
    thread comsumer_1(Comsumer, stg, ref(v1));

    producter.join();
    comsumer_1.join();

    cout << "The comsumer v1 has these products:" << endl;
    for (auto v : v1)
    {
        cout << v << " ";
    }
    cout << endl;

    return 0;
}

image-20240416155905955

运行结果

练习题目2:写手——读者问题

假设有一个共享资源,比如一个文件,多个读者可以同时从中读取数据,但是写者在写入数据时需要独占资源。写者和读者之间应该通过适当的同步机制来确保数据的一致性和正确性。

你的任务是实现一个写者-读者问题的解决方案,确保以下要求:

  1. 多个读者可以同时读取共享资源的数据,读操作不会相互影响。
  2. 写者在写入数据时,需要独占共享资源,此时不允许其他读者或写者访问共享资源。
  3. 写者完成写入操作后,允许其他读者继续读取数据,或者允许其他写者进行写操作。
  4. 保证程序的正确性和线程安全性,避免出现竞态条件和死锁。

读写

解题思路:

  • 使用文件访问锁与写作锁分工合作,设置访问计数count。
  • 写与写互斥,写与读互斥,读与读之间可以同时进行。

如果先拿到锁的是读者:

  • 在读之前,先获得写锁,防止写手访问,增加一名读者增加访问计数
  • 通过访问计数,我们只需要在首次读者访问时,对文件进行上锁
  • 每一名读者读完可以立刻释放写作锁:
    • 若此时写作锁被读者获取,新读者可以立刻开始线程进行阅读(从微观上可以有多个读线程同时运行),加入访问队列
    • 若此时写作锁被写手获取,只需要等待当前已有的访问队列进行完,可以立刻开始写作
  • 等当前读者队列全部结束,访问计数归零,释放文件锁。

如果先拿到锁的是写手:

  • 阻塞其他线程(不管是写手,还是读者)
  • 执行程序,结束时释放锁。

从获锁顺序上,读者和写手都是A、B的先后顺序,避免死锁。

以下代码写法,读者和写手公平竞争,但是写手之间可能存在插队的情况,而且每次运行顺序结果也许不同。(因为笔者想要的是随机结果,所以不再修正代码,根据需求可以自己编写自己的写法)

#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <iostream>
#include <random>

using namespace std;

mutex writer_mtx;        // 写手权益锁
mutex file_mtx;          // 文件权锁
mutex count_mtx;         // 互斥访问计数
condition_variable f_cv; // 同步机制
int count = 0;

string fiction = "Harry Potter"; // 原始文本文件

// 用于随机生成一个大写字母
random_device rd;
default_random_engine random(rd());
uniform_int_distribution<int> dis('A', 'Z');

void WriteFiction(int wirter_number)
{
    // 获取写手权限
    unique_lock<mutex> wulg(writer_mtx);
    // 获取文件权限
    unique_lock<mutex> fulg(file_mtx, defer_lock);
    while (true)
    {
        if (fulg.try_lock())
        {
            // 生成随机大写字母
            char newChar = dis(random);
            fiction += newChar;
            cout << "Writer " << wirter_number << " wrote over, she add the char \'" << newChar << "\' " << endl;
            return;
        }
        else
        {
            f_cv.wait(wulg);    // 无法锁定文件,释放写入锁
            // 休眠一下
            this_thread::sleep_for(chrono::milliseconds(300));
        }
    }
}

void ReadFiction(int read_number)
{
    writer_mtx.lock();
    if (++count == 1)
    {
        // 无论多少个人阅读,都只对写锁上锁一次
        file_mtx.lock();
    }

    // 阅读进行时
    cout << "read_number is: " << read_number << endl;
    cout << "the Fiction is :" << fiction << endl;

    writer_mtx.unlock();    // 已读完,可以释放写手锁,
    f_cv.notify_one();      f_cv.notify_one();
    
    if (--count == 0)
    {
        // 已经没有读者,可以释放写手锁给写手
        file_mtx.unlock();
    }
}

int main(){
    thread w1(WriteFiction,1);
    thread r1(ReadFiction, 1);
    
    thread r2(ReadFiction, 2);
    thread r3(ReadFiction, 3);
    thread w2(WriteFiction, 2);
    thread r4(ReadFiction, 4);

    r1.join();
    r2.join();
    r3.join();
    w1.join();
    w2.join();
    r4.join();
    return 0;
}

image-20240416203805852

运行结果1

image-20240416203910737

运行结果2

参考:

[1] 面试必考的:并发和并行有什么区别?

[2] C++网络编程高并发 讲师:陈子青

[3] cppreference——thread

[4] cppreference——mutex

[5] 帝江VII ——c++11 call_once用法(多线程时仅初始化一次的完美解决方案)

评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值