Qt线程同步类

Qt线程同步类

Qt中常用到多线程,因此处理线程间的关系非常重要,对于Qt中的各种线程同步的方法,我在这里做了一个小结。
Qt为线程同步提供了至少8个类,包括QMutex、QMutexLocker、QReadWriteLock、QReadLocker、QWriteLocker、QSemaphore、QSemaphoreReleaser、QWaitCondition。他们能提供基本的线程锁、便利的线程锁、针对读写的线程锁、针对读写的便利线程锁、基于资源数量的线程同步、基于资源数量的便利线程同步、基于条件的线程同步。功能各有不同,使用时需要首先分清楚自己的线程同步所需要的是什么。其中有些我也没有使用过,这些内容来源是Qt的官方文档。

QMutex

QMutex类提供线程之间的访问序列化。
QMutex的目的是保护对象,数据结构或代码段,以便一次只能有一个线程访问它(这类似于Java synchronized关键字)。通常最好使用带有QMutexLocker的互斥锁,因为这样可以轻松确保一致地执行锁定和解锁。
在线程中调用lock()时,尝试在同一位置调用lock()的其他线程将阻塞,直到获得锁定的线程调用unlock()。 Lock()的非阻塞替代方法是tryLock()。
QMutex经过优化,可以在非竞争情况下快速进行。 如果该互斥锁上没有争用,则非递归QMutex将不会分配内存。 它的构造和销毁几乎没有开销,这意味着将许多互斥体作为其他类的一部分是很好的。

QMutex mutex;
int number = 6;

void method1()
{
    mutex.lock();
    number *= 5;
    number /= 4;
    mutex.unlock();
}

QMutexLocker

QMutexLocker类是一个简便类,可以简化锁定和解锁互斥锁。
在复杂的函数和语句中或在异常处理代码中锁定和解锁QMutex是容易出错且难以调试的。 可以在这种情况下使用QMutexLocker来确保互斥锁的状态始终是明确定义的。
应在需要锁定QMutex的函数中创建QMutexLocker。 创建QMutexLocker时锁定互斥锁。您可以使用unlock()和relock()解锁并重新锁定互斥锁。如果锁定,则在销毁QMutexLocker时将解锁互斥锁。

int complexFunction(int flag)
{
    mutex.lock();

    int retVal = 0;

    switch (flag) {
    case 0:
    case 1:
        retVal = moreComplexFunction(flag);
        break;
    case 2:
        {
            int status = anotherFunction();
            if (status < 0) {
                mutex.unlock();
                return -2;
            }
            retVal = status + flag;        }
        break;
    default:
        if (flag > 10) {
            mutex.unlock();
            return -1;
        }
        break;
    }

    mutex.unlock();
    return retVal;
}

这个示例函数在开发时会变得更复杂,这增加了错误发生的可能性。
使用QMutexLocker极大地简化了代码,并使其更具可读性:

int complexFunction(int flag)
{
    QMutexLocker locker(&mutex);

    int retVal = 0;

    switch (flag) {
    case 0:
    case 1:
        return moreComplexFunction(flag);
    case 2:
        {
            int status = anotherFunction();
            if (status < 0)
                return -2;
            retVal = status + flag;
        }
        break;
    default:
        if (flag > 10)
            return -1;
        break;
    }

    return retVal;
}

现在,当QMutexLocker对象被销毁时,互斥锁将始终被解锁(当函数返回时,因为locker是一个自动变量)。
同样的原则适用于抛出和捕获异常的代码。 在将异常向上传递给调用函数之前,未锁定互斥锁的函数中未捕获的异常无法解锁互斥锁。
QMutexLocker还提供了一个mutex()成员函数,该函数返回QMutexLocker正在其上运行的互斥锁。 这对于需要访问互斥锁的代码很有用,例如QWaitCondition :: wait()。

QReadWriteLock

QReadWriteLock类提供读写锁定。
读写锁是一种同步工具,用于保护可以访问以进行读写的资源。如果要允许多个线程同时具有只读访问权限,则此类型的锁定非常有用,但只要一个线程想要写入资源,就必须阻止所有其他线程,直到写入完成为止。
在许多情况下,QReadWriteLock是QMutex的直接竞争对手。如果有很多并发读取和写入很少发生,QReadWriteLock是一个很好的选择。

QReadWriteLock lock;

void ReaderThread::run()
{
    ...
    lock.lockForRead();
    read_file();
    lock.unlock();
    ...
}

void WriterThread::run()
{
    ...
    lock.lockForWrite();
    write_file();
    lock.unlock();
    ...
}

为了确保读者不会永久阻止编写者,如果存在阻塞的编写器等待访问,则尝试获取锁定的读者将不会成功,即使该锁定当前仅由其他读者访问。 此外,如果作者访问了锁并且另一位作者进来,则该作者将优先于可能也在等待的任何读者。

与QMutex一样,当使用QReadWriteLock::Recursive构造QReadWriteLock :: RecursionMode时,QReadWriteLock可以由同一线程递归锁定。 在这种情况下,必须调用unlock()与调用lockForWrite()或lockForRead()的次数相同。 请注意,尝试以递归方式锁定时无法更改锁定类型,即无法锁定读取已经锁定以进行写入的线程(反之亦然)。

QReadLocker

QReadLocker类是一个便利类,它简化了锁定和解锁读写访问的读写锁。
QReadLocker(和QWriteLocker)的目的是简化QReadWriteLock锁定和解锁。 锁定和解锁语句或异常处理代码容易出错且难以调试。 可以在这种情况下使用QReadLocker来确保锁的状态始终是明确定义的。
这是一个使用QReadLocker锁定和解锁读写锁以进行读取的示例:

QReadWriteLock lock;

