天天看點

Qt随筆之Qt線程同步

一、Qt線程

1、Qt中的QThread類提供了平台無關的線程。相對于一般的程式都是從main()函數開始執行,QThread從run()函數開始執行。預設的,run()通過調用exec()來開啟事件循環。

實作一個簡單的繼承自QThread的使用者線程類,代碼如下。

class Thread : public QThread 
{
public:
    Thread();
    void stop();
protected:
    virtual void run();
private:
  volatile  bool m_stop;
};

Thread::Thread()
{
    m_stop = false;
}

void Thread::stop()
{
    m_stop = true;
}

void Thread::run()
{
    qreal i=0;
    while (!m_stop)
    {
        sleep(1);
        qDebug()<<QString("in Thread:%1").arg(i++);
    }
    m_stop = false;
}
           

在以上的示例中可以看出,線程的編寫并不難!

    啟動線程的時候可以,調用函數QThread::start(),開始Thread線程對象。通過按鈕發射信号即可。

    停止線程的時候可以,調用函數QThread::terminate(),但是terminate()函數并不會立刻終止線程,該線程何時終止取決于作業系統的排程政策。需要注意的是,terminate()函數過于毒辣,它可能線上程執行的任意一步終止執行,進而産生不可預知的後果(如修改某個重要資料時),另外,它也沒有給線程任何清理現場的機會(如釋放記憶體和鎖等)。

    是以,停止線程可以,如上代碼所示,手寫函數stop(),使其線程柔和的退出。

    線程停止後,應調用QThread::wait()函數,它使的線程阻塞等待直到退出或逾時。

二、線程同步

1、Qt中QMutex、QMutexLocker、QReadWriteLocker、QSemphore、QWaitCondition類提供了常用的同步線程的方法:

(1):QMutex、QMutexLocker

    QMutex類提供了一個保護一段臨界區代碼的方法,他每次隻允許一個線程通路這段臨界區代碼。QMutex::lock()函數用來鎖住互斥量,如果互斥量處于解鎖狀态,目前線程就會立即抓住并鎖定它;否則目前線程就會被阻塞,直到持有這個互斥量的線程對其解鎖。線程調用lock()函數後就會持有這個互斥量直到調用unlock()操作為止。QMutex還提供了一個tryLock()函數,如果互斥量已被鎖定,就立即傳回。

    現在使用QMutex保護上面的線程類的m_stop布爾變量,雖然沒啥用,但這裡的目的隻是為了示範下QMutex的用法~~

//thread.h頭檔案,添加互斥量對象

private:

    ...

    QMutex mutex;

};

void Thread::run()

{

    forever {

        mutex.lock();

        if (m_stop) {

            m_stop = false;

            mutex.unlock();

            break;

        }

        mutex.unlock();

        qDebug("vic.MINg!");

    }

    qDebug("end!");

}

void Thread::stop()

{

    mutex.lock();

    m_stop = true;

    mutex.unlock();

}

    在這裡QMutex能夠完全完成互斥操作,但是有些情況下QMutex類是無法某些特定的互斥操作的,下面舉個例子:

    這裡我們把void stop()函數,重新定義下,讓他以布爾形式傳回,實際也沒有啥用...隻為示例的示範效果~~

bool Thread::stop()

{

    m_stop = true;

    return m_stop;

}

    現在問題出來了,如果要在stop()函數中使用mutex進行互斥操作,但unlock()操作寫在那裡?unlock()操作卻不得不再return之後,進而導緻unlock()操作永遠也無法執行...

    Qt提供了QMutexLocker類何以簡化互斥量的處理,它在構造函數中接受一個QMutex對象作為參數并将其鎖定,在析構函數中解鎖這個互斥量。

    這樣可以像下面這樣重新編寫stop()函數:

bool Thread::stop()

{

    QMutexLocker locker(&mutex);

    m_stop = true;

    return m_stop;

}

(2):QReadWriteLocker、QReadLocker、QWriteLocker

    下面是一段對QReadWriteLocker類的對象進行,讀寫鎖的操作,比較簡單,這裡也不多做講解了,自己看吧 :)

MyData data;

QReadWriteLock lock;

void ReaderThread::run()

{

    ...

    lock.lockForRead();

    access_data_without_modifying_it(&data);

    lock.unlock();

    ...

}

void WriterThread::run()

{

    ...

    lock.lockForWrite();

    modify_data(&data);

    lock.unlock();

    ...

}

(3):QSemphore

    Qt中的信号量是由QSemaphore類提供的,信号量可以了解為互斥量功能的擴充,互斥量隻能鎖定一次而信号量可以擷取多次,它可以用來保護一定數量的同種資源。

    acquire(n)函數用于擷取n個資源,當沒有足夠的資源時調用者将被阻塞直到有足夠的可用資源。release(n)函數用于釋放n個資源。

    QSemaphore類還提供了一個tryAcquire(n)函數,在沒有足夠的資源是該函數會立即傳回。

    一個典型的信号量應用程式是在兩個線程間傳遞一定數量的資料(DataSize),而這兩個線程使用一定大小(BufferSize)的共享循環緩存。

const int DataSize = 100000;

const int BufferSize = 4096;

char buffer[BufferSize];

    生産者線程向緩存中寫入資料,直到它到達終點,然後在起點重新開始,覆寫已經存在的資料。消費者線程讀取前者産生的資料。

    生産者、消費者執行個體中對同步的需求有兩處,如果生産者過快的産生資料,将會覆寫消費者還沒有讀取的資料,如果消費者過快的讀取資料,将越過生産者并且讀取到一些垃圾資料。

    解決這個問題的一個有效的方法是使用兩個信号量:

