04. 多執行緒 (Multi-thread)

4.多執行緒 (Multi-thread)

4.1概述

1.動機
(1)現代大多數應用程式都是多執行緒
(2)應用程式內運行多執行緒
(3)多工作的應用程式可以通過獨立的執行緒來實現
•顯示器更新
•獲取數據
•拼寫檢查
•回答一個網絡的要求
(4)行程(process)創建是重量級的,然而執行緒(Thread)創
建重量是輕的
(5)簡化編碼可以提高小效率
(6)內核(Kernels)一般是多執行緒

2.多執行緒服務架構


3.益處(Benefits) (1)Responsiveness
    響應度高:一個多線程的應用在執行中,即使其中的某個線程堵
 塞,其他的線程還可繼續執行,從而提高響應速度
(2)Resource Sharing
資源共享:同一進程的多個線程共享該進程的内存等資源

(3)Economy
經濟性:創建和切換線程的開銷要低於進程。

(4)Scalability
可擴展性:行程可以採用多處理機結構。

4.2多核心程式(Multicore Programming)

1.多核心程式
(1)多核心及多處理器秒對的挑戰
•劃分活動
•平衡
   •數據的分割
   •數據的依賴
   •測試和調試
(2)並行(Parallelism):一個系統可以同時執行多個任務
(3)並發(Concurrency):支持多個任務執行
•單處理器或單核心,排程提供的並發
(4)並行的類型
   •數據的並行(Data Parallelism)——分佈相同的數據子集在
多核心,每個數據具有相同的運作
   •任務的並行(Task Parallelism)——分佈執行緒在核心中,
每個執行緒具有獨特的運作

2.並發及並行
(1)並發執行在單核心系統




(2)並行執行在多核心系統



3.單執行緒排程(Single-threadedprocess)及多執行緒排程(Multithreaded
process)



4.阿達爾定律(Amdahl’s Law)
(1)并行化之后的效率提升:用并行前的执行速度和并行后的执行速度之比来表示
(2)S是串行部分
(3)N程式核心

(4)如果應該程式是75%平行/25%串行,從1核心到2核心,那麼加
速度(speedup)為1.6
(5)當N趨近與無窮,則speedup接近于1/S
5.用戶執行緒(User Threads)和內核執行緒(Kernel Threads)
(1)用戶執行緒——由用户级執行緒库进行管理
(2)三個主要用戶執行緒:
   •POSIX Pthreads
   •Windows執行緒
   •Java 執行緒
(3)內核執行緒——由内核支持
(4)內核執行緒的例子:
   •Windows
   •Solaris
   •Linux
   •Tru64 UNIX
   •Mas OS X

4.3多執行緒模型(Mutithreading Models)

4.3.1 多對一(Many-to-One)
許多到一個模型的實現(許多用戶線程到一個內核線程)允許應用程序創建任意數量可以並發執行的線程。
在一個多到一(用戶級線程)執行,所有線程的活動僅限於用戶空間。
此外,只有一個線程時可以訪問內核,所以只有一個調度實體是已知的操作系統。
因此,這種多線程模型提供有限的並發性和不利用多處理器。
在Solaris系統上的Java線程實施初期是多到一,如圖所示。

Graphic


4.3.2 一對一(One-to-One)
一對一模型(一個用戶線程1內核線程)是真實的多線程的最早的實現中。
在這個實現中,由應用程序創建的每個用戶級線程被稱為內核,並且所有線程可以同時訪問內核。
這種模式的主要問題是,它將會限制使用者要小心,節儉與線程
因為每個額外的線程增加了更多的“權重”的過程。
因此,這種模式的許多實現中,如Windows NT和OS/2線程包,支持限制系統中的線程的數量。

Graphic

 



4.3.3 多對多(Many-to-Many)

       許多一對多模式(許多用戶級線程多內核級線程)避免了許多的單對單模型的局限性
       同時擴展的多線程能力更進一步。許多一對多的模型,也稱為雙級車型
       同時減少每個線程的成本和重量減少編程工作量。

       在許多一對多的模型,一個程序可以作為是合適的不會使進程過重負擔或有盡可能多的線程。

       在這個模型中,一個用戶級線程庫提供以上內核線程用戶級線程的複雜調度。

       內核只需要管理當前活動的線程。

       一個many- to-many的實現在用戶級別上降低了編程的工作量

       因為它升降機上,可以在應用程序中有效地使用線程數限制。

       一個many- to-many的多線程實現從而提供了一個標準接口,一個簡單的編程模型,並為每個進程        獲得最佳性能。在Java上的Solaris操作環境是Java上的MT操作系統的第一個多到多的商業實現。

 

Graphic

 

 

4.4多執行緒庫(Thread Libraries)

