多線程入門

1、背景

爲了更好的理解多線程的概念,先對進程,線程的概念背景做一下簡單介紹。

早期的計算機系統都只允許一個程序獨佔系統資源,一次只能執行一個程序。在大型機年代,計算能力是一種寶貴資源。對於資源擁有方來說,最好的生財之道自然是將同一資源同時租售給儘可能多的用戶。最理想的情況是壟斷全球計算市場。所以不難理解爲何當年IBM預測“全球只要有4臺計算機就夠了”。

這種背景下,一個計算機能夠支持多個程序併發執行的需求變得十分迫切。由此產生了進程的概念。進程在多數早期多任務操作系統中是執行工作的基本單元。進程是包含程序指令和相關資源的集合。每個進程和其他進程一起參與調度,競爭CPU,內存等系統資源。每次進程切換,都存在進程資源的保存和恢復動作,這稱爲上下文切換。

進程的引入可以解決支持多用戶的問題,但是多進程系統也在如下方面產生了新的問題:
1.進程頻繁切換引起的額外開銷可能會嚴重影響系統性能。
2.進程間通信要求複雜的系統級實現。


在程序功能日趨複雜的情況下,上述缺陷也就凸現出來。比如,一個簡單的GUI程序,爲了有更好的交互性,通常用一個任務支持界面交互,另一個任務支持後臺運算。如果每個任務均由一個進程來實現,那會相當低效。對每個進程來說,系統資源看上去都是其獨佔的。比如內存空間,每個進程認爲自己的內存空間是獨有的。一次切換,這些獨立資源都需要切換。

由此就演化出了利用分配給同一個進程的資源,儘量實現多個任務的方法。這也就引入了線程的概念。同一個進程內部的多個線程,共享的是同一個進程的所有資源。

比如,與每個進程獨有自己的內存空間不同,同屬一個進程的多個線程共享該進程的內存空間。例如在進程地址空間中有一個全局變量globalVar,若A線程將其賦值爲1,則另一線程B可以看到該變量值爲1。兩個線程看到的全局變量globalVar是同一個變量。


通過線程可以支持同一個應用程序內部的併發,免去了進程頻繁切換的開銷,另外併發任務間通信也更簡單。

目前多線程應用主要用於兩大領域:網絡應用和嵌入式應用。爲什麼在這兩個領域應用較多呢?因爲多線程應用能夠解決兩大問題:

1.併發。

網絡程序具有天生的併發性。比如網絡數據庫可能需要同時處理數以千計的請求。而由於網絡連接的時延不確定性和不可

靠性,一旦等待一次網絡交互,可以讓當前線程進入睡眠,退出調度,處理其他線程。這樣就能夠有效利用系統資源,充分發揮系統處理能力。

2.實時。線程的切換是輕量級的,所以可以保證足夠快。每當有事件發生,狀態改變,都能有線程及時響應,而且每次線程內部處理的計算強度和複雜度都不大。在這種情況下,多線程實現的模型也是高效的。


在有些語言中,對多線程或者併發的支持是直接內建在語言中的,比如Ada和VHDL。在C++裏面,對多線程的支持由具體操作系統提供的函數接口支持。不同的系統中具體實現方法不同。後面所有例子只給出windows和Unix/Linux的實現。

在後面的實現中,考慮的是儘量封裝隔離底層的多線程函數接口,屏蔽操作系統底層的線程實現具體細節,介紹的重點是多線程編程中較通用的概念。同時也儘量體現C++面向對象的一面。

最後,由於空閒時間有限,我只求示例代碼能夠明確表達自己的意思即可。至於代碼的盡善盡美就只能有勞各位盡力以爲之了。


2、線程的創建

在一個線程的生存期內,可以在多種狀態之間轉換。不同操作系統可以實現不同的線程模型,定義許多不同的線程狀態,每個狀態還可以包含多個子狀態。但大體說來,如下幾種狀態是通用的:

       就緒:參與調度,等待被執行。一旦被調度選中,立即開始執行。
       運行:佔用CPU,正在運行中。
       休眠:暫不參與調度,等待特定事件發生。
       中止:已經運行完畢,等待回收線程資源(要注意,這個很容易誤解,後面解釋)。

線程存在於進程之中。進程內所有全局資源對於內部每個線程均是可見的。
進程內典型全局資源有如下幾種:

1.代碼區;這意味着當前進程空間內所有可見的函數代碼,對於每個線程來說也是可見的。

2.靜態存儲區。

3.全局變量。

4.靜態變量。
5.動態存儲區;也就是堆空間。

線程內典型的局部資源有:

1.本地棧空間;存放本線程的函數調用棧,函數內部的局部變量等。
2.部分寄存器變量;例如本線程下一步要執行代碼的指針偏移量。


一個進程發起之後,會首先生成一個缺省的線程,通常稱這個線程爲主線程。C/C++程序中主線程就是通過main函數進入的線程。由主線程衍生的線程稱爲從線程,從線程也可以有自己的入口函數,作用相當於主線程的main函數。

