Qt中的多線程及其應用(1)

一、進程與線程的概念

什麼是程序?

程序是計算機存儲系統中的數據文件。分爲源代碼程序和可執行程序。源代碼程序一般爲文本文件,用來描述程序的行爲和功能,可執行程序一般爲二進制文件,可以直接加載並執行。

源代碼程序經過編譯器編譯,就成爲可執行程序。

什麼是進程?

廣義的概念認爲是程序關於某個數據集合的一次運行活動,狹義地講,就是程序被加載到內存中後,執行得到的進程。

程序和進程的區別:

程序是硬盤中的靜態文件,是存儲系統中的一段二進制表示。

進程是內存中動態的運行實體,如數據段、代碼段、PC指針等。

程序和進程的聯繫:

一個程序可以多次運行,每次運行都會產生一個進程。

一個進程也可能包含很多個程序,比如Qt工程的release版本,依賴很多其他的動態庫。

但是,在當代操作系統中,資源分配的基本單位是進程,而CPU進行調度執行的基本單位是線程

什麼是線程?

線程是進程內的一個執行單元;

是操作系統中一個可調度的實體;

是進程中相對獨立的一個控制流序列;

是執行時的線程數據和其他調度所需的信息。

C/C++程序被執行後從main()函數開始運行,中間經歷了什麼樣的歷程?

可執行程序加載執行後,第一步系統分配資源(內存、IO等),然後將PC指針指向main()函數入口地址,然後從PC指針包含的地址處開始執行。——第一個線程開始。

線程是進程使用CPU資源的基本單位。

進程與線程的關係:

進程中可以存在多個線程來共享進程資源;(進程(資源分配)中的多個線程(調度執行)並行執行,共享進程資源)

線程是被調度的執行單元,而進程不是;

線程不能脫離進程單獨存在,只能依賴於進程運行;

線程有生命期,有誕生和死亡;

任意線程都可以創建其他新的線程。



二、Qt中的多線程編程

Qt中通過QThread類來支持多線程,它是一個跨平臺的多線程解決方案,以簡潔易用的方式實現多線程編程。

Qt中,線程以對象的形式被創建和使用,每個線程對應着一個QThread對象。

QTread中的關鍵成員函數:

1、void run()函數:線程體函數,用於定義線程功能(執行流)

2、void start()函數:啓動函數,將線程入口地址設置爲run()函數

3、void terminate()函數:強制結束線程(工程中不推薦

編程示例:創造兩個線程,分別實現輸出1~5的功能;

#include <QCoreApplication>
#include <QThread>
#include <QDebug>
class MyThread : public QThread
{
protected:
    void run()
    {
        for(int i=0; i<5; i++)
        {
            qDebug() << objectName() << " : " << i;
            sleep(1);
        }
    }
};
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    MyThread t;
    t.setObjectName("t");
    t.start();

    MyThread tt;
    tt.setObjectName("tt");
    tt.start();

    for(int i=0; i<10000; i++)
    {
        for(int j=0; j<10000; j++)
        {
        }
    }
    return a.exec();
}

從上述代碼可以看出,線程由QThread類 創建對象,通過start()函數開始運行,run()函數執行具體功能,線程結束後自動死亡。

也可以通過調用terminate()函數直接對線程進行強制結束。

但是,強制結束不能保證數據完整性,比如內存收回等功能可能就會因爲線程提前結束而無法執行,造成內存泄漏。所以在工程中terminate()函數是禁止使用的,因爲其是使操作系統暴力終止線程。

所以如何能安全地在需要的時候終止線程呢?

run()函數執行結束是安全地終止線程的唯一方法,所以我們需要在線程類中增加標誌變量(volatile bool),通過標識變量的值,判斷是否需要從run()函數中返回。

示例如下:在自定義類中定義volatile bool類型的變量 m_toStop,然後在構造函數中對其初始化爲false,定義成員函數stop()來講變量的值設置爲true,在run()函數運行時,將m_toStop的值作爲函數執行的條件之一,通過調用stop()函數,可以安全地結束run()函數。

class Sample : public QThread
{
protected:
    volatile bool m_toStop;

    void run()
    {
        int* p = new int[10000];

        for(int i=0; !m_toStop && (i<10); i++)
        {
            qDebug() << objectName() << " : " << i;

            p[i] = i * i * i;

            msleep(500);
        }

        delete[] p;  //內存泄漏

        qDebug() << objectName() << " : end";
    }

public:
    Sample()
    {
        m_toStop = false;
    }

