1. 程式人生 > 其它 >學習筆記7(必做)

學習筆記7(必做)

第4章 併發程式設計

4.1 平行計算導論

在早期,大多數計算機只有一個處理元件,稱為處理器或中央處理器(CPU)。受這種硬體條件的限制,計算機程式通常是為序列計算編寫的。要求解某個問題,先要設計一種演算法,描述如何一步步地解決問題,然後用計算機程式以序列指令流的形式實現該演算法。在只有一個CPU的情況下,每次只能按順序執行某演算法的一個指令和步驟。但是,基於分治原則(如二叉樹查詢和快速排序等)的演算法經常表現出高度的並行性,可通過使用並行或併發執行來提高計算速度。平行計算是一種計算方案,它嘗試使用多個執行並行演算法的處理器更快速地解決問題。過去,由於平行計算對計算資源的大量需求,普通程式設計師很少能進行平行計算近年來,隨著多核處理器的出現,大多數作業系統(如Linux)都支援對稱多處理(SMP)。甚至對於普通程式設計師來說,平行計算也已經成為現實。顯然,計算的未來發展方向 是平行計算-因此,迫切需要在電腦科學和計算機工程專業學生的早期學習階段引入並行 計算。在本章中,我們將介紹通過併發程式設計實現平行計算的基本概念和方法。

4.1.1 順序演算法與並行演算法

在描述順序演算法時,常用的方法是用一個begin-end程式碼塊列出演算法,如下圖左側所示。

begin-end程式碼塊中的順序演算法可能包含多個步驟。所有步驟都是通過單個任務依次執行的, 每次執行一個步驟。當所有步驟執行完成時,演算法結束。相反,圖的右側為並行演算法的描述,它使用cobegin-coend程式碼塊來指定並行演算法的獨立任務。在cobegin-coend塊中.所有任務都是並行執行的,緊接著cobegin-coend程式碼塊的下一個步驟將只在所有這些任務完成之後執行。

4.1.2 並行性與併發性

通常,並行演算法只識別可並行執行的任務,但是它沒有規定如何將任務對映到處理元件。在理想情況下,並行演算法中的所有任務都應該同時實時執行。然而,真正的並行執行只曲在有多個處理元件的系統中實現,比如多處理器或多核系統。在單CPU系統中,一次只能執行一個任務。在這種情況下,不同的任務只能併發執行,即在邏輯上並行執行。在單CPU系統中,併發性是通過多工處理來實現的,該內容已在第3章中討論過。在本章的最後,我們將在一個程式設計專案中再次講解和示範多工處理的原理和方法。

4.2 執行緒

4.2.1 執行緒的原理

一個作業系統(OS)包含許多併發程序“在程序模型中,程序是獨立的執行單元。所有程序均在核心模式或使用者模式下執行。在核心模式下,各程序在唯一地址空間上執行,與其他程序是分開的。雖然每個程序都是一個獨立的單元,但是它只有一個執行路徑。當某程序必須等待某事件時,例如I/O完成事件,它就會暫停,整個程序會停止執行匸執行緒是某程序同一地址空間上的獨立執行單元。建立某個程序就是在一個唯一地址空間建立一個主執行緒。當某程序開始時,就會執行該程序的主執行緒。如果只有一個主執行緒,那麼程序和執行緒實際上並沒有區別。但是,主執行緒可能會建立其他執行緒。每個執行緒又可以建立更多的執行緒等。某程序的所有執行緒都在該程序的相同地址空間中執行,但每個執行緒都是一個獨立的執行單元。線上程模型中,如果一個執行緒被掛起,其他執行緒可以繼續執行。除了共享共同的地址空間之外.執行緒還共享程序的許多其他資源,如使用者id、開啟的檔案描述符和訊號等。打個簡單的比方,程序是一個有房屋管理員(主執行緒)的房子,執行緒是住在程序房子裡的人。房子裡的每個人都可以獨立做自己的事情,但是他們會共用一些公用設施,比如同一個信箱、廚房和浴室等。過去,大多數計算機供應商都是在自己的專有作業系統中支援執行緒。不同系統之間的實現有極大的區別。目前,幾乎所有的作業系統都支援Pthread,它是IEEE POSIX 1003.1c的執行緒標準(POS1X 1995)o如需瞭解更多資訊,讀者可査閱更多關於Pthread程式設計的書籍(Buttlar等1996)和線上文章(Pthreads 2017 )。

