Qt4_同步线程

同步线程

对于多线程应用程序,一个最基本要求就是能实现几个线程的同步执行。 Qt 提供了以下这几个用于同步的类:QMutex、QReadWriteLock、QSemaphore、QWaitCondition。

QMutex 类提供了一种保护一个变量或者一段代码的方法,这样就可以每次只让一个线程读取它。这个类提供了一个lock()函数来锁住互斥量 (mutex)。如果互斥量是解锁的 (unlock),那么当前线程就立即占用并锁定(lock) 它;否则,当前线程就会被阻塞,直到掌握这个互斥量的线程对它解锁为止。

以上述两种方式中的任意一种对lock() 调用返回时,当前线程都会保持这个互斥量,直到调用 unlock() 为止。 QMutex 类还提供了一个tryLock() 函数,如果该互斥量已经锁住,它就会立即返回。

例如,假设我们想使用 QMutex 来保护前一节中Thread 类的 stopped 变量,则应当向Thread类中添加如下的成员变量:

private:
    ...
    QMutex mutex;
};

函数run()也需要相应修改:

void Thread::run()
{
    forever
    {
        mutex.lock();
        if(stopped)
        {
           stopped = false;
           mutex.unlock();
           break;
        }
        mutex.unlock();
        std::cerr << qPrintable(messageStr);
   }
   std::cer << std::endl;
}

stop()函数则需要修改为:

void Thread::stop()
{
    mutex.lock();
    stopped = true;
    mutex.unlock();
}

在一些复杂函数或是在抛出 C++ 异常的函数中锁定和解锁互斥量,非常容易发生错误。Qt提供了方便的 QMutexLocker 类来简化对互斥量的处理。 QMutexLocker 的构造函数接受 QMutex为参数并且将其锁住。 QMutexLocker的析构函数则对这个互斥量进行解锁。例如,我们可以重新编写之前介绍过的 run()和stop()函数如下:

void Thread::run()
{
    forever
    {
        {
            QMutexLocker locker(&mutex);
            if(!stopped)
            {
                stopped = false;
                break;
            }
        }
        std::cerr << qPrintable(messageStr);
    }
    std::cerr << std::endl;
}

void Thread::stop()
{ 
     QMutexLocker locker(&mutex);
     stopped = true;
}

使用互斥量的一个问题在于:每次只能有一个线程可以  同一变量。在程序中,可能会有许多线程同时尝试访问读取某一变量(不是修改该变量),此时,互斥量可能就会成为一个严重的性能瓶颈。在这种情况下,可以使用QReadWriteLock,它是一个同步类,允许同时执行多个 取而不会影响性能。

在Thread 类中,用QReadWriteLock 替换QMutex 来保护s topped 变量毫无意义,因为在任意给定时刻,最多只有一个线程会试图读取这个变量。一个更为恰当的实例,是在程序中应当包含一个或多个访问某些共享数据的阅读程序线程,并且还有一个或多个修改这些共享数据的写入程序线程。例如:

MyData data;
QReadWriteLock lock;

void ReaderThread::run()
{
    ...
    lock.lockForRead();
    access_data_without_modifying_it(&data);
    lock.unlock();
    ...
}

void WriteThread::run()
{
    ...
    lock.lockForWrite();
    modify_data(&data);
    lock.unlock();
    ...
}

为简便起见,我们可以使用QReadLbcker 类和QWriteLocker 类对QReadWriteLock 进行锁定和解锁。

QSemaphore 是互斥量的另外一种泛化表示形式,但与读-写锁定不同,信号量(semaphore) 可以用于保护(guard) 一定数量的相同资源。下面的两小段程序代码给出了QSemaphore 和QMutex 之间的对应关系:
在这里插入图片描述
通过把1传递给构造函数,就告诉这个信号量它控制了一个单一的资源。使用信号量的优点是可以传递除1之外的数字给构造函数,然后可以多次调用acquire() 来获取大量资源。

一个典型的信号量应用程序是当两个线程间传递一定量的数据(DataSize) 时。两个线程会使用某一特定大小(BufferSize)的共享环形缓冲器(circular buffer):

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

生产者线程向缓冲器中写入数据,直到它到达缓冲器的终点为止;然后它会再次从起点重新开始,覆盖已经存在的数据。消费者线程则会读取生成的数据。图14.2 阐明了这一情况,其中假设使用的只是一个很小的16 字节缓冲器。
在这里插入图片描述
在生产者一消费者实例中对于同步的需求有两个部分:如果生产者线程生成数据的速度太快,那么将会把消费者线程还没有读取的数据覆盖掉;如果消费者线程读取数据的 度过快,那么它将会跃过生产者线程而读取一些垃圾数据。

解决这一问题的一个粗略的方法是让生产者线程填满缓冲器,然后等待消费者线程读取完缓冲器中全部数据为止。然而,在多处理器的机器上,让生产者和消费者两个线程分别同时操作缓冲器的两个线程分别同时操作缓冲器的两个部分则要比前面的方案快得多。

