Mutexes
一般情况下,如果两个线程同时访问同一对象(例如变量或者类实例),而且线程对对象的访问顺序非常重要,那么最终的结果不是我们想要的。
假如有一个线程在一直读取名为image的Mat对象(假设在重写的run函数中或者使用moveToThread方法的独立的线程中)
forever
{
image = imread("image.jpg");
}
forever宏是Qt创建无限循环的宏,等同于for(;;),使用这样的宏可以增加我们程序的可读性。
第二个线程一直在修改图像
forever
{
cvtColor(image, image, CV_BGR2GRAY);
resize(image, image, Size(), 0.5, 0.5);
}
假如这两个线程同时运行,在某个时间点,第一个线程可能在第二个线程的cvtColor之后,resize之前被调用,那么我们不会得到一个大小等于原图一半的灰度图,我们没有任何方法阻止这个事情发生,因为它们完全由操作系统在运行时处理。在多线程问题中,有一类竟态条件问题,解决方法是保证每个线程在轮到它执行的时候才去访问对象。这种解决方法叫做访问序列化(access serialization),在多线程编程中,通常靠互斥来解决。
互斥是防止多个线程同时访问同一个对象的方法。Qt中提供QMutex来处理访问序列化问题。只需创建一个QMutex对象,把它命名为imageMutex,然后我们在线程访问Mat对象时锁住互斥,当访问结束时解锁互斥即可。
forever
{
imageMutex.lock();
image = imread("image.jpg");
imageMutex.unlock();
}
对于第二个线程,我们有
forever
{
imageMutex.lock();
cvtColor(image, image, CV_BGR2GRAY);
resize(image, image, Size(), 0.5, 0.5);
imageMutex.unlock():
}
通过这种方法,当任何一个线程想要访问image时,首先要锁住mutex,假如线程执行到一半了,操作系统决定要切换到另一个新线程,另一个线程同样想要锁住mutex,但是此时mutex已经被锁了,在老线程解锁之前,新线程会一直阻塞,可以把这个过程想像为试图去拿一把锁的钥匙。只有锁住mutex的线程才可以解锁Mutex,这样就保证了当一个线程在访问一个对象时,另一个想访问这个对象的线程只能等待前一个线程访问完毕。
当对同一对象访问的线程增加时,使用mutex会成为一个负担。我们在Qt中可以使用QMutexLocker类来处理解锁、锁住mutex,我们把上面的代码改写为:
forever
{
QMutexLocker locker(&imageMutex);
image = imread("image.jpg");
}
第二个线程中
forever
{
QMutexLocker locker(&imageMutex);
cvtColor(image, image, CV_BGR2GRAY);
resize(image, image, Size(), 0.5, 0.5);
}
当构造QMutexLocker时传入mutex,这个锁被锁住,当QMutexLocker销毁时(变量超出作用域),锁被解锁。
读写锁
尽管mutex很强大,但它没有一些特定的功能,例如设定不同类型的锁。所以即使它在访问序列化的时候很有用,但不能有效的处理读写序列化问题。
假如我们想让多个线程同时读同一对象(例如变量,实例,文件等),同一时刻,只能让一个线程修改这个对象,这种情况下,我们要用于mutex的增加版-读-写锁机制。Qt提供了QReadWriteLock类:
- 如果一个线程调用 lockForRead 函数,其它线程仍然可以调用 lockForRead 函数来读敏感对象。
- 如果一个线程调用 lockForRead 函数, 其它线程只能等到解锁时才可调用lockForWrite
- 如果某一线程调用 lockForWrite 函数, 其它线程都不可以读或写敏感对象
- 如果在调用 lockForWrite 之前,已经有线程调用lockForRead了,所有调用lockForRead锁的线程都必须等待这个有写入锁的线程,也就是说,lockForWrite拥有更高的权限。
简单来说,这一机制保证了:
- 多个线程可以同时访问某一对象,写入者必须等待读取者完成才可以执行写入。
- 只允许一个写入者写入对象
- 在读者过多时,为确保写入者不永远等待,写入者拥有更高权限
下面例子来了,注意下面的 lock 变量是 QReadWriteLock 类,read_image是一个任意的只作访问目的的函数
forever
{
lock.lockForRead();
read_image();
lock.unlock():
}
类似的,在需要写入的线程里,我们有 write_image 用来代表任意需执行写入的函数
forever
{
lock.lockForWrite();
write_image();
lock.unlock();
}
与QMutex相似,我们用QMutexLocker来简化使用lock unlock函数,这里有QReadLocker 和QWriteLocker 类用于锁定或解锁QReadWriteLock,我们将之前代码改写为
forever
{
QReadLocker locker(&lock);
Read_image();
}
forever
{
QWriteLocker locker(&lock);
write_image();
}
Semaphores - 计数量
有些时候,我们要保证多个线程能访问有限数目的相同资源,例如,一个设备只有有限的内存,我们希望需要扩展内存的线程要兼顾可用内存问题,常常用semaphore来处理这一问题。semaphore不仅能锁定、解锁,还能追踪可用资源。
Qt提供了QSemaphore,以下是semaphore的一些函数
- acquire: 此函数用于获取特定大小的内资源,假如没有足够大的资源,那么线程将被阻塞,直到有充足资源。
- release: 此函数用于释放线程中使用的并且不再需要的资源
- available: 此·函数用于获取可用资源的总量。这个函数让线程在资源不足时,不去等待资源,而是执行其它任务
假如有100MB的内存,每个线程所需的内存都不一样,例如所需的内存是基于要处理的图像的大小。我们可以借助QSemaphore确保所有的线程只能用可用的内存大小,不能超过这个总量。
QSemaphore memSem(100);
在每个线程里面,在内存消耗很大的过程的前后,我们需要申请、释放所需的内存,像这样
memSem.acquire(X);
process_image(); // memory intensive process
memSem.release(X);
注意在这个示例里,假如X大于100,则必须等待release的内存大于或等于acquire的内存,才会执行内存集中的运算。这意味着可用资源的总量可以通过release函数来增加。
等待条件
有时我们希望一个线程在释放了读写锁之后进入休眠,这样其他线程就可以动作了,当某一条件达到时,它可以由其它线程唤醒。
假如有一些线程处理Mat对象,一个线程负责读取图像。假如有个进程负责创建这个图像文件,由于图像被多个线程占用,我们要用mutex保证每次只能有一个线程访问。然而,当图像不存在的时候,读线程需要等待,所以,我们引入:
forever
{
mutex.lock();
imageExistsCond.wait(&mutex);
read_image();
mutex_unlock();
}
注意mutex是QMutex类,imageExistsCond是QWaitCondition类,这个代码段表示锁定,开始读图像,但是假如你需要等待图像存在这个条件的话,你先释放掉这个锁,这样其它线程就可以运行了。同时需要其它线程唤醒这个线程,比如
forever
{
if(QFile::exists("image.jpg")
imageExistsCond.wakeAll();
}
此线程只是简单的检查图像是否存在,假如条件通过,它就唤醒其它线程。我们可以用wakeOne来唤醒等待这一条件的单一线程。
到此,我们的线程同步工具介绍完了,这一部分是介绍的类是Qt中有关线程同步最重要的类。请参考Qt文档学习更多的函数用来提高你的程序。在使用low-level线程的时候,要使用这些工具进行线程间通讯。