4.2.2 執行緒的優點

與程序相比,執行緒有許多優點。
(1)執行緒建立和切換速度更快:程序的上下文複雜而龐大。其複雜性主要來自管理程序映像的需要。例如,在具有虛擬記憶體的系統中.程序映像可能由叫作頁面的許多記憶體單元組成。在執行過程中.有些頁面在記憶體中,有些則不在記憶體中。作業系統核心必須使用多個頁表和多個級別的硬體輔助來跟蹤每個程序的頁面-要想建立新的程序,作業系統必須為進
程分配記憶體並構建頁表。若要在某個程序中建立執行緒,作業系統不必為新的執行緒分配記憶體和建立頁表,因為執行緒與程序共用同一個地址空間。所以,建立執行緒比建立程序更快。另外.由於以下原因,執行緒切換比程序切換更快。程序切換涉及將一個程序的複雜分貞環境替換為另一個程序的複雜分頁環境,需要大量的操作和時間。相比之下,同一個程序中的執行緒切換要簡單得多、也快得多,因為作業系統核心只需要切換執行點,而不需要更改程序映像。
(2)執行緒的響應速度更快:一個程序只有一個執行路徑。當某個程序被掛起時,整個程序都將停止執行。.相反,當某個執行緒被掛起時,同進程中的其他執行緒可以繼續執行。這使得有多個執行緒的程式響應速度更快.例如,在一個多執行緒的程序中,當一個執行緒被阻塞以等待I/O時,其他執行緒仍可在後臺進行計算。在有執行緒的伺服器中,伺服器可同時服務多個客戶機。
(3)執行緒更適合平行計算:平行計算的目標是使用多個執行路徑更快地解決問題。基於分治原則(如二叉樹查詢和快速排序等)的演算法經常表現出高度的並行性,可通過使用並行或併發執行來提高計算速度。這種演算法通常要求執行實體共享公用資料。在程序模型中,各程序不能有效共享資料,因為它們的地址空間都不一樣。為了解決這個問題,程序必須使用程序間通訊(IPC)來交換資料或使用其他方法將公用資料區包含到其地址空間中。相反,同一程序中的所有執行緒共享同一地址空間中的所有(全域性)資料。因此,使用執行緒編寫並行執行的程式比使用程序編寫更簡單、更自然。

4.2.3 執行緒的缺點

另一方面,執行緒也有一些缺點,其中包括:
(1)由於地址空間共享,執行緒需要來自使用者的明確同步。
(2)許多庫函式可能對執行緒不安全,例如傳統strtok()函式將一個字串分成一連串令牌。通常,任何使用全域性變數或依賴於靜態記憶體內容的函式.執行緒都不安全。為了使庫函式適應執行緒環境,還需要做大量的工作。
(3)在單CPU系統上,使用執行緒解決問題實際上要比使用順序程式慢,這是由在執行時建立執行緒和切換上下文的系統開銷造成的。

4.3 執行緒操作

執行緒的執行軌跡與程序類似。執行緒可在核心模式或使用者模式下執行在使用者模式下,執行緒在程序的相同地址空間中執行,但每個執行緒都有自己的執行堆疊。執行緒是獨立的執行単元,可根據作業系統核心的排程策略,對核心進行系統呼叫,變為掛起、啟用以繼續執行等。為了利用執行緒的共享地址空間,作業系統核心的排程策略可能會優先選擇同-程序中的執行緒,而不是不同程序中的執行緒。截至目前,幾乎所有的作業系統都支援POSIX Pthread, 定義了一系列標準應用程式程式設計介面(API)來支援執行緒程式設計。下面,我們將討論和演示 Linux 中的 Pthread 併發程式設計(Goldt 等 1995 : IBM ; Love 2005 ; Linux Man Page Progect 2017 )。

4.4 執行緒管理函式

Pthread庫提供了用於執行緒管理的以下API。

pthread_create(thread, attr, function, arg): create thread
pthread_exit(status)	:	terminate thread
pthread_cancel(thread)	:	cancel thread
pthread_attr_init(attr)	:	initialize thread attributes
pthread_attr_destroy(attr): destroy thread attribute

4.4.1 建立執行緒

使用pthread_create()函式建立執行緒。

int pthread_create (pthread_t *pthread_id, pthread_attr_t *attr, void *(*func)(void *), void *arg);

