QMutex 與 QMutexLocker


如果您的程式只是一個單執行緒,單一流程的程式,那麼通常您只要注意到程式邏輯的正確,您的程式通常就可以正確的執行您想要的功能,但當您的程式是多執行緒程式,多流程同時執行時,那麼您就要注意到更多的細節,例如在多執行緒共用同一物件的資料時。

如果一個物件所持有的資料可以被多執行緒同時共享存取時,您必須考慮到「資料同步」的 問題,所謂資料同步指的是兩份資料的整體性一致,例如物件A有 name與id兩個屬性,而有一份A1資料有name與id的資料要更新物件A的屬性,如果A1的name與id設定給A物件完成,則稱A1與A同步,如 果A1資料在更新了物件的name屬性時,突然插入了一份A2資料更新了A物件的id屬性,則顯然的A1資料與A就不同步,A2資料與A也不同步。

資料在多執行緒下共享時,就容易因為同時多個執行緒可能更新同一個物件的資訊,而造成物件資料的不同步,因為資料的不同步而可能引發的錯誤通常不易察覺, 而且可能是在您程式執行了幾千幾萬次之後,才會發生錯誤,而這通常會發生在您的產品已經上線之後,甚至是程式已經執行了幾年之後。

這邊舉個簡單的例子,考慮您設計這麼一個類別:
  • UserInfo.h
#ifndef USERINFO_H
#define USERINFO_H

#include <QString>

class UserInfo {
public:
UserInfo();
void setNameAndID(const QString &name, const QString &id);

private:
bool checkNameAndID();

QString name;
QString id;
long count;
};

#endif

  • UserInfo.cpp
#include "UserInfo.h"
#include <QString>
#include <iostream>
using namespace std;

UserInfo::UserInfo() {
name = "nobody";
id = "N/A";
}

void UserInfo::setNameAndID(const QString &name, const QString &id) {
this->name = name;
this->id = id;
if(!checkNameAndID()) {
cout << count
<< ": illegal name or ID....."
<< endl;
}
count++;
}

bool UserInfo::checkNameAndID() {
return (name.at(0) == id.at(0)) ? true : false;
}

在這個類別中,您可以設定使用者的名稱與縮寫id,並簡單檢查一下名稱與id的第一個字是否相同,單就這個類別本身而言,它並沒有任何的錯誤,但如果它被 用於多執行緒的程式中,而且同一個物件被多個執行存取時,就會"有可能"發生錯誤,來寫個簡單的測試程式:
  • CheckerThread.h
#ifndef CHECKERTHREAD_H
#define CHECKERTHREAD_H
#include <QThread>
#include <QString>

class UserInfo;

class CheckerThread : public QThread {
public:
CheckerThread(UserInfo *userInfo,
const QString &name, const QString &id);

protected:
void run();

private:
UserInfo *userInfo;
QString name;
QString id;
};

#endif

  • CheckerThread.cpp
#include "CheckerThread.h"
#include "UserInfo.h"

CheckerThread::CheckerThread(UserInfo *userInfo,
const QString &name, const QString &id) {
this->userInfo = userInfo;
this->name = name;
this->id = id;
}

void CheckerThread::run() {
while(true) {
userInfo->setNameAndID(name, id);
}
}

  • main.cpp
#include <QCoreApplication>
#include "UserInfo.h"
#include "CheckerThread.h"

int main(int argc, char *argv[]) {
QCoreApplication app(argc, argv);

UserInfo *userInfo = new UserInfo;

CheckerThread *thread1 =
new CheckerThread(userInfo, "Justin Lin", "J.L.");
CheckerThread *thread2 =
new CheckerThread(userInfo, "Shang Hwang", "S.H.");

thread1->start();
thread2->start();
thread1->wait();
thread2->wait();

return 0;
}

來看一下執行時的一個例子(為簡化範例,並無設置停止條件,請直接使用工作管理員結束程式):
2522482: illegal name or ID.....
2522498: illegal name or ID.....
2522514: illegal name or ID.....
2522530: illegal name or ID.....
2522542: illegal name or ID.....
2522560: illegal name or ID.....
2522815: illegal name or ID.....
2522832: illegal name or ID.....
2522858: illegal name or ID.....


看到了嗎?如果以單執行緒的觀點來看,上面的訊息在測試中根本不可能出現,然而在這個程式中卻出現了錯誤,而且重點是,第一次錯誤是發生在第2522482次的設定(您的電腦上可能是不同的數字),如果您在程式完成並開始應用之後,這個時間點可能是幾個月甚至幾年之後。

問題出現哪?在於這邊:
void UserInfo::setNameAndID(const QString &name, const QString &id) {
    this->name = name;
    this->id = id;
    if(!checkNameAndID()) {
        cout << count
             << ": illegal name or ID....."
             << endl;
    }
    count++;
}

雖然您設定給它的參數並沒有問題,在某個時間點時,thread1設定了"Justin Lin", "J.L."給name與id,在進行測試的前一刻,thread2可能此時剛好呼叫setNameAndID("Shang Hwang", "S.H."),在name被設定為"Shang Hwang"時,checkNameAndID()開始執行,此時name等於"Shang Hwang",而id還是"J.L.",所以checkNameAndID()就會傳回false,結果就顯示了錯誤訊息。

您必須同步資料對物件的更新,也就是在有一個執行緒正在設定userInfo物件的資料時,不可以又被另一個執行緒同時進行設定,您可以使用QMutex來進行這個動作,例如在UserInfo中宣告QMutex:
class UserInfo {
...
private:
    ...   
    QMutex mutex;
    ....
};

然後改寫一下setNameAndID(),您使用QMutex的lock()與unlock()方法來鎖定同步區域:
void UserInfo::setNameAndID(const QString &name, const QString &id) {
    mutex.lock();
    this->name = name;
    this->id = id;
    if(!checkNameAndID()) {
        cout << count
             << ": illegal name or ID....."
             << endl;
    }
    count++;
    mutex.unlock();
}

當執行緒執行QMutex的lock()時,它會鎖定接下來的程式流程,其它嘗試再執行lock()的執行緒 必須等待目前執行緒先執行了QMutex的unlock(),才可以取得鎖定,QMutex還有個tryLock(),如果QMutex已經鎖定,則 tryLock()立即返回。

您也可以使用QMutexLocker,這是個方便的類別,建構時以QMutex物件作為引數並進行鎖定,而解構時自動解除鎖定,例如可以改寫一下setNameAndID()如下,效果相同:
void UserInfo::setNameAndID(const QString &name, const QString &id) {
    QMutexLocker locker(&mutex);
    this->name = name;
    this->id = id;
    if(!checkNameAndID()) {
        cout << count
             << ": illegal name or ID....."
             << endl;
    }
    count++;
}