2017/7/28 14:14:43
Synchronizing QThread
While the purpose of threads is to allow code to run in parallel, there are times where threads must stop and wait for other threads. For example, if two threads try to write to the same variable simultaneously, the result is undefined. The principle of forcing threads to wait for one another is called mutual exclusion. It is a common technique for protecting shared resources such as data. Qt provides low-level primitives as well as high-level mechanisms for synchronizing threads.
QMutex
The purpose of a QMutex
is to protect an object, data structure or section of code so that only one thread can access it at a time (this is similar to the Java synchronized keyword).
QMutex
QMutex
is the basic method to protect the data access.
QMutex mutex;
class WorkerThread_1 : public QThread {
protected:
void run() {
mutex.lock();
for ( int i = 0; i < 100000; ++i ) {
value++;
}
qDebug() << "Thread 1:" << value;
mutex.unlock();
}
};
class WorkerThread_2 : public QThread {
protected:
void run() {
mutex.lock();
for ( int i = 0; i < 200000; ++i ) {
value++;
}
qDebug() << "Thread 2:" << value;
mutex.unlock();
}
};
QMutexLocker
The QMutexLocker
class is a convenience class that simplifies locking and unlocking mutexes. Locking and unlocking a QMutex
in complex functions and statements or in exception handling code is error-prone and difficult to debug. If locked, the mutex will be unlocked when the QMutexLocker
is destroyed. The above code can be written as:
class WorkerThread_1 : public QThread {
protected:
void run() {
QMutexLocker locker( &mutex );
for ( int i = 0; i < 100000; ++i ) {
value++;
}
qDebug() << "Thread 1:" << value;
}
};
class WorkerThread_2 : public QThread {
protected:
void run() {
QMutexLocker locker( &mutex );
for ( int i = 0; i < 200000; ++i ) {
value++;
}
qDebug() << "Thread 2:" << value;
}
};
QWaitCondition
The QWaitCondition
class provides a condition variable for synchronizing threads. QWaitCondition
allows a thread to tell other threads that some sort of condition has been met. One or many threads can block waiting for a QWaitCondition
to set a condition with wakeOne()
or wakeAll()
. Use wakeOne()
to wake one randomly selected condition or wakeAll()
to wake them all.
Key functions:
The thread that is woken up depends on the operating system' scheduling policies, and cannot be controlled or predicted.
-
wakeAll()
: Wakes all threads waiting on the wait condition. -
wakeOne()
: Wakes one thread waiting on the wait condition. -
wait(QMutex *lockedMutex, unsigned long time = ULONG_MAX)
. Flows:
mutex.lock(); // Lock a mutex first.
if ( numUsedBytes == 0 ) {
// Releases the locked Mutex and waits on the wait condition.
BufferNotEmpty.wait( &mutex );
}
// The locked Mutex will be returned to the same locked state.
mutex.unlock();
Producer-consumer example:
// global variable
const int DATASIZE = 100000;
const int BUFFERSIZE = 8192;
char buffer[BUFFERSIZE];
// Wait Condition
QWaitCondition BufferNotEmpty;
QWaitCondition BufferNotFull;
QMutex mutex;
int numUsedBytes = 0;
// producer
class Producer : public QThread {
protected:
void run() {
qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
for ( int i = 0; i < DATASIZE; ++i ) {
mutex.lock();
if ( numUsedBytes == BUFFERSIZE ) {
BufferNotFull.wait( &mutex );
}
mutex.unlock();
buffer[i%BUFFERSIZE] = "abcd"[(int)qrand() % 4];
mutex.lock();
++numUsedBytes;
BufferNotEmpty.wakeAll();
mutex.unlock();
}
}
};
class Consumer : public QThread {
protected:
void run() {
for ( int i = 0; i < DATASIZE; ++i ) {
mutex.lock();
if ( numUsedBytes == 0 ) {
BufferNotEmpty.wait( &mutex );
}
mutex.unlock();
qDebug() << buffer[i%BUFFERSIZE];
mutex.lock();
--numUsedBytes;
BufferNotFull.wakeAll();
mutex.unlock();
}
}
};
If numUsedBytes == BUFFERSIZE
, producer thread will wait until consumer eats some sources(BufferNotFull.wakeAll()
). If numUsedBytes == 0
, the consumer need to wait until producer produces some sources(BufferNotEmpty.wakeAll()
).
QSemaphore
A semaphore is a generalization of a mutex. While a mutex can only be locked once, it's possible to acquire a semaphore multiple times. Semaphores are typically used to protect a certain number of identical resources.
Key functions:
-
QSemaphore::QSemaphore(int n = 0)
: Creates a new semaphore and initializes the number of resources it guards to n (by default, 0). -
acquire(n)
: Tries to acquire n resources guarded by the semaphore. If n >available()
, this call will block until enough resources are available. -
release(n)
: Returns the number of resources currently available to the semaphore. This number can never be negative.
Same as QWaitCondition
example, we rewrite producer-consumer example as follows:
// global variable
const int DATASIZE = 100000;
const int BUFFERSIZE = 8192;
char buffer[BUFFERSIZE];
QSemaphore freeSema(BUFFERSIZE);
QSemaphore usedSema;
class Producer : public QThread {
protected:
virtual void run() {
qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
for ( int i = 0; i < DATASIZE; ++i ) {
freeSema.acquire();
buffer[i%BUFFERSIZE] = "ABCD"[(int)qrand()%4];
usedSema.release();
}
}
};
class Consumer : public QThread {
protected:
virtual void run() {
for ( int i = 0; i < DATASIZE; ++i ) {
usedSema.acquire();
qDebug() << buffer[i%BUFFERSIZE];
freeSema.release();
}
}
};