    void stop()
    {
        m_toStop = true;
    }
};

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

    qDebug() << "main begin";

    Sample t;

    t.setObjectName("t");

    t.start();

    for(int i=0; i<10000; i++)
    {
        for(int j=0; j<10000; j++)
        {

        }
    }

    t.stop();  //執行後無內存泄漏

    //t.terminate();  //會在線程未結束時,殺死線程, 內存泄漏

    qDebug() << "main end";

    return a.exec();
}

要點:start()啓動run()函數,run()函數實現線程執行體。

三、多線程之間的同步

多線程編程的本質是什麼?   —— 併發性

在宏觀上,所有線程併發執行,多個線程間相互獨立,互不干涉。


線程間的運行是相互獨立的,但是線程間總是完全獨立毫無依賴嗎?——並不是。

在某些情況下,多線程的執行需要時序上的相互支持。

舉個例子:煮飯、做菜、吃飯在執行時都是相互獨立的,但是必須是煮飯和做菜兩個線程結束後,吃飯這個線程纔可以執行。

同步的概念:在特殊情況下,需要控制多線程間的相對執行順序。

QThread類直接內置了多線程間的同步函數:

bool QThread::wait(unsigned long time = ULONG_MAX)

qDebug() << "begin";

QThread t;

t.start();
t.wait();

qDebug() << "end";

編程示例如下:

/*
    sum(n) => 1 +2 +3 +4 +... +n;

    sum(1000) => ?;

    [1, 1000] = [1, 300]、 [301, 600]、 [601, 1000]
*/

class Calculator : public  QThread//QObject
{
  protected:
    int m_begin;
    int m_end;
    int m_result;

    void run()
    {
        qDebug() << objectName() << ": run() begin";

        for(int i=m_begin; i<=m_end; i++)
        {
            m_result += i;

            msleep(10);
        }

        qDebug() << objectName() << ": run() end";
    }
public:
    Calculator(int begin, int end)
    {
        m_begin = begin;
        m_end = end;
        m_result = 0;
    }

    void work()
    {
        run();
    }

    int result()
    {
        return m_result;
    }
};

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

    qDebug() << "main begin : ";

    Calculator call1(1, 300);
    Calculator call2(301, 600);
    Calculator call3(601, 1000);

    call1.setObjectName("call1");
    call2.setObjectName("call2");
    call3.setObjectName("call3");

    call1.start();
    call2.start();
    call3.start();

    call1.wait(); //不添加wait()函數直接執行會導致result等於0
    call2.wait();
    call3.wait();

    //call1.work();  
    //call2.work();
    //call3.work();

    int result = call1.result() + call2.result() + call3.result();

    qDebug() << " result : " << result;

    qDebug() << "main end .";

    return a.exec();
}

四、多線程間的互斥

多線程除了在時序上可能存在依賴,在其他方面是否也可能產生依賴呢?

生產—消費者問題:

有n個生產者同時製造產品,並把產品存入倉庫中,有m個消費者同時需要從倉庫中取出產品,規則:當倉庫未滿時,任意生產者可以存入產品,當倉庫未空時,任意消費者可以取出產品。

存入和取出不得衝突!

同樣的例子,洗手間的使用和清掃,當保潔員清洗時,洗手間必須暫停使用。

也就是說倉庫、洗手間是一個被“限制”的資源,每次只允許一個線程進行訪問(讀、寫),我們稱之爲臨界資源(Critical Resource)

線程間的互斥:多個線程在同一時間都需要訪問鄰接資源。

Qt中,用QMutex類來保證線程間的互斥,它是一把線程鎖,利用線程鎖可以保證臨界資源的安全性。

QMutex類中的關鍵成員函數:

void lock()函數:當調用這個函數時,獲取鎖並執行,獲取之後,阻塞並等待鎖釋放;

void unlock()函數:釋放鎖。(通一把鎖的獲取和釋放必須在同一線程中成對的出現)

注意:如果mutex在調用unlock()時處於空閒狀態,那麼程序的行爲是未定義的。

解決生產消費者問題:

創建兩個類,分別表示生產者和消費者的存入和取出行爲,定義倉庫對象QString類,用來表示倉庫中的產品。

程序實例如下:

#include <QCoreApplication>
#include <QThread>
#include <QDebug>
#include <QMutex>

static QString g_store;
static QMutex g_mutex;

