QMutex 與 QMutexLocker

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++;
}

雖然您設定給它的參數並沒有問題,在某個時間點時,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;
    ....
};

然後改寫一下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++;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值