一、進程與線程的概念
什麼是程序?
程序是計算機存儲系統中的數據文件。分爲源代碼程序和可執行程序。源代碼程序一般爲文本文件,用來描述程序的行爲和功能,可執行程序一般爲二進制文件,可以直接加載並執行。
源代碼程序經過編譯器編譯,就成爲可執行程序。
什麼是進程?
廣義的概念認爲是程序關於某個數據集合的一次運行活動,狹義地講,就是程序被加載到內存中後,執行得到的進程。
程序和進程的區別:
程序是硬盤中的靜態文件,是存儲系統中的一段二進制表示。
進程是內存中動態的運行實體,如數據段、代碼段、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個線程同時訪問臨界資源。