QByteArray readData()
{
    QReadLocker locker(&lock);
    ...
    return data;
}

它等同于以下代码:

QReadWriteLock lock;

QByteArray readData()
{
    lock.lockForRead();
    ...
    lock.unlock();
    return data;
}

QWriteLocker

QWriteLocker类是一个便利类,它简化了写访问的锁定和解锁读写锁。
QWriteLocker(和QReadLocker)的目的是简化QReadWriteLock锁定和解锁。 锁定和解锁语句或异常处理代码容易出错且难以调试。 可以在这种情况下使用QWriteLocker来确保锁的状态始终是明确定义的。
这是一个使用QWriteLocker锁定和解锁写 - 写锁的示例:

QReadWriteLock lock;

void writeData(const QByteArray &data)
{
    QWriteLocker locker(&lock);
    ...
}

它等同于以下代码:

QReadWriteLock lock;

void writeData(const QByteArray &data)
{
    lock.lockForWrite();
    ...
    lock.unlock();
}

QSemaphore

QSemaphore类提供通用计数信号量。
信号量是互斥体的推广。 虽然互斥锁只能锁定一次,但可以多次获取信号量。 信号量通常用于保护一定数量的相同资源。
信号量支持两个基本操作:acquire()和release():
acquire(n)尝试获取n个资源。 如果没有那么多可用资源,则呼叫将阻止,直到出现这种情况。
release(n)发布n个资源。
还有一个tryAcquire()函数,如果它无法获取资源,则立即返回;以及一个可随时返回可用资源数量的available()函数。

QSemaphore sem(5);      // sem.available() == 5

sem.acquire(3);         // sem.available() == 2
sem.acquire(2);         // sem.available() == 0
sem.release(5);         // sem.available() == 5
sem.release(5);         // sem.available() == 10

sem.tryAcquire(1);      // sem.available() == 9, returns true
sem.tryAcquire(250);    // sem.available() == 9, returns false

信号量的典型应用是用于控制对生产者线程和消费者线程共享的循环缓冲区的访问。 信号量示例显示了如何使用QSemaphore来解决该问题。
信号量的非计算示例将在餐厅用餐。 信号量初始化为餐厅中的椅子数量。 当人们到达时,他们想要一个座位。 当座位被填满时,available()减少。 当人们离开时,available()会增加,允许更多人进入。 如果一个10人的聚会想要坐下,但只有9个座位,那么10个人会等待,但是4个人的聚会将坐下来(将可用的座位调到5个,使10个人的聚会等待更长时间)。

QSemaphoreReleaser

QSemaphoreReleaser类提供QSemaphore :: release()调用的异常安全延迟
QSemaphoreReleaser可以在任何使用QSemaphore :: release()的地方使用。 构造QSemaphoreReleaser会延迟对信号量的release()调用,直到QSemaphoreReleaser被销毁(参见RAII模式)。
您可以使用它来可靠地释放信号量,以避免在异常或早期返回时出现死锁:

// ... do something that may throw or return early
sem.release();

如果在达到sem.release()调用之前进行了早期返回或抛出异常,则不会释放信号量,这可能会阻止线程在相应的sem.acquire()调用中等待继续执行。

使用RAII时

const QSemaphoreReleaser releaser(sem);
// ... do something that may throw or early return
// implicitly calls sem.release() here and at every other return in between

这不再发生,因为编译器将确保始终调用QSemaphoreReleaser析构函数,因此始终释放信号量。

QSemaphoreReleaser是启用移动的,因此可以从函数返回以转移从函数或作用域释放信号量的责任:

{ // some scope
    QSemaphoreReleaser releaser; // does nothing
    // ...
    if (someCondition) {
        releaser = QSemaphoreReleaser(sem);
        // ...
    }
    // ...
} // conditionally calls sem.release(), depending on someCondition

可以通过调用cancel()来取消QSemaphoreReleaser。 取消的信号量释放器将不再在其析构函数中调用QSemaphore :: release()。

QWaitCondition

QWaitCondition类提供用于同步线程的条件变量。
QWaitCondition允许线程告诉其他线程已满足某种条件。 一个或多个线程可以阻止等待QWaitCondition使用wakeOne()或wakeAll()设置条件。 使用wakeOne()唤醒一个随机选择的线程或wakeAll()来唤醒所有线程。
例如,假设每当用户按下某个键时,我们就会执行三个任务。 每个任务都可以拆分成一个线程,每个线程都有一个run()体,如下所示:

forever {
    mutex.lock();
    keyPressed.wait(&mutex);
    do_something();
    mutex.unlock();
}

这里,keyPressed变量是QWaitCondition类型的全局变量。

第四个线程会读取按键并在每次收到一个线程时唤醒其他三个线程,如下所示

forever {
    getchar();
    keyPressed.wakeAll();
}

三个线程被唤醒的顺序是不确定的。 此外,如果某些线程在按下键时仍然在do_something()中,它们将不会被唤醒(因为它们没有等待条件变量),因此不会对该按键执行任务。 可以使用计数器和QMutex来解决此问题。 例如,这是工作线程的新代码:

forever {
    mutex.lock();
    keyPressed.wait(&mutex);
    ++count;
    mutex.unlock();

    do_something();

    mutex.lock();
    --count;
    mutex.unlock();
}

这是第四个线程的代码:

forever {
    getchar();

    mutex.lock();
    // Sleep until there are no busy worker threads
    while (count > 0) {
        mutex.unlock();
        sleep(1);
        mutex.lock();
    }
    keyPressed.wakeAll();
    mutex.unlock();
}

互斥是必要的,因为两个线程试图同时更改同一变量的值的结果是不可预测的。
等待条件是强大的线程同步原语。

猜你喜欢

转载自blog.csdn.net/u013992365/article/details/81045821