Qt-条件变量与锁的使用

借鉴自CSDN

概述

在多线程同步时,条件变量是一个很好的选择,在使用时也需要注意。

一个线程调用QWaitCondition::wait进行等待,另一个线程调用QWatiCondition::wakeAllQWaitCondition::wakeOne进行唤醒。

示例

在下面的情况中,我们使用a线程往b线程发送一个数据包,然后阻塞等待回包才继续执行。b线程不断从通信接口接收并解析数据,然后唤醒a线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// a线程
...
Send(&package);
mutex.lock();
condition.wait(&lock);
if(receivedPackage)
{
HandlePackage(package);
}
mutex.unlock();
...

// b线程
...
receivedPackage = ParsePackage(buffer);
condition.wakeAll();
...

通常情况下,上述代码能跑得很好。但在某些特殊情况下,可能会出现混乱,大大降低通信可靠性。

在主线程中,调用Send(&packet)发送后,假如通信线程立即收到回包,在主线程还来不及调用wait()的时候,已经先wakeAll了,显然这次唤醒是无效的,但主线程继续调用wait,然后一直阻塞在那里,因为该回的包已经回了。

改进方法

QWaitCondition::wait的第一个入参(此处只考虑此,不考虑截止时间)为一个锁的指针,且该锁必须已经lock。在执行后,传入的锁会立即unlock并阻塞当前线程。

在上述代码中,我们虽然传入了mutex,但却没有任何意义,只是形式上为了满足参数传递。而这个mutex本身就是为了让多个线程协调工作。

要了解如何使用他,首先看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
bool QWaitCondition::wait(QMutex *mutex, QDeadlineTimer deadline)
{
if (!mutex)
return false;

// Qt手册中写道,如果lockedMutex是递归互斥锁,则此函数立即返回。
if (mutex->isRecursive()) {
qWarning("QWaitCondition: cannot wait on recursive mutexes");
return false;
}
report_error(pthread_mutex_lock(&d->mutex), "QWaitCondition::wait()", "mutex lock");
++d->waiters;

// 释放锁
mutex->unlock();

// 执行事件
bool returnValue = d->wait(deadline);

// 等待结束,再次上锁
mutex->lock();
return returnValue;
}

bool QWaitConditionPrivate::wait(QDeadlineTimer deadline)
{
int code;

// 等待事件
forever {
if (!deadline.isForever()) {
code = wait_relative(deadline);
} else {
code = pthread_cond_wait(&cond, &mutex);
}
if (code == 0 && wakeups == 0) {
// many vendors warn of spurious wakeups from
// pthread_cond_wait(), especially after signal delivery,
// even though POSIX doesn't allow for it... sigh
continue;
}
break;
}
Q_ASSERT_X(waiters > 0, "QWaitCondition::wait", "internal error (waiters)");
--waiters;
if (code == 0) {
Q_ASSERT_X(wakeups > 0, "QWaitCondition::wait", "internal error (wakeups)");
--wakeups;
}
report_error(pthread_mutex_unlock(&mutex), "QWaitCondition::wait()", "mutex unlock");
if (code && code != ETIMEDOUT)
report_error(code, "QWaitCondition::wait()", "cv wait");
return (code == 0);
}

wait执行过程中,首先释放锁,进行等待,等待结束时再将其锁住。

通过这个原理可以对上述代码做出如下改动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// a线程
...
Send(&package);
mutex.lock();
condition.wait(&lock);
if(receivedPackage)
{
HandlePackage(package);
}
mutex.unlock();
...

// b线程
...
mutex.lock(); // 与a线程同一个锁
receivedPackage = ParsePackage(buffer);
condition.wakeAll();
mutex.unlock();
...

此时,执行的顺序如下:

  1. 发送完数据包后,a线程首先调用mutex.lock进行加锁。
  2. b线程获取mutex,假设a线程还未执行到condition.wait,那么b线程无法获取到mutex,将一直阻塞。直到a线程调用condition.wait,将mutex释放并阻塞当前线程。
  3. b线程成功获取并锁住mutex后,执行完相关操作并调用 condition.wakeAll,此时a线程condition.wait完成,尝试对mutex进行加锁,但此时mutexb线程锁获取,a线程继续阻塞。
  4. 直到b线程调用完成mutex.unlocka线程condition.wait才可以对mutex进行加锁,执行后续操作,待完成相关逻辑,执行mutex.unlock

结语

至此,有关QWaitCondition的相关注意事项总结完毕。且此类设计模式在原生C++std::condition_variablestd::mutex同样试用。