因此,解决这一问题的一个更为有效的方法是使用两个信号:

QSemaphore freeSpace(BufferSize);
QSemaphore usedSpace(0);

freeSpace 信号量控制生产者线程写入数据的那部分缓冲器,usedSpace 信号量则控制消费者线程读取数据的那部分缓冲器区域。两个区域是相互补充的。我们用BufferSize出(4096)的初始化freeSpace 信号,这也就意味着它至多可以获得4096 字节的缓冲器资源。在启动这个应用程序时,阅读程序线程(reader thread)将会开始获得"自由的(free)字节并且把它们转换为"用过"的(used) 字节。我们用
0初始化usedSpace 信号量,以确保消费者线程不会在一开始就读取到垃圾数据。

对于这个实例来说,每一个字节都计为一个资源。在实际债用的应用程序中,很可能会在更大的单元(例如,一次64 字节或256 字节)上进行操作,这样可以相应减少信号 的开销。

void Producer::run()
{
    for (int i = 0; i < DataSize; ++i) {
        freeSpace.acquire();
        buffer[i % BufferSize] = "ACGT"[uint(std::rand()) % 4];
        usedSpace.release();
    }
}

在生产者线程中,每次反复写入都是从获取一个"自由"字 开始的。如果该缓冲器中充满了消费者线程还没有读取的数据,那么对acquire()的调用就会被阻塞,直到消费者线程开始"消费"这些数据为止。一旦获取了这一字节,就用一些随机数据(如"A"、 “C” 、“G” 或 “T”)进行写入,并
且把这个字节释放为"用过的"字节,以便让消费这线程读取到。

void Consumer::run()
{
    for (int i = 0; i < DataSize; ++i) {
        usedSpace.acquire();
        std::cerr << buffer[i % BufferSize];
        freeSpace.release();
    }
    std::cerr << std::endl;
}

在消费者线程中,我们从获取一个"用过的"字节开始。如果缓冲器中没有任何可读的数据,那么将会阻塞对acquire()调用,直到生产者线程生成一些可读的数据为止。一旦获取到这个字节,就打印出它并且把这个字节释放为"自由的"字节,这样生产者线程就可以再次使用其他的数据复写它。

int main()
{
    Producer producer;
    Consumer consumer;
    producer.start();
    consumer.start();
    producer.wait();
    consumer.wait();
    return 0;
}

最后,在main() 中,我们开启生产者和消费者线程。然后 生产者线程把一些"自由的"空间转换成"用过的"空间,而消费者线程则可以把它再次转换为"自由的"空间。

当运行这个程序时,它会把一个含有 100000个"A" 、“C”、“G”、“T"的随机序列写入控制台中,然后终止。为了真正理解到底发生了什么,我们可以禁用写入输出数据 ,而让生产者线程在每次产生一个字节时输出一个"P”,让消费者线程在每次读取一个字节时输出一个"c"。为了更加简单地跟踪这一过程,我们可以使用较小DataSize 和BufferSize 值。
例如,这里给出的是当使用DataSize 等于10 且BufferSize 等于4 时的可能的运行结果:
“PcPcPcPcPcPcPcPcPcPc” 。在这种情况下,只要生产者线程一产生字节,消费线程者就会迅速读取它。也就是说,这两个线程的执行速度是一样的。另外一种可能是,在消费者开始读取缓存之前,生产者线程就已经写满了整个缓冲器,这样输出的结果就会是:“PPPPccccPPPPccccPPcc”。还有很多其他可能的情形。信号为系统特定的线程调度程序提供了很大程度上的 自由,利用线程调度程序可以研究这些线程的行为并且可以用于选择一个适当的调度策略。

semaphores.cpp

#include <QtCore>
#include <iostream>

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

QSemaphore freeSpace(BufferSize);
QSemaphore usedSpace(0);

class Producer : public QThread
{
public:
    void run();
};

void Producer::run()
{
    for (int i = 0; i < DataSize; ++i) {
        freeSpace.acquire();
        buffer[i % BufferSize] = "ACGT"[uint(std::rand()) % 4];
        usedSpace.release();
    }
}

class Consumer : public QThread
{
public:
    void run();
};

void Consumer::run()
{
    for (int i = 0; i < DataSize; ++i) {
        usedSpace.acquire();
        std::cerr << buffer[i % BufferSize];
        freeSpace.release();
    }
    std::cerr << std::endl;
}

int main()
{
    Producer producer;
    Consumer consumer;
    producer.start();
    consumer.start();
    producer.wait();
    consumer.wait();
    return 0;
}

在这里插入图片描述

对于使生产者和消费者线程同步的这个问题,另一解决方案是使用QWaitCondition 和QMutex。QWaitCondidon 允许一个线程在满足一定的条件下触发其他多个线程。这样就可以比只使用互斥量提供更为精确的控制。为了说明它是如何工作的,我们将会使用等待条件来重新完成上述的生产者-消费者例子。

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

