1. 程式人生 > 其它 >2021-2022-1-diocs-併發程式設計第八週學習筆記

2021-2022-1-diocs-併發程式設計第八週學習筆記

20191205 2021-2022-1-diocs-併發程式設計(第八週學習筆記)

一、任務詳情

自學教材第4章,提交學習筆記(10分)

知識點歸納以及自己最有收穫的內容 (3分)

問題與解決思路(2分)

實踐內容與截圖,程式碼連結(3分)

...(知識的結構化,知識的完整性等,提交markdown文件,使用openeuler系統等)(2分)

二、教材內容歸納整理

本章論述了併發程式設計,介紹了平行計算的概念,指出了平行計算的重要性;比較了順序演算法與並行演算法,以及並行性與併發性;解釋了執行緒的原理及其相對於程序的優勢;通過示例介紹了 Pthread 中的執行緒操作,句括執行緒管理函式。互斥量、連線、條件變數和屏魔等線

程同步工具;通過具體示例演示瞭如何使用執行緒進行併發程式設計,包括矩陣計算、快速排序和用併發執行緒求解線性方程組等方法;解釋了死鎖問題,並說明了如何防止併發程式中的死鎖問題;討論了訊號量,並論證了它們相對於條件變數的優點;還解釋了支援Linux 中執行緒的獨特方式。程式設計專案是為了實現使用者級執行緒。它提供了一個基礎系統來幫助讀者開始工作。這個基礎系統支援併發任務的動態建立、執行和終止,相當幹在某個程序的同一地址空間中執行執行緒。讀者可通過該專案實現執行緒同步的執行緒連線、互斥量和訊號量,並演示它們在併發程式中的用法。該程式設計專案會讓讀者更加深入地瞭解多工處理、執行緒同步和併發程式設計的原理及方法。

思維導圖

1.平行計算導論

(1)順序演算法與並行演算法

2)並行性與併發性

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

2.執行緒

(1)執行緒的原理

執行緒是某程序同一地址空間上的獨立執行單元。建立某個程序就是在一個唯一地址空間建立一個主執行緒。當某程序開始時,就會執行該程序的主執行緒。如果只有一個主執行緒,那麼程序和執行緒實際上並沒有區別。但是,主執行緒可能會建立其他執行緒。每個執行緒又可以建立更多的執行緒等。

(2)執行緒的優點

  1. 執行緒建立和切換速度更快;
  2. 執行緒的相應速度更快;
  3. 執行緒更適合平行計算;

(3)執行緒的缺點

1)由於地址空間共享,執行緒需要來自使用者的明確同步。

2)許多庫函式可能對執行緒不安全,例如傳統 strtok()函式將一個字串分成一連串令牌。通常,任何使用全域性變數或依賴於靜態記憶體內容的函式,執行緒都不安全。為了使庫函式適應執行緒環境,還需要做大量的工作。

3)在單 CPU系統上,使用執行緒解決問題實際上要比使用順序程式慢,這是由在執行時建立執行緒和切換上下文的系統開銷造成的。

3.執行緒管理函式

1)建立執行緒

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

int prhread_create (pthread_t *pthread_id,pthread_attr_t *attr,

Void *(*func)(void *), void *arg);

如果成功則返回0,如果失敗則返回錯誤程式碼。

其中,attr引數最複雜。下面給出了 attr引數的使用步驟。

1.定義一個pthread屬性變數 pthread_attr_t attr

2.pthread_attr_init&attr)初始化屬性變數。

3.設定屬性變數並在 pthread_create()呼叫中使用。

4.必要時,通過 pthread_attr_destroy&attr)釋放 attr資源。

2)執行緒ID

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

int pthread_equal (pthread_t t1, pthread_t t2);

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

3)執行緒終止

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

int pthread_exit (void *status);

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

4)執行緒連線

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

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

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

4.執行緒同步

(1)互斥量

1.一種是靜態方法:

pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER

定義互斥量m,並使用預設屬性對其進行初始化。

2.一種是動態方法:使用pthread_mutex_init()函式,可通過attr引數設定互斥屬性。

pthread_mutex_init(pthread_mutex_t *m,pthread_mutexattr_t,*attr);

(2)死鎖預防