如果成功則返回0,如果失敗則返回錯誤程式碼。pthread_create()函式的引數為

  • pthread_id是指向pthread_t型別變數的指標。它會被作業系統核心分配的唯一執行緒ID填充。在POSIX中,pthread_t是一種不透明的型別。程式設計師應該不知道不透明物件的內容,因為它可能取決於實現情況。執行緒可通過pthread_self()函式獲得自己的ID。在Linux中,pthreadj型別被定義為無符號長整型,因此執行緒1D可以列印為%111。
  • attr是指向另一種不透明資料型別的指標,它指定執行緒屬性,下面將對此進行更詳細的說明。
  • fimc是要執行的新執行緒函式的入口地址。
  • arg是指向執行緒函式引數的指標,可寫為:
void *func(void *arg)

其中,attr引數最複雜。下面給出了attr引數的使用步驟。
(1)定義一個pthread屬性變數pthread_attr_t attr,,
(2)用pthread_attr_init(&attr)初始化屬性變數。
(3)設定屬性變數並在pthread_create()呼叫中使用。
(4)必要時,通過pthread_attr_destroy(&attr)釋放attr資源。
下面列出了使用屬性引數的一些示例。每個執行緒在建立時都預設可與其他執行緒連線。必要時,可使用分離屬性建立一個執行緒,使它不能與其他執行緒連線。下面的程式碼段顯示瞭如何建立一個分離執行緒。

pthread_attr_t attr;	// define an attr variable
pthread_attr_init(&attr);	// initialize attr
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // set attr pthread_create(&thread_idz	func, NULL);	// create thread with attr
pthread_attr_destroy;	// optional: destroy attr

每個執行緒都使用預設堆疊的大小來建立。在執行過程中,執行緒可通過函式找到它的堆疊大小:

size_t pthread_attr_getstacksize()

它可以返回預設的堆疊大小。下面的程式碼段顯示瞭如何建立具有特定堆疊大小的執行緒。

pthread_attr_t attr;            // attr variable
size_t stacksize;               // stack size
pthread_attr_init(&attr);       // initialize attr
stacksize = 0x10000;            // stacksize=16KB;
pthread_attr_setstacksize(ftattr, stacksize);   // set stack size in attr
pthread_create(&threads[t], &attr, func, NULL); // create thread with stack size

如果attr引數為NULL,將使用預設屬性建立執行緒。實際上,這是建立執行緒的建議方法,除非有必要更改執行緒屬性,否則應該遵循這種方法。接下來,我們將attr設定為NULL,就可始終使用預設屬性。

4.4.2 執行緒ID

執行緒ID是一種不透明的資料型別,取決於實現情況。因此,不應該直接比較執行緒ID。如果需要,可以使用pthread_equal()函式對它們進行比較。

int pthread_equal (pthread_t tl, pthread_t t2);

如果是不同的執行緒,則返回0,否則返回非0。

4.4.3 執行緒終止

執行緒函式結束後,執行緒即終止。或者,執行緒可以呼叫函式

int pthread_exit (void *status);

進行顯式終止,其中狀態是執行緒的退出狀態。通常,0退出值表示正常終止,非0值表示異常終止。

4.4.4 執行緒連線

一個執行緒可以等待另一個執行緒的終止,通過:

int pthread_join (pthread_t thread, void **status_ptr);

終止執行緒的退出狀態以status_ptr返回。

4.6執行緒同步

由於執行緒在程序的同一地址空間中執行,它們共享同一地址空間中的所有全域性變數和數 據結構。當多個執行緒試圖修改同一共享變數或資料結構時,如果修改結果取決於執行緒的執行 順序,則稱之為競態條件。在併發程式中,絕不能有競態條件。否則,結果可能不一致。除 了連線操作之外,併發執行的執行緒通常需要相互協作。為了防止出現競態條件並且支援執行緒 協作,執行緒需要同步。通常,同步是一種機制和規則,用於確保共享資料物件的完整性和並 發執行實體的協調性。它可以應用於核心模式下的程序,也可以應用於使用者模式下的執行緒。 下面,我們將討論Pthread中執行緒同步的具體問題。

———————————————————————————————————————————————————————————————— 轉載麻煩附上本文連結和本宣告,感謝! 博主<葉家星>部落格園連結如下:https://www.cnblogs.com/yejiaxing-01/