Wait Condition 例子

本文翻译自Qt帮助文档,介绍了一个使用QWaitCondition和QMutex控制循环缓冲区访问的多线程编程例子。生产者线程填充缓冲区,消费者线程读取并打印数据。QWaitCondition允许更高级的并发控制,解决了生产者-消费者问题。文中详细解释了缓冲区、等待条件、互斥锁和线程交互的工作原理。

翻译自Qt帮助文档:Wait Condition Example

用Qt证明多线程编程。

Wait Condition 例子展示了如何使用 QWaitCondition 和 QMutex 来控制对一个循环缓冲区的访问,生产者线程和消费者线程共享该循环缓冲区。

生产者线程往缓冲区写数据,直到缓冲区的尾部,然后从缓冲区开头重新开始重写已经存在的数据。当数据被生产出来后消费者线程读取数据并写道标准错误中。

Wait Conditions 使高级并发成为可能,比单独使用mutexes的可能性大。如果想访问QMutex控制的缓冲区,消费者线程和生产者线程不能同时访问缓冲区。然而,让两个线程同时访问缓冲区的不同部分是没有什么害处的。

这个例子包括两个类:生产者(Producer)和 消费者(Consumer)。它们都继承了QThread。这两个类通过循环缓冲区交互,保护缓冲区的同步工具设为全局变量。

使用 QWaitCondition 和 QMutex 可以解决生产者-消费者的问题,另一种替代的解决方法就是使用QSemaphore。


全局变量

让我们从循环缓冲区和相关的同步工具开始吧:

const int DataSize = 100000;
const int BufferSize = 8192;
char buffer[BufferSize];

QWaitCondition bufferNotEmpty;
QWaitCondition bufferNotFull;
QMutex mutex;
int numUsedBytes = 0;


DataSize 是生产者将要产生的数据量大小。为了尽量使例子简单些,将它设为常量。

BufferSize是循环缓冲区的大小。它比DataSize小,意味着某一时候生产者将会到达缓冲区的尾部,然后重新回到缓冲区起始端。

为了同步生产者和消费者,我们需要两个 wait condition和一个mutex。当生产者已经产生一些数据时 发送bufferNotEmpty condition 信号,告诉消费者它可以开始读数据了。当消费者已经读到一些数据时 发送 bufferNotFull condition信号,告诉生产者它可以产生更多的数据了。numUsedBytes是缓冲区中包含了数据的字节数。

同时,wait conditions,mutex,以及numUsedBytes计数器 确保了生产者永远不会超过消费者 BufferSize 字节,并且消费者永远不会去读生产者还未产生出来的数据。


生产者类

让我们从生产者的类开始吧:

class Producer:public QThread
{
public:
    Producer(QObject *parent=NULL):QThread(parent)
    {
    }
    void run()
    {
        qsrand(QTimer(0,0,0).secsTo(QTime::currentTime()));
        
        for(int i=0; i<DataSize; ++i){
            mutex.lock();
            if(numUsedBytes == BufferSize)
            {
                <span style="background-color: rgb(255, 255, 153);">bufferNotFull.wait(&mutex);</span>
            }
            mutex.unlock();

            buffer[i%BufferSize] = "ACGT"[(int)qrand()%4];

            mutex.lock();
            ++numUsedBytes;
            <span style="background-color: rgb(255, 204, 255);">bufferNotEmpty.wakeAll();</span>
            mutex.unlock();
        }
    }
};
生产者产生DataSize字节数据。在它往循环缓冲区些一个字节前,它必须先检查缓冲区是否满了(即 numUsedBytes=BufferSize)。如果缓冲区已满,该线程等待 bufferNotFull condition。

最后,生产者使用mutex 增加 numUsedBytes 变量值,如果 numUsedBytes > 0 就通知condition bufferNotEmpty 是true。

我们通过mutex控制所有对numUsedBytes 变量的访问。另外,QWaitCondition::wait() 函数将mutex作为实参。在线程进入睡眠状态前解锁mutex,并当线程被唤醒时锁住mutex。而且,从锁住状态转换到等待状态是原子的(即支持多线程并发),是为了竞争条件的产生。

消费者类

让我们转到消费者类:

