FromGossip@Openhome
Qt4 Gossip: 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++;
}
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等於"ShangHwang",而id還是"J.L.",所以checkNameAndID()就會傳回false,結果就顯示了錯誤訊息。
您必須同步資料對物件的更新,也就是在有一個執行緒正在設定userInfo物件的資料時,不可以又被另一個執行緒同時進行設定,您可以使用QMutex來進行這個動作,例如在UserInfo中宣告QMutex:
class UserInfo {
...
private:
...
QMutex mutex;
....
};
...
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();
}
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++;
}