這個函數由用戶指定;Pthread和winapi中都是通過傳入函數指針實現。在指定線程入口函數時,也可以指定入口函數的參數。

就像main函數有固定的格式要求一樣,線程的入口函數一般也有固定的格式要求,參數通常都是void *類型,返回類型在

pthread中是void *, winapi中是unsigned int,而且都需要是全局函數。

最常見的線程模型中,除主線程較爲特殊之外,其他線程一旦被創建,相互之間就是對等關係 (peer to peer), 不存在隱含的層次關係。每個進程可以創建的最大線程數由具體實現決定。

爲了更好的理解上述概念,下面通過具體代碼來詳細說明。

線程類接口定義

一個線程類無論具體執行什麼任務,其基本的共性無非就是:

1.創建並啓動線程
2.停止線程

另外還有就是能睡,能等,能分離執行(有點拗口,後面再解釋)。還有其他的可以繼續加…

將線程的概念加以抽象,可以爲其定義如下的類:

文件 thread.h

#ifndef __THREAD__H_ #define __THREAD__H_ class Thread { public: Thread(); virtual ~Thread(); int start (void * = NULL); void stop(); void sleep (int); void detach(); void * wait(); protected: virtual void * run(void *) = 0; private: //這部分win和unix略有不同,先不定義,後面再分別實現。 //順便提一下,我很不習慣寫中文註釋,這裏爲了更明白一 //點還是選用中文。   };  #endif

Thread::start() //函數是線程啓動函數,其輸入參數是無類型指針。 Thread::stop() //函數中止當前線程。 Thread::sleep() //函數讓當前線程休眠給定時間,單位爲秒。 Thread::run() //函數是用於實現線程類的線程函數調用。 Thread::detach() //和thread::wait()函數涉及的概念略複雜一些。在稍後再做解釋。

Thread類是一個基類,派生類可以重載自己的線程函數。下面是一個例子。

示例程序

代碼寫的都不夠精緻,暴力類型轉換比較多,歡迎有閒階級美化,謝過了先。

文件create.h

#ifndef __CREATOR__H_ #define __CREATOR__H_

#include <stdio.h> #include "thread.h"

class Create: public Thread