多執行緒庫是專門為程式設計師提供創建和管理執行緒的API。主要有兩種方法來實做多執行緒庫。

第一種方法是在使用者空間(User space)中提供一個不需要核(Kernel)支持的多執行緒庫,裡面所有的代碼和數據都存在於使用者空間中,使用此執行緒庫中的函數只是調用了使用者空間中的一個區域函數(Local function),而不是由作業系統調用。

第二種方法則是執行一個由作業系統直接支持的核心階層(Kernel-level)的多執行緒庫,裡面的代碼和數據都存於核心空間(Kernel space)中。使用此執行緒庫裡面的函數通常是由作業系統調用。

目前主要使用的三種多執行緒庫分別為:

(1)POSIX Pthreads:

Pthreads是依據IEEE 1003的標準來定義創建和操縱執行緒的一套API。作為POSIX標準的擴展,Pthreads可以提供使用者層級(User-level)或是核心階層(Kernel-level)的多執行緒庫。

Pthreads API中大致共有100個函數調用,全都以"pthread_"開頭,並可以分為四類

1.執行緒管理:例如創建執行緒,等待(join)執行緒,查詢執行緒狀態等。

2.Mutex:創建、摧毀、鎖定、解鎖、設置屬性等操作。

3.Condition Variable:創建、摧毀、等待、通知、設置與查詢屬性等操作。

4.使用了讀寫鎖的執行緒間的同步管理。


(2)Win32 threads:

Win32執行緒庫適用於Windows作業系統的核心階層多執行緒庫,它在創建執行緒的技術在某些方面和Pthreads類似。不過要注意,在使用Win32 API時必須含有windows.h的標頭檔。


(3)Java threads:
執行緒是Java程序中程序執行的基本模型,Java語言和它的API為創建和管理執行緒提供了豐富的特徵集。所有Java程序至少由一個執行緒組成,即使一個只有main()函數的簡單Java程序也是在JVM中作為一個執行緒運行的。

在Java程序中有兩種創建執行緒的技術:

一種是繼承自Thread類別,直接產生執行緒。先宣告一個Thread的次類別,依所需重寫run()方法作為執行緒主體,並使用start()方法啟動執行緒並將執行權交至run()手上。

另一種則是安裝Runnable介面,間接產生執行緒。通常是applet程式使用,因applet程式已繼承Applet類別了,所以只能安裝Runnable介面來建立新執行緒。

 4.5執行緒的含義(Implicit Threading)

 三種方法

        1.Tread Pools

是執行緒的集合,可以用來執行許多幕後工作。這讓主要執行緒可以同步處理其他工作。
通常是在伺服器應用程式中採用執行緒集區。 每個傳入要求都會指派給執行緒集區中的一個執行緒,因此可以同步處理要求,而不用中斷主要執行緒或延遲後續要求的處理。
一旦集區中的執行緒完成工作,就會回到等待中執行緒的佇列,執行緒可以在這裡被重複使用。 重覆使用可讓應用程式省去為每個工作建立新執行緒的成本。
執行緒集區通常有執行緒的數目上限。 如果所有的執行緒都在忙碌中,多出來的工作會放置在佇列中,直到執行緒可提供服務為止。
 

        2.OpenMP

OpenMP是一個跨平台的多執行緒實現,主執行緒(順序的執行指令)生成一系列的子執行緒,並將任務劃分給這些子執行緒進行執行。這些子執行緒並行的運行,由運行時環境將執行緒分配給不同的處理器。

要進行並行執行的代碼片段需要進行相應的標記,用預編譯指令使得在代碼片段被執行前生成執行緒,每個執行緒會分配一個id,可以通過函數(called omp_get_thread_num())來獲得該值,該值是一個整數,主執行緒的id為0。在並行化的代碼運行結束後,子執行緒join到主執行緒中,並繼續執行程序。

默認情況下,各個執行緒獨立的執行並行區域的代碼。可以使用Work-sharing constructs來劃分任務,使每個執行緒執行其分配部分的代碼。通過這種方式,使用OpenMP可以實現任務並行和數據並行。

運行時環境分配給每個處理器的執行緒數取決於使用方法、機器負載和其他因素。執行緒的數目可以通過環境變數或者代碼中的函數來指定。在C/C++中,OpenMP的函數都聲明在頭文件omp.h中。

        3.Grand Central Dispatch

Grand Central Dispatch 簡稱(GCD)是蘋果公司開發的技術,以優化的應用程序支持多核心處理器和其他的對稱多處理系統的系統。這建立在任務並行執行的線程池模式的基礎上的。它首次發佈在Mac OS X 10.6 ,iOS 4及以上也可用。

        其他: Microsoft Threading Building Block (TBB)