除了缓冲器之外,我们还声明了两个QWaitCondition 、一个QMutex 和一个变量,该变量用来存储在缓冲器中有多少个"用过的"字节。

void Producer::run()
{
    for (int i = 0; i < DataSize; ++i) {
        mutex.lock();
        while (usedSpace == BufferSize)
            bufferIsNotFull.wait(&mutex);
        buffer[i % BufferSize] = "ACGT"[uint(std::rand()) % 4];
        ++usedSpace;
        bufferIsNotEmpty.wakeAll();
        mutex.unlock();
    }
}

在生产者线程中,首先检查缓冲器是否已经写满。如果它已经备写满,就等待"缓冲器非写满"这一条件。当满足这个条件时,就向缓冲器中写入一个字节,增加"用过的空间"(usedSpace),并且触发任何一个等待"缓冲器非空"条件变为true 值时的线程。

我们使用一个互斥量来保护所有对usedSpace 变量的访问。QWaitCondition::wait()函数可以把锁定的互斥量作为它的第一个参数,在阻塞当前线程之前它会解锁,然后在 返回之前它会锁定。

对于这个实例,可以把下面的while 循环:

while (usedSpace == BufferSize)
     bufferIsNotFull.wait(&mutex);

替换为如下的if语句:

if(usedSpace == BufferSize)
{
   mutex.unlock();
   bufferIsNotFull.wait();
   mutex.lock();
}

然而,一旦我们被允许使用多个生产者线程时,将会中断 if语句,因为另外一个生产者可在wait()调用之后立即占用 个互斥量并且可使这个"缓冲器非写满"条件再次变为false 值。

void Consumer::run()
{
    for (int i = 0; i < DataSize; ++i) {
        mutex.lock();
        while (usedSpace == 0)
            bufferIsNotEmpty.wait(&mutex);
        std::cerr << buffer[i % BufferSize];
        --usedSpace;
        bufferIsNotFull.wakeAll();
        mutex.unlock();
    }
    std::cerr << std::endl;
}

消费者线程所做的和生产者正好相反:它等待"缓冲器非空"的条件并且触发正在等待"缓冲器非写满"条件的任意线程。

在目前为止提及的所有实例中,我们的线程都已经访问了 些相同的全局变量。但是在一些多线程的应用程序中,需要有一个在不同线程中保存不同数值的全局变量。这种变 量常称为线程本地存储(thread-local storage) 或者线程特定数据(thread-specifc data)。我们可以使用一个联通线
程ID 号[由QThread::currentThread()返回]的映射来模仿它,但是更好的方法是使用QThreadStorge<T>类。

QThreadStorge <T>的一种常见用法是用于高速缓存中。通过在不同线程中拥有一个独立的高速缓存,就可以避免用于锁住、解锁和可能等待一个互斥量的计算开销。例如:

QThreadStorage<QHash<int, double> *> cache;
void insertIntoCache(int id, double value)
{
    if(!cache.hasLocalData())
       cache.setLocalData(new QHash<int, double>);
    cache.localData()->insert(id, value);
}

void removeFromCache(int id)
{
    if(cache.hasLocalData())
       cache.localData()->remove(id);
}

这里的cache 变量在每一个线程中都保存一个指向QMap<int, double> 的指针。(因为某些编辑器的问题,QThreadStorage<T>中的模板类型必须是指针类型。)在特定线程中第一次使用高速缓存时,hasLocalData()就会返回false值,而且我们可以创建一个QHash<int, double> 对象。

除了高速缓存之外,QThreadStorage<T>还可以用于全局错误状态变量(与errno相似), 这样可以确保对某一线程的修改不会影响到其他的线程。

waitconditions

#include <QtCore>
#include <iostream>

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

QWaitCondition bufferIsNotFull;
QWaitCondition bufferIsNotEmpty;
QMutex mutex;
int usedSpace = 0;

class Producer : public QThread
{
public:
    void run();
};

void Producer::run()
{
    for (int i = 0; i < DataSize; ++i) {
        mutex.lock();
        while (usedSpace == BufferSize)
            bufferIsNotFull.wait(&mutex);
        buffer[i % BufferSize] = "ACGT"[uint(std::rand()) % 4];
        ++usedSpace;
        bufferIsNotEmpty.wakeAll();
        mutex.unlock();
    }
}

class Consumer : public QThread
{
public:
    void run();
};

void Consumer::run()
{
    for (int i = 0; i < DataSize; ++i) {
        mutex.lock();
        while (usedSpace == 0)
            bufferIsNotEmpty.wait(&mutex);
        std::cerr << buffer[i % BufferSize];
        --usedSpace;
        bufferIsNotFull.wakeAll();
        mutex.unlock();
    }
    std::cerr << std::endl;
}

int main()
{
    Producer producer;
    Consumer consumer;
    producer.start();
    consumer.start();
    producer.wait();
    consumer.wait();
    return 0;
}

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阳光开朗男孩

你的鼓励是我最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值