QSemaphore freeSpace(BufferSize);

QSemaphore usedSpace(0);

    freeSpace信号量控制生産者可以填充資料的緩存部分。usedSpace信号量控制消費者可以讀取的區域。這兩個信号量是互補的。其中freeSpace信号量被初始化為BufferSize(4096),表示程式一開始有BufferSize個緩沖區單元可被填充,而信号量usedSpace被初始化為0,表示程式一開始緩沖區中沒有資料可供讀取。

    對于這個執行個體,每個位元組就看作一個資源,實際應用中常會在更大的機關上進行操作,進而減小使用信号量帶來的開銷。

void Producer::run()

{

    for (int i = 0; i < DataSize; ++i) {

        freeSpace.acquire();

        buffer[i % BufferSize] = "MING"[uint(rand()) % 4];

        usedSpace.release();

    }

}

    在生産者中,我們從擷取一個“自由的”位元組開始。如果緩存被消費者還沒有讀取的資料填滿,acquire()的調用就會阻塞,直到消費者已經開始消耗這些資料為止。一旦我們已經擷取了這個位元組,我們就用一些随機資料("M"、"I"、"N"或"G")填充它并且把這個位元組釋放為“使用的”,是以它可以被消費者線程使用。

void Consumer::run()

{

    for (int i = 0; i < DataSize; ++i) {

        usedSpace.acquire();

        cerr << buffer[i % BufferSize];

        freeSpace.release();

    }

    cerr << endl;

}

    在消費者中,我們從擷取一個“使用的”位元組開始。如果緩存中沒有包含任何可讀的資料,acquire()調用将會阻塞,直到生産者已經産生一些資料。一旦我們已經擷取了這個位元組,我們就列印它并且把這個位元組釋放為“自由的”,使它可以被生産者使用來再次填充資料。

int main()

{

    Producer producer;

    Consumer consumer;

    producer.start();

    consumer.start();

    producer.wait();

    consumer.wait();

    return 0;

}

    main()函數的功能比較簡單,負責啟動生産者和消費者線程,然後等待其各自執行完畢後自動退出。

(4):QWaitCondition

    對生産者和消費者問題的另一個解決方法是使用QWaitCondition,它允許線程在一定條件下喚醒其他線程。其中wakeOne()函數在條件滿足時随機喚醒一個等待線程,而wakeAll()函數則在條件滿足時喚醒所有等待線程。

    下面重寫生産者和消費者執行個體,以QMutex為等待條件,QWaitCondition允許一個線程在一定條件下喚醒其他線程。

const int DataSize = 100000;

const int BufferSize = 4096;

char buffer[BufferSize];

QWaitCondition bufferIsNotFull;

QWaitCondition bufferIsNotEmpty;

QMutex mutex;

int usedSpace = 0;

    在緩存之外,我們聲明了兩個QWaitCondition、一個QMutex和一個存儲了在緩存中有多少個“使用的”位元組的變量。

void Producer::run()

{

    for (int i = 0; i < DataSize; ++i) {

        mutex.lock();

        if (usedSpace == BufferSize)

            bufferIsNotFull.wait(&mutex);

        buffer[i % BufferSize] = "MING"[uint(rand()) % 4];

        ++usedSpace;

        bufferIsNotEmpty.wakeAll();

        mutex.unlock();

    }

}

    在生産者中,我們從檢查緩存是否充滿開始。如果是充滿的,我們等待“緩存不是充滿的”條件。當這個條件滿足時,我們向緩存寫入一個位元組,增加usedSpace,并且在喚醒任何等待這個“緩存不是空白的”條件變為真的線程。

   for循環中的所有語句需要使用互斥量加以保護,以保護其操作的原子性。

    bool wait ( QMutex * mutex, unsigned long time = ULONG_MAX );

    這個函數做下說明,該函數将互斥量解鎖并在此等待,它有兩個參數,第一個參數為一個鎖定的互斥量,第二個參數為等待時間。如果作為第一個參數的互斥量在調用是不是鎖定的或出現遞歸鎖定的情況,wait()函數将立即傳回。

    調用wait()操作的線程使得作為參數的互斥量在調用前變為鎖定狀态,然後自身被阻塞變成為等待狀态直到滿足以下條件:

      1)、其他線程調用了wakeOne()或者wakeAll()函數,這種情況下将傳回"true"值。

      2)、第二個參數time逾時(以毫秒記時),該參數預設情況是ULONG_MAX,表示永不逾時,這種情況下将傳回"false"值。

    wait()函數傳回前會将互斥量參數重新設定為鎖定狀态,進而保證從鎖定狀态到等待狀态的原則性轉換。

void Consumer::run()

{

    forever {

        mutex.lock();

        if (usedSpace == 0)

            bufferIsNotEmpty.wait(&mutex);

        cerr << buffer[i % BufferSize];

        --usedSpace;

        bufferIsNotFull.wakeAll();

        mutex.unlock();

    }

    cerr << endl;

}

    消費者做的和生産者正好相反,他等待“緩存不是空白的”條件并喚醒任何等待“緩存不是充滿的”的條件的線程。

    main()函數與上面的基本相同,這個不再多說。

    在QThread類的靜态函數currentThread(),可以傳回目前線程的線程ID。在X11環境下,這個ID是一個unsigned long類型的值。