在多核的平台上開發並行化的程序,必須合理地利用系統的資源-如與內核數目相匹配的線程,內存的合理訪問次序,最大化重用緩存。有時候用戶使用(系統)低級的應用接口創建、管理線程,很難保證是否程序處於最佳狀態。 Intel Thread Building Blocks (TBB)很好地解決了上述問題

 

    優點

     1.比較快去service a request 

     2.允許在application(s)的n個threads被約束size

     3.分開的task

     4.工作可以被規劃或設定優先權

 

4.6執行緒的問題(Threading Issues)

4.6.1 fork()與exec()系統呼叫

exec()可重新載入可執行檔 

把可執行檔的機器碼搬進程序的記憶體 

然後呼叫可執行檔之中的 main 函式 

fork()建立一個子程序 

完整的複製一份程序 

正本的fork函式會回傳副本的PID 

複本的fork回傳值是0

 

4.6.2 訊號處理

signals 是UNIX系統中通知特別事件發生

被特殊事件產生

被傳遞給process

如果被傳遞,就要被處理(default signal handler user-defined signal handler)

選項:

傳給有允許的(signal applies)

傳給每一個

傳給確定的

指定一個thread 來接收所有 signals

 

4.6.3執行序取消

1.非同步取消 (asynchronous cancellation)

立即結束執行緒

 

2.延遲取消 (deferred cancellation)

執行緒週期性的檢查是否取消

 

Mode State Type
Off Disable  
Deferred Enable Deferred
Asynchrous Enable Asynchrous

 

 

 

 

pthread 可以設定要使用哪種cancellation points

預設是 deferred cancellation

 

4.6.4執行序的本機儲存 

讓指定多個執行緒中的每個執行緒來配置位置

以儲存執行緒特定資料 (using a thread pool)

4.6.5排程活動

LWP-lightweight process process 

是介於user thread 和 kernel thread之間的資料結構

提供一個虛擬處理器讓application 執行排班

需要幾個LWP由

CPU-bound application

I/O-intensive application決定

向上呼叫到kernel to the thread library來執行

 

4.7作業系統的例子(Operating System Examples)

 4.7.1 Windows XP執行緒

Windows XP 應用程式 以一個個別的行程執行,其中每一個行程可能包含一個或多個執行緒。

Windows XP所使用的模式是一對一,其中每一個使用者層次的執行緒對應到一個相關的核心執行緒。而視窗也提共了fiber程式庫的支援,此程式庫提供了多對多模式的功能。藉著使用執行緒的程式庫,屬於一個行程的每一個執行緒可以存取此行程虛擬位址空間。

 

一個行程的一般元件包括:

1.唯一識別此行程的行程ID

2.表示處理器狀態的暫存器組

3.當執行緒在使用者模式執行時所使用的使用者堆疊。同理,每一個執行緒也有一個核心堆疊,當此執行緒在核心模式執行使用時。

4.被不同的執行時程式庫和動態連結程式庫(DLLs)所使用的私有儲存區域。

 

暫存器組,堆疊和私有儲存區域通稱為內容(context)。執行緒的主要資料結構包括了:

1.ETHREAD(不包括執行緒區段),主要包括了一個指向此執行緒所屬行程的指標和此執行緒開始控制之常式的位置,並有一個指向相對應KTHREAD。

2.KTHREAD(核心執行緒區段),包括了此執行緒的排班和同步資訊、核心堆疊和指向TEB的指標。

3.TEB(執行環境區段),是使用者空間的資料結構。在其他欄位間包含了執行緒辨識器,一個使用者模式的堆疊和一個執行緒特有的資料陣列。

 

結構說明如圖: 

 

4.7.2 Linux執行緒

Linux提供一個fork()系統呼叫,他擁有傳統的複製行程功能。Linux也提供clone()系統呼叫來產生執行緒。然而Linux無法區分行程與執行緒。事實上,當在程式內一連串控制時,Linux通常使用任務而不是行程或執行緒。

當啟動clone(),它傳遞一組旗標,決定有多少共用發生在父任務與子任務之間。以下舉例一些旗標:

 

CLONE_FS --------------------共用檔案系統訊息

CLONE_VM ---------------共用相同記憶體空間

CLONE_SIGHAND ---------供用訊號處理程式

CLONE_FILES ------------共用一組的開啟檔案

 

如果clone()傳遞以上所以旗標,父任務與子任務將共用相同的檔案系統訊息、記憶體空間、訊號處理程式和一組開啟檔案。此種型式便是Linux的產生執行緒,因為父任務與子任務共享大部份資源。反之,如果當clone()載入時,沒有發生共用,結果與系統呼叫fork()所提供的功能類似。