统信UOS应用开发进阶教程
上QQ阅读APP看书,第一时间看更新

1.3 线程同步

线程同步即当有一个线程在对内存地址进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他处于等待的线程才能对该内存地址进行操作,而别的线程又处于等待状态。

1.3.1 互斥量

线程锁能够保证临界资源的安全性,通常,每个临界资源需要一个线程锁进行保护。下面介绍两个概念。

临界资源:每次只允许一个线程访问的资源。

线程间互斥:多个线程在同一时刻都需要访问临界资源。

QMutex、QReadWriteLock、QSemaphore和QWaitCondition可提供线程同步的手段。使用线程主要是希望它们可以尽可能并发执行,而在一些关键点上线程之间需要停止或等待。例如,如果两个线程要同时访问同一个全局变量,则无法实现。

QMutex 类提供相互排斥的锁,或称为互斥量。在一个时刻至多一个线程拥有QMutex的某对象m_Mutex。假如一个线程试图访问已经被锁定的m_Mutex,那么该线程将休眠,直到拥有m_Mutex对象的线程对此m_Mutex解锁。QMutex常用来保护共享数据访问。QMutex类的所有成员函数是线程安全的。

在程序中使用QMutex 时需要声明头文件,在程序开始之前声明QMutex m_Mutex,在只能一个线程访问的代码之前加锁,代码之后解锁。相关的代码如下。

头文件声明: #include <QMutex>

互斥量声明: QMutex m_Mutex;

互斥量加锁: m_Mutex.lock();

互斥量解锁: m_Mutex.unlock();

如果对没有加锁的互斥量进行解锁,那么执行的结果是可能造成死锁。互斥量的加锁(Lock)和解锁(Unlock)必须在同一线程中成对出现。

QMutex有两种模式:Recursive和NonRecursive。

Recursive:一个线程可以对mutex对象多次加锁,直到相应次数的解锁调用后,mutex对象才真正被解锁。

NonRecursive:默认模式,mutex对象只能被加锁一次。

如果使用了m_Mutex.lock加锁而没有使用对应的m_Mutex.unlock解锁,就会造成死锁,其他线程将永远也得不到接触m_Mutex锁住的共享资源的机会。尽管可以不使用lock而使用tryLock(timeout)来避免“死等”造成的死锁[tryLock(负值)==lock()],但是还是可能造成错误。两个函数的具体情况如下。

bool tryLock:如果当前其他线程已对QMutex对象加锁,则该调用会立即返回,而不被阻塞。

bool tryLock(int timeout):如果当前其他线程已对该QMutex对象加锁,则该调用会等待一段时间,直到超时。

下面通过一个多线程售票的例子来看一下QMutex的使用。在这个例子中,首先通过继承QObject类,创建TicketSeller类,并创建两个对象——seller1和seller2,然后通过创建线程t1和t2,再将对象交给线程。在具体售票过程中,售票前先对互斥对象票的总数加锁,售票后再解锁释放。

#include <QCoreApplication>
#include <QObject>
#include <QThread>
#include <QMutex>
#include <string>
#include <iostream>
class TicketSeller : public QObject
{
public:
TicketSeller();
~TicketSeller();
public slots:
void sale();
public:
int* tickets;
QMutex* mutex;
std::string name;
};
TicketSeller::TicketSeller()
{
tickets = 0;
mutex = NULL;
}
TicketSeller::~TicketSeller()
{
}
void TicketSeller::sale()
{
while((*tickets) > 0)
{
mutex->lock();//加锁
std::cout << name << " : " << (*tickets)-- << std::endl;
mutex->unlock();//解锁
}
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
int ticket = 100;
QMutex mutex;
/*创建、设置线程1*/
//创建线程1
QThread t1;
TicketSeller seller1;
//设置线程1
seller1.tickets = &ticket;
seller1.mutex = &mutex;
seller1.name = "seller1";
//将对象移动到线程
seller1.moveToThread(&t1);
/*创建、设置线程2*/
//创建线程2
QThread t2;
TicketSeller seller2;
seller2.tickets = &ticket;
seller2.mutex = &mutex;
seller2.name = "seller2";
//将对象移动到线程
seller2.moveToThread(&t2);
QObject::connect(&t1, &QThread::started, &seller1, &TicketSeller::sale);
QObject::connect(&t2, &QThread::started, &seller2, &TicketSeller::sale);
t1.start();
t2.start();
return a.exec();
}

编译运行后,可以看到票的总数通过两个线程从100依次递减到0。

另外还有一个QMutexLocker 类,主要用来管理 QMutex。使用 QMutexLocker 的好处是可以防止线程死锁。QMutexLocker在构造的时候加锁,析构的时候解锁。

1.3.2 死锁以及解决方案

多线程以及多进程可改善系统资源的利用率,并提高系统的处理能力。然而,运行过程中因争夺资源可能会造成一种僵局(Deadly-embrace),若无外力作用,这些进程(线程)都将无法向前推进。

产生死锁的条件,一是系统中存在多个临界资源且临界资源不可抢占,二是线程需要多个临界资源才能继续执行。死锁可采用的解决方案如下:对使用的每个临界资源都分配一个唯一的序号,对每个临界资源对应的线程锁分配相应的序号,系统中的每个线程按照严格递增的次序请求临界资源。

1.3.3 读写锁

QReadWriteLock 与QMutex相似,但对读写操作区别对待,可以允许多个读者同时读数据,但只能有一个写,并且读写操作不能同时进行。使用QReadWriteLock而不是QMutex,可以使多线程程序更具有并发性。 QReadWriteLock的默认模式是NonRecursive。

QReadWriteLock类成员函数如下。

QReadWriteLock:读写锁构造函数。

QReadWriteLock ( RecursionMode recursionMode):递归模式。在这种模式下,一个线程可以加多次相同的读写锁,直到相应数量的unlock被调用才能被解锁。

void lockForRead:加读锁。

void lockForWrite:加写锁。

QReadWriteLock ( RecursionMode NonRecursive):非递归模式。在这种模式下,一个线程仅可以加读写锁一次,不可递归。

bool tryLockForRead:尝试读锁定。如果读锁定成功,则返回true,否则它立即返回false。

bool tryLockForRead ( int timeout ):尝试读锁定。如果读锁定成功,则返回true;如果不成功,则等待timeout时间,等待其他线程解锁,当timeout为负数时,一直等待。

bool tryLockForWrite:尝试写锁定。如果写锁定成功,则返回true,否则它立即返回false。

bool tryLockForWrite ( int timeout ):尝试写锁定。如果写锁定成功,则返回true;如果不成功,则等待timeout时间,等待其他线程解锁,当timeout为负数时,一直等待。

void unlock:解锁。

下面给出了一个QReadWriteLock的使用实例,代码如下。

QReadWriteLock lock;
void ReaderThread::run()
{
lock.lockForRead();
read_file();
lock.unlock();
}
void WriterThread::run()
{
lock.lockForWrite();
write_file();
lock.unlock();
}

1.3.4 条件变量

QWaitCondition条件变量允许一个线程通知其他线程,如果所等待的某个条件已经满足,可以继续运行。一个或多个线程可以在同一个条件变量上等待。当条件满足时,可以调用wakeOne从所有等待在该条件变量上的线程中随机唤醒一个线程继续运行,也可以使用wakeAll同时唤醒所有等待在该条件变量上的线程。

QWaitCondition和QSemaphore一样,因为要访问共享资源,所以要和QMutex配合使用。