class Consumer:public QThread
{
    Q_OBJECT
public:
    Consumer(QObject *parent=NULL):QThread(parent)
    {
    }
    void run()
    {
        for(int i=0; i<DataSize; ++i){
            mutex.lock();
            if(numUsedBytes == 0)
            {
                 <span style="background-color: rgb(255, 204, 255);">bufferNotEmpty.wait(&mutex)</span>;
            }
             mutex.unlock();

             fprintf(sederr,"%c",buffer[i%BufferSize]);

             mutex.lock();
             --numUsedBytes;
            <span style="background-color: rgb(255, 255, 153);"> bufferNotFull.wakeAll();</span>
             mutex.unlock();
        }
        fprintf(stderr,"\n");
    }

signals:
    void stringConsumed(const QString &text);
};
这个代码与生产者类似。在读字节之前,我们检查缓冲区是否是空的(numUsedBytes =0),而不是检查它是不是满的,如果它时空的就等待 bufferNotEmpty condition。在我们读到字节之后,我们减少 numUsedBytes(而不是增加),然后给 bufferNotFull condition 发信号(而不是 bufferNotEmpty condition)。

主函数

在主函数中,我们创建两个线程,并调用QThread::wait()来确保两个线程在退出前有时间完成。

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
    Producer producer;
    Consumer consumer;
    producer.start();
    consumer.start();
    producer.wait();
    consumer.wait();
    return 0;
}
那么当我们运行程序时会发生什么呢?刚开始的时候,生产者线程时唯一的一个可以做事情的线程,消费者被阻塞等待bufferNotEmpty condition(numUsedBytes=0)触发。一旦生产者往缓冲区中写入一个字节,numUsedBytes = BufferSize-1,并且bufferNotEmpty condition被触发。同时,有可能发生两件事:要么消费者线程接着执行,要么生产者接着产生第一个字节。

这个例子展示的生产者-消费者模式使那些写出高并发的多线程应用程序称为可能。在一个多处理器机器上,这个程序的效率可能是那些基于mutex的程序两倍,因为处理缓冲区的不同部分时可以运行两个线程。

注意,尽管经常未意识到这些好处,获取和释放一个 QMutex 需要付出一个代价。事实上,这样做是值得的:将缓冲区分成数块(chunks),然后操作块而不是单个字节。基于实验,选取缓冲大小时要留心。







### C++ `condition_variable` 的 `wait_for` 方法 #### 使用场景 当希望线程等待特定条件发生,但又不想无限期挂起时,`std::condition_variable` 提供的 `wait_for` 函数非常有用。此函数允许设置最大等待时间,如果在此时间内未满足条件,则继续执行后续逻辑[^1]。 #### 参数说明 - **pred**: 预测表达式, 用于判断唤醒条件是否成立。 - **rel_time**: 相对超时时间, 表示最多愿意等待的时间长度。 #### 返回值解释 该方法返回一个布尔值,表示是否有因预测变为真而提前退出的情况;若是因为超时则返回假。 #### 示例代码 下面是一个简单的例子展示如何利用 `wait_for` 来控制线程的行为: ```cpp #include <iostream> #include <thread> #include <mutex> #include <chrono> #include <condition_variable> int data = 0; bool ready = false; std::mutex mtx; std::condition_variable cv; void producer() { std::this_thread::sleep_for(std::chrono::seconds(2)); // Simulate delay before producing the value. std::lock_guard<std::mutex> lock(mtx); data = 42; // Produce some meaningful result here... ready = true; cv.notify_one(); // Notify one of the waiting threads that work is done. } void consumer() { std::unique_lock<std::mutex> lock(mtx); while (!ready && !cv.wait_for(lock, std::chrono::milliseconds(500), []{return ready;})) { std::cout << "Still waiting...\n"; } if (ready) { std::cout << "Data received: " << data << '\n'; } else { std::cout << "Timeout occurred without receiving any data.\n"; } } int main() { std::thread t1(producer); std::thread t2(consumer); t1.join(); t2.join(); return 0; } ``` 在这个例子中,生产者线程模拟了一个耗时操作之后设置了共享变量并通知消费者线程。消费者尝试通过调用带有超时期限的 `wait_for` 进行有条件地等待直到接收到信号或达到指定时限为止。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值