{

protected: void * run(void * param) {     char * msg = (char*) param;     printf ("%s\n", msg);     //sleep(100); 可以試着取消這行註釋,看看結果有什麼不同。     printf("One day past.\n");     return NULL; } }; #endif

然後,實現一個main函數,來看看具體效果:

文件Genesis.cpp

#include <stdio.h> #include "create.h"

int main(int argc, char** argv) { Create monday; Create tuesday; printf("At the first God made the heaven and the earth.\n"); monday.start("Naming the light, Day, and the dark, Night, the first day."); tuesday.start("Gave the arch the name of Heaven, the second day."); printf("These are the generations of the heaven and the earth.\n");

return 0; }

編譯運行,程序輸出如下:

At the first God made the heaven and the earth. These are the generations of the heaven and the earth.

令人驚奇的是,由週一和週二對象創建的子線程似乎並沒有執行!這是爲什麼呢?別急,在最後的printf語句之前加上如下語句:

monday.wait(); tuesday.wait();

重新編譯運行,新的輸出如下:

At the first God made the heaven and the earth. Naming the light, Day, and the dark, Night, the first day. One day past. Gave the arch the name of Heaven, the second day. One day past.

These are the generations of the heaven and the earth.

爲了說明這個問題,需要了解前面沒有解釋的Thread::detach()和Thread::wait()兩個函數的含義。

無論在windows中,還是Posix中,主線程和子線程的默認關係是:
無論子線程執行完畢與否,一旦主線程執行完畢退出,所有子線程執行都會終止。這時整個進程結束或僵死(部分線程保持一種終止執行但還未銷燬的狀態,而進程必須在其所有線程銷燬後銷燬,這時進程處於僵死狀態),在第一個例子的輸出中,可以看到子線程還來不及執行完畢,主線程的main()函數就已經執行完畢,從而所有子線程終止。

需要強調的是,線程函數執行完畢退出,或以其他非常方式終止,線程進入終止態(請回顧上面說的線程狀態),但千萬要記住的是,進入終止態後,爲線程分配的系統資源並不一定已經釋放,而且可能在系統重啓之前,一直都不能釋放。終止態的線程,仍舊作爲一個線程實體存在與操作系統中。(這點在win和unix中是一致的。)而什麼時候銷燬線程,取決於線程屬性。

通常,這種終止方式並非我們所期望的結果,而且一個潛在的問題是未執行完就終止的子線程,除了作爲線程實體佔用系統資源之外,其線程函數所擁有的資源(申請的動態內存,打開的文件,打開的網絡端口等)也不一定能釋放。所以,針對這個問題,主線程和子線程之間通常定義兩種關係:

1.可會合(joinable);這種關係下,主線程需要明確執行等待操作。在子線程結束後,主線程的等待操作執行完畢,子線程

和主線程會合。這時主線程繼續執行等待操作之後的下一步操作。主線程必須會合可會合的子線程,Thread類中,這個操作通過在主線程的線程函數內部調用子線程對象的wait()函數實現。這也就是上面加上三個wait()調用後顯示正確的原因。必須強調的是,即使子線程能夠在主線程之前執行完畢,進入終止態,也必需顯示執行會合操作,否則,系統永遠不會主動銷燬線程,分配給該線程的系統資源(線程id或句柄,線程管理相關的系統資源)也永遠不會釋放。

2.相分離(detached);顧名思義,這表示子線程無需和主線程會合,也就是相分離的。這種情況下,子線程一旦進入終止態

,系統立即銷燬線程,回收資源。無需在主線程內調用wait()實現會合。Thread類中,調用detach()使線程進入detached狀態。這種方式常用在線程數較多的情況,有時讓主線程逐個等待子線程結束,或者讓主線程安排每個子線程結束的等待順序,是很困難或者不可能的。所以在併發子線程較多的情況下,這種方式也會經常使用。缺省情況下,創建的線程都是可會合的。可會合的線程可以通過調用detach()方法變成相分離的線程。但反向則不行。

UNIX實現

文件 thread.h

#ifndef __THREAD__H_ #define __THREAD__H_ class Thread { public: Thread(); virtual ~Thread(); int start (void * = NULL); void stop(); void sleep (int); void detach(); void * wait(); protected: virtual void * run(void *) = 0; private: pthread_t handle; bool started; bool detached; void * threadFuncParam; friend void * threadFunc(void *); };

//pthread中線程函數必須是一個全局函數,爲了解決這個問題 //將其聲明爲靜態,以防止此文件之外的代碼直接調用這個函數。 //此處實現採用了稱爲Virtual friend function idiom 的方法。 Static void * threadFunc(void *);  #endif

文件thread.cpp

#include <pthread.h>

#include <sys/time.h> #include thread.h

static void * threadFunc (void * threadObject) { Thread * thread = (Thread *) threadObject; return thread->run(thread->threadFuncParam); }

Thread::Thread() { started = detached = false; }

Thread::~Thread() { stop(); }

bool Thread::start(void * param) { pthread_attr_t attributes; pthread_attr_init(&attributes); if (detached) {     pthread_attr_setdetachstate(&attributes, PTHREAD_CREATE_DETACHED); }

threadFuncParam = param;

if (pthread_create(&handle, &attributes, threadFunc, this) == 0) {     started = true; }

pthread_attr_destroy(&attribute); }

void Thread::detach() { if (started && !detached) {     pthread_detach(handle); }

detached = true; }

void * Thread::wait() { void * status = NULL; if (started && !detached) {     pthread_join(handle, &status); } return status; }

void Thread::stop() { if (started && !detached) {     pthread_cancel(handle);     pthread_detach(handle);     detached = true; }  }

void Thread::sleep(unsigned int milliSeconds) { timeval timeout = { milliSeconds/1000, millisecond%1000}; select(0, NULL, NULL, NULL, &timeout); }

小結

本節的主要目的是幫助入門者建立基本的線程概念,以此爲基礎,抽象出一個最小接口的通用線程類。在示例程序部分,初學者可以體會到並行和串行程序執行的差異。有興趣的話,大家可以在現有線程類的基礎上,做進一步的擴展和嘗試。如果覺得對線程的概念需要進一步細化,大家可以進一步擴展和完善現有Thread類。

想更進一步瞭解的話,一個建議是,可以去看看其他語言,其他平臺的線程庫中,線程類抽象了哪些概念。比如Java, perl等跨平臺語言中是如何定義的,微軟從winapi到dotnet中是如何支持多線程的,其線程類是如何定義的。這樣有助於更好的理解線程的模型和基礎概念。

另外,也鼓勵大家多動手寫寫代碼,在此基礎上嘗試寫一些代碼,也會有助於更好的理解多線程程序的特點。比如,先開始的線程不一定先結束。線程的執行可能會交替進行。把printf替換爲cout可能會有新的發現,等等。

每個子線程一旦被創建,就被賦予了自己的生命。管理不好的話,一隻特例獨行的豬是非常讓人頭痛的。

對於初學者而言,編寫多線程程序可能會遇到很多令人手足無措的bug。往往還沒到考慮效率,避免死鎖等階段就問題百出,而且很難理解和調試。這是非常正常的,請不要氣餒,後續文章會盡量解釋各種常見問題的原因,引導大家避免常見錯誤。目前能想到入門階段常遇到的問題是:

1.內存泄漏,系統資源泄漏。
2.程序執行結果混亂,但是在某些點插入sleep語句後結果又正確了。
3.程序crash, 但移除或添加部分無關語句後,整個程序正常運行(假相)。
4.多線程程序執行結果完全不合邏輯,出於預期。


轉自:http://snoopyxdy.blog.163.com/blog/static/6011744020138733425374/

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