如何保證QT中類的線程安全?(讓多線程不再崩潰)

1 什麼是類的線程安全(或線程安全的類)?

瞭解多線程的人大概都知道,類的線程安全比可重入更加嚴格,它要求在不同線程同時調用類同一實例的成員函數,而不會引發程序的崩潰。

2 哪些情況下不用考慮線程安全問題?

2.1 在多線程中對bool,int,float,QString等類型的操作,你不用考慮任何安全性問題。

因爲你無論以什麼方式在不同線程中對這些類型進行操作,都像真正的原子性操作一樣(其實不是嚴格的原子性操作),完全不會引起程序崩潰。我想這可能是QT已經對這些類型進行了線程安全保障。
比如,我下面的這個類本身就是線程安全的:

class Counter : public QObject
{
    Q_OBJECT
public:
    Counter() { n = 0; }
    void O1() { ++n; }
    void O2() { --n; }
    int get() const { return n; }
private:
    int n;
};

2.2 在多線程中使用信號槽機制進行資源共享時,不用考慮線程安全問題。

例如在兩個不同的子線程中通過信號槽將字符串信息傳遞到UI線程,用於更新界面的顯示,這個操作是線程安全的,因爲信號槽作爲QT獨創的通信機制,是線程安全的。

3 設計一個線程安全的類

3.1 先展示一個未進行線程安全保障的類是如何引發崩潰的

定義一個非線程安全的類:

class NotSafe : public QObject
{
    Q_OBJECT
public:
    NotSafe() { }
    void O1() { list.append(1); list.append(2); }
    void O2() { list.append(3); list.clear(); }
    QList<int> get() const { return list; }
private:
    QList<int> list; //對QList的操作不是線程安全的
};

定義兩個線程:

class Thread1: public QThread
{
public:
    Thread1();
    virtual void run() override;
};
class Thread2: public QThread
{
public:
    Thread2();
    virtual void run() override;
};

在兩個子線程中對這個類的同一實例進行訪問:

NotSafe notsafe; //同一實例
void Thread1::run()
{
    while(1){
        QApplication::processEvents();
        notsafe.O1(); //可能引發崩潰的操作
        notsafe.O1(); //可能引發崩潰的操作
        qDebug()<<notsafe.get(); //可能引發崩潰的操作
    }
}
void Thread2::run()
{
    while(1){
        QApplication::processEvents();
        notsafe.O1(); //可能引發崩潰的操作
        notsafe.O2(); //可能引發崩潰的操作
        qDebug()<<notsafe.get(); //可能引發崩潰的操作
    }
}

不出意外,程序運行一分鐘以內就會崩潰。

3.2 安全寫法1

使用互斥量QMutex,並使用 lock() 和 unlock() 方法進行鎖定和解鎖,但這個方法難以對返回參數進行保護

class Safe1 : public QObject
{
    Q_OBJECT
public:
    Safe1() { }
    void O1() { mutex.lock(); list.append(1); list.append(2); mutex.unlock();}
    void O2() { mutex.lock(); list.append(3); list.clear(); mutex.unlock();}
    QList<int> get() const { QMutexLocker locker(&mutex); return list; }
private:
    mutable QMutex mutex;
    QList<int> list;
};

3.2 安全寫法2

使用互斥量QMutex,並使用 QMutexLocker 進行鎖定,當 QMutexLocker 從棧釋放後,自動解鎖

class Safe2 : public QObject
{
    Q_OBJECT
public:
    Safe2() { }
    void O1() { QMutexLocker locker(&mutex); list.append(1); list.append(2); }
    void O2() { QMutexLocker locker(&mutex); list.append(3); list.clear(); }
    QList<int> get() const { QMutexLocker locker(&mutex); return list; }
private:
    mutable QMutex mutex;
    QList<int> list;
};

3.3 安全寫法3

使用讀寫鎖 QReadWriteLock,並使用 lockForRead() 和 lockForWrite() 對讀或寫進行保護,這種方法相比QMutex效率更高,因爲它允許多線程同時讀。

class Safe3 : public QObject
{
    Q_OBJECT
public:
    Safe3() { }
    void O1() { QWriteLocker locker(&RWlock); list.append(1); list.append(2); }
    void O2() { QWriteLocker locker(&RWlock); list.append(3); list.clear(); }
    QList<int> get() const { QReadLocker locker(&RWlock); return list; }
private:
    mutable QReadWriteLock RWlock;
    QList<int> list;
};

3.4 更多保證類的線程安全性的辦法

1.使用生產者 — 消費者模式,通過緩衝區產生多個數據的備份,可提高線程間數據共享的效率
2.使用socket進行線程間,甚至進程間的數據共享
3.類不提供訪問實例中成員的方法,而是通過信號槽機制,進行線程間的數據共享

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章