class Producer : public QThread
{
protected:
    void run()
    {
        int count = 0;

        while (true)
        {
            g_mutex.lock();

            g_store.append(QString::number((count++) % 10));  //將隨機數字(0-9)存入倉庫

            qDebug() << objectName() << " : " + g_store;

            g_mutex.unlock();

            msleep(1);
        }
    }
};

class Customer : public QThread
{
protected:
    void run()
    {
        while (true)
        {
            g_mutex.lock();

            if( g_store != "")
            {
                g_store.remove(0, 1);

                qDebug() << objectName() << " : " + g_store;
            }

            g_mutex.unlock();

            msleep(1);
        }

    }
};

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

    Producer p;
    Customer c;

    p.setObjectName("Producer");
    c.setObjectName("Customer");

    p.start();
    c.start();

    return a.exec();
}

程序運行後,對倉庫的訪問(g_store)只能是單線程,無論是生產者還是消費者,在進行具體的操作前,都會對線程上鎖,保證不會出現衝突現象。

程序有多少臨界資源?需要多少線程鎖?

一般性原則:每一個臨界資源都需要一個線程所保護。

但是,下面的 示例哪個是正確的呢?


編程發現,當兩個線程按照上述方式對線程上鎖之後,程序無法執行,也無法解鎖。這就是線程中的死鎖現象。

死鎖:線程間相互等待臨界資源而造成彼此無法繼續執行。

什麼時候會發生死鎖:

系統中存在多個臨界資源且臨界資源不可搶佔,線程需要多個臨界資源才能繼續執行。

如何避免線程間的死鎖呢?

對所有的臨界資源都分配一個唯一的序號,對應的線程也分配同樣的序號,系統中每個線程按照嚴格遞增的次序請求資源。

修改代碼如下:

class THreadA : public QThread
{
  protected:
    void run()
    {
        while ( true )
        {
            m1.lock();
            m2.lock();
            m2.unlock();
            m1.unlock();

            sleep(1);
        }

    }
};

class THreadB : public QThread
{
  protected:
    void run()
    {
        while ( true )
        {
            m1.lock();
            m2.lock();

            m2.unlock();
            m1.unlock();

            sleep(1);
        }

    }
};

信號量的概念:

信號量是特殊的線程鎖,它允許N個線程同時訪問臨界資源,Qt中以QSemaphore類的方式支持信號量。

示例如下:


注意:QSemaphore對象中維護了 一個整形值,acquire()使得該值減一,release()使得該值加一,當該值爲零時,acquire()函數將阻塞當前線程。

再論生產消費者問題:

#include <QCoreApplication>
#include <QThread>
#include <QDebug>
#include <QSemaphore>

const int SIZE = 5;
unsigned char g_buff[SIZE] = {0};
QSemaphore g_sem_free(SIZE);  //提高併發性,標識當前五個倉庫都是空閒的
QSemaphore g_sem_used(0);     //標識多少個已經被使用

class Producer : public QThread
{
  protected:
    void run()
    {
        while ( true)
        {
            int value = qrand() % 256;

            g_sem_free.acquire();

            for(int i=0; i<SIZE; i++)
            {
                if( !g_buff[i] )
                {
                    g_buff[i] = value;

                    msleep(1000);

                    qDebug() << objectName() << " generate : {" << i << ", " << value << "} ";

                    msleep(1000);

                    break;
                }
            }

            g_sem_used.release(); //貨物已經放進去了

            sleep(2);
        }
    }
};

class Customer : public QThread
{
  protected:
    void run()
    {
        while ( true)
        {
            g_sem_used.acquire();

            for(int i=0; i<SIZE; i++)
            {
               if( g_buff[i])
               {
                   int value = g_buff[i];

                   g_buff[i] = 0;

                   msleep(500);

                   qDebug() << objectName() << " consume : {" << i << ", " << value << "} ";

                   msleep(500);

                   break;
               }
            }

            g_sem_free.release();

            sleep(1);
        }
    }
};

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

    Producer p1;
    Producer p2;
    Producer p3;

    p1.setObjectName("Producer1");
    p2.setObjectName("Producer2");
    p3.setObjectName("Producer3");

    Customer c1;
    Customer c2;

    c1.setObjectName("Customer1");
    c2.setObjectName("Customer2");

    p1.start();
    p2.start();
    p3.start();
    c1.start();
    c2.start();

    return a.exec();
}

通過這種方式,可以提高併發性,使得三個生產者、兩個消費者都可以對倉庫進行操作。

重點:信號量允許N個線程同時訪問臨界資源。


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