有多種方法可以解決可能的死鎖問題,其中包括死鎖預防、死鎖規避、死鎖檢測和恢復等。在實際系統中,唯一可行的方法是死鎖預防,試圖在設計並行演算法時防止死鎖的發生。一種簡單的死鎖預防方法是對互斥量進行排序,並確保每個執行緒只在一個方向請求互斥量,這樣請求序列中就不會有迴圈。

但是,僅使用單向加鎖請求來設計每個並行演算法是不可能的。在這種情況下,可以使用條件加鎖函式 pthread mutex trylock(來預防死鎖。如果互斥量已被加鎖,則 trylock(函式會立即返回一個錯誤。在這種情況下,呼叫執行緒可能會釋放它已經獲取的一些互斥量以便進行退避,從而讓其他執行緒繼續執行。在上面的交叉加鎖示例中,我們可以重新設計其中一個執行緒,例如 T1,利用條件加鎖和退避來預防死鎖。

(3)條件變數

1.靜態方法:

pthread_cond_t con = PTHREAD_COND_INITIALIZER;

定義一個條件變數con,並使用預設屬性對其進行初始化。

2.動態方法:

使用pthread cond init()函式,可通過 attr引數設定條件變數。為簡便起見,我們總是使用NULL attr引數作為預設屬性。

在互斥量的臨界區中,執行緒可通過以下函式使用條件變數來相互協作。

pthread_cond_waitconditlonmutex該函式會阻塞呼叫執行緒,直到發出指定條件的訊號。當互斥量被加鎖時、應呼叫該例程。它會線上程等待時自動釋放互斥量。互斥量將在接收到訊號並喚醒阻塞的執行緒後自動鎖定。

pthread cond signalcondition;該函式用來發出訊號,即喚醒正在等待條件變數的執行緒或解除阻塞。它應在互斥量被加鎖後呼叫,而且必須解鎖互斥量才能完成pthread_cond_wait ()

pthread cond broadcastcondition該函式會解除被阻塞在條件變數上的所有執行緒阻塞。所有未阻塞的執行緒將爭用同一個互斥量來訪問條件變數。它們的執行順序取決於執行緒排程。

(4)訊號量

訊號量和條件變數之間的主要區別是,前者包含一個計數器,可操作計數器,測試計數器值以做出決策等,所有這些都是臨界區的原子操作或基本操作,而後者需要一個特定的互斥量來執行臨界區。在 Pthreads 中,互斥量嚴格用於封鎖。而條件變數可用於執行緒協作。相反,可以把使用初始值1計算訊號量當作鎖。帶有其他初始值的訊號量可用於協作。因此,訊號量比條件變數更通用、更靈活。下面的示例說明了訊號量相對於條件變數的優勢。

下面顯示了使用訊號量的生產者-消費者問題的虛擬碼。

(5)屏障

執行緒連線操作允許某執行緒(通常是主執行緒)等待其他執行緒終止。在等待的所有執行緒都終止後,主執行緒可建立新執行緒來繼續執行並行程式的其餘部分。建立新執行緒需要系統開銷。在某些情況下,保持執行緒活動會更好,但應要求它們在所有執行緒都達到指定同步點之前不能繼續活動。

(6)Linux中的執行緒

與許多其他作業系統不同,Linux不區分程序和執行緒。對於Linux 核心,執行緒只是一個與其他程序共享某些資源的程序。在Linux 中,程序和執行緒都是由clone()系統呼叫建立的,具有以下原型

int clone(int(*fn)(void *),void *child_stack,int flags,void *arg)

三、最有收穫的內容

生產者—消費者問題

我們將使用執行緒和條件變數來實現一個簡化版的生產者-消費者問題,也稱有限緩衝問題。生產者-消費者問題通常將程序定義為執行實體,可看作當前上下文中的執行緒。下面是該問題的定義。

一系列生產者和消費者程序共享數量有限的緩衝區。每個緩衝區每次有一個特定的專案。最開始,所有緩衝區都是空的。當一個生產者將一個專案放人一個空緩衝區時,該緩衝區就會變滿。當一個消費者從一個滿的緩衝區中獲取一個專案時,該緩衝區就會變空。如果沒有空緩衝區,生產者必須等待。同樣,如果沒有滿緩衝區,則消費者必須等待。此外,當等待事件發生時、必須允許等待程序繼續。

在示例程式中,假設每個緩衝區都有一個整數值。共享全域性變數定義為;

// shared global variables int buf[NBUF];

int head, tail;// number of full buffers

int data;

緩衝區用作一系列迴圈緩衝區。索引變數 head用於將一個專案放人空緩衝區,tail 則用於從滿緩衝區中取出一個專案。變數資料就是滿緩衝區的數量。為支援生產者和消費者之間的協作,我們定義了一個互斥量和兩個條件變數。

pthread_mutex_t mutex;

pthread_cond_t empty,full;// condition variables

其中,empty 表示所有空緩衝區的條件,full 表示所有滿緩衝區的條件。當生產者發現沒有空緩衝區時,它會等待empty 條件變數,當消費者使用了一個滿緩衝區時,就會發出訊號。同樣,當消費者發現沒有滿緩衝區時,它會等待 ful1條件變數。 當生產者將一個項放人空緩衝區時,就會發出訊號。

該程式從主執行緒開始,將緩衝區控制變數和條件變數初始化。初始化完成後,它會建立一個生產者執行緒和一個消費者執行緒,並等待執行緒結合。緩衝區大小設定為NBUF=5.如果生產者試圖將 N=10個項放入緩衝區,當所有緩衝區已滿時,它就要等待。同樣.如果消費者試圖從緩衝區獲取N=10個項,將會導致它在所有緩衝區都空時等待。在這兩種情況下,當等待條件得到滿足時,另一個執行緒會通知正在等待的執行緒。因此,這兩個執行緒通過條件變數相互協作。下面是示例程式 C4.4 的程式程式碼,實現了一個簡版的生產者-消費者問題,即只有一個生產者和一個消費者。

問題與解決思路

問題:Linux裡程序與執行緒的區別?

解決思路:

執行緒是指程序內的一個執行單元,也是程序內的可排程實體.

與程序的區別:

(1)地址空間:程序內的一個執行單元;程序至少有一個執行緒;它們共享程序的地址空間;而程序有自己獨立的地址空間;

(2)資源擁有:程序是資源分配和擁有的單位,同一個程序內的執行緒共享程序的資源;

(3)執行緒是處理器排程的基本單位,但程序不是;

(4)二者均可併發執行:

程序和執行緒都是由作業系統所體會的程式執行的基本單元,系統利用該基本單元實現系統對應用的併發性。程序是系統進行資源分配和排程的一個獨立單位。執行緒是程序的一個實體,是CPU排程和分派的基本單位,執行緒自己基本上不擁有系統資源,只擁有一點在執行中必不可少的資源(如程式計數器,一組暫存器和棧),但是它可與同屬一個程序的其他的執行緒共享程序所擁有的全部資源。一個執行緒可以建立和撤銷另一個執行緒,同一個程序中的多個執行緒之間可以併發執行。

五、實踐內容(截圖、程式碼連結)本次實踐是基於OpenEuler系統下實現的

編寫一個C語言程式使用訊號量實現生產者—消費者問題:

1.目的

1實現生產者消費者問題的模擬,以便更好的理解此經典程序同步問題。生產者-消費者問題是典型的PV操作問題,假設系統中有一個比較大的緩衝池,生產者的任務是隻要緩衝池未滿就可以將生產出的產品放入其中,而消費者的任務是隻要緩衝池未空就可以從緩衝池中拿走產品。緩衝池被佔用時,任何程序都不能訪問。

2每一個生產者都要把自己生產的產品放入緩衝池,每個消費者從緩衝池中取走產品消費。在這種情況下,生產者消費者程序同步,因為只有通過互通訊息才知道是否能存入產品或者取走產品。他們之間也存在互斥,即生產者消費者必須互斥訪問緩衝池,即不能有兩個以上的程序同時進行。

2.原理

在同一個程序地址空間內執行兩個執行緒。生產者執行緒生產物品,然後將物品放置在一個空緩衝區中供消費者執行緒消費。消費者執行緒從緩衝區中獲得物品,然後釋放緩衝區。當生產者執行緒生產物品時,如果沒有空緩衝區可用,那麼生產者執行緒必須等待消費者執行緒釋放一個空緩衝區。當消費者執行緒消費物品時,如果沒有滿的緩衝區,那麼消費者執行緒將被阻擋,直到新的物品被生產出來。

3.流程圖(左面是生產者流程圖,右面是消費者流程圖)

C語言程式碼連結:

https://gitee.com/two_thousand_and_thirteen/codes/y172whr9os5ma4fzpbix066

編譯執行結果截圖: