1. 程式人生 > >執行緒(一):理論知識

執行緒(一):理論知識

目錄

一、作業系統執行緒理論

1)執行緒概念的引入背景

程序

有了程序為什麼要有執行緒

執行緒的出現

2)程序和執行緒的關係

3)執行緒的特點

4)使用執行緒的實際場景

5)記憶體中的執行緒

6)使用者級執行緒和核心級執行緒(瞭解)

使用者級執行緒

核心級執行緒

使用者級與核心級執行緒的對比

混合實現

二、執行緒和python

1)全域性直譯器鎖GIL

2)python執行緒模組的選擇


一、作業系統執行緒理論

1)執行緒概念的引入背景

程序

   之前我們已經瞭解了作業系統中程序的概念,程式並不能單獨執行,只有將程式裝載到記憶體中,系統為它分配資源才能執行,而這種執行的程式就稱之為程序。程式和程序的區別就在於:程式是指令的集合,它是程序執行的靜態描述文字;程序是程式的一次執行活動,屬於動態概念。在多道程式設計中,我們允許多個程式同時載入到記憶體中,在作業系統的排程下,可以實現併發地執行。這是這樣的設計,大大提高了CPU的利用率。程序的出現讓每個使用者感覺到自己獨享CPU,因此,程序就是為了在CPU上實現多道程式設計而提出的。

有了程序為什麼要有執行緒

  程序有很多優點,它提供了多道程式設計,讓我們感覺我們每個人都擁有自己的CPU和其他資源,可以提高計算機的利用率。很多人就不理解了,既然程序這麼優秀,為什麼還要執行緒呢?其實,仔細觀察就會發現程序還是有很多缺陷的,主要體現在兩點上:

  • 程序只能在一個時間幹一件事,如果想同時幹兩件事或多件事,程序就無能為力了。

  • 程序在執行的過程中如果阻塞,例如等待輸入,整個程序就會掛起,即使程序中有些工作不依賴於輸入的資料,也將無法執行。

  如果這兩個缺點理解比較困難的話,舉個現實的例子也許你就清楚了:如果把我們上課的過程看成一個程序的話,那麼我們要做的是耳朵聽老師講課,手上還要記筆記,腦子還要思考問題,這樣才能高效的完成聽課的任務。而如果只提供程序這個機制的話,上面這三件事將不能同時執行,同一時間只能做一件事,聽的時候就不能記筆記,也不能用腦子思考,這是其一;如果老師在黑板上寫演算過程,我們開始記筆記,而老師突然有一步推不下去了,阻塞住了,他在那邊思考著,而我們呢,也不能幹其他事,即使你想趁此時思考一下剛才沒聽懂的一個問題都不行,這是其二。

  現在你應該明白了程序的缺陷了,而解決的辦法很簡單,我們完全可以讓聽、寫、思三個獨立的過程,並行起來,這樣很明顯可以提高聽課的效率。而實際的作業系統中,也同樣引入了這種類似的機制——執行緒。

執行緒的出現

  60年代,在OS中能擁有資源和獨立執行的基本單位是程序,然而隨著計算機技術的發展,程序出現了很多弊端,一是由於程序是資源擁有者,建立、撤消與切換存在較大的時空開銷,因此需要引入輕型程序;二是由於對稱多處理機(SMP)出現,可以滿足多個執行單位,而多個程序並行開銷過大。

  因此在80年代,出現了能獨立執行的基本單位——執行緒(Threads)

  注意:程序是資源分配的最小單位,執行緒是CPU排程的最小單位.

     每一個程序中至少有一個執行緒。 

2)程序和執行緒的關係

  執行緒與程序的區別可以歸納為以下4點:

  1)地址空間和其它資源(如開啟檔案):程序間相互獨立,同一程序的各執行緒間共享。某程序內的執行緒在其它程序不可見。

  2)通訊:程序間通訊IPC,執行緒間可以直接讀寫程序資料段(如全域性變數)來進行通訊——需要程序同步和互斥手段的輔助,以保證資料的一致性。

  3)排程和切換:執行緒上下文切換比程序上下文切換要快得多。

  4)在多執行緒作業系統中,程序不是一個可執行的實體。

3)執行緒的特點

  在多執行緒的作業系統中,通常是在一個程序中包括多個執行緒,每個執行緒都是作為利用CPU的基本單位,是花費最小開銷的實體。執行緒具有以下屬性。

  1)輕型實體

  執行緒中的實體基本上不擁有系統資源,只是有一點必不可少的、能保證獨立執行的資源。

  執行緒的實體包括程式、資料和TCB。執行緒是動態概念,它的動態特性由執行緒控制塊TCB(Thread Control Block)描述。

TCB包括以下資訊:
(1)執行緒狀態。
(2)當執行緒不執行時,被儲存的現場資源。
(3)一組執行堆疊。
(4)存放每個執行緒的區域性變數主存區。
(5)訪問同一個程序中的主存和其它資源。
用於指示被執行指令序列的程式計數器、保留區域性變數、少數狀態引數和返回地址等的一組暫存器和堆疊。

  2)獨立排程和分派的基本單位。

  在多執行緒OS中,執行緒是能獨立執行的基本單位,因而也是獨立排程和分派的基本單位。由於執行緒很“輕”,故執行緒的切換非常迅速且開銷小(在同一程序中的)。

  3)共享程序資源。

  執行緒在同一程序中的各個執行緒,都可以共享該程序所擁有的資源,這首先表現在:所有執行緒都具有相同的程序id,這意味著,執行緒可以訪問該程序的每一個記憶體資源;此外,還可以訪問程序所擁有的已開啟檔案、定時器、訊號量機構等。由於同一個程序內的執行緒共享記憶體和檔案,所以執行緒之間互相通訊不必呼叫核心。

  4)可併發執行。

  在一個程序中的多個執行緒之間,可以併發執行,甚至允許在一個程序中所有執行緒都能併發執行;同樣,不同程序中的執行緒也能併發執行,充分利用和發揮了處理機與外圍裝置並行工作的能力。

4)使用執行緒的實際場景

 

  開啟一個字處理軟體程序,該程序肯定需要辦不止一件事情,比如監聽鍵盤輸入,處理文字,定時自動將文字儲存到硬碟,這三個任務操作的都是同一塊資料,因而不能用多程序。只能在一個程序裡併發地開啟三個執行緒,如果是單執行緒,那就只能是,鍵盤輸入時,不能處理文字和自動儲存,自動儲存時又不能輸入和處理文字。

5)記憶體中的執行緒

 

  多個執行緒共享同一個程序的地址空間中的資源,是對一臺計算機上多個程序的模擬,有時也稱執行緒為輕量級的程序。

  而對一臺計算機上多個程序,則共享實體記憶體、磁碟、印表機等其他物理資源。多執行緒的執行也多程序的執行類似,是cpu在多個執行緒之間的快速切換。

  不同的程序之間是充滿敵意的,彼此是搶佔、競爭cpu的關係,如果迅雷會和QQ搶資源。而同一個程序是由一個程式設計師的程式建立,所以同一程序內的執行緒是合作關係,一個執行緒可以訪問另外一個執行緒的記憶體地址,大家都是共享的,一個執行緒乾死了另外一個執行緒的記憶體,那純屬程式設計師腦子有問題。

  類似於程序,每個執行緒也有自己的堆疊,不同於程序,執行緒庫無法利用時鐘中斷強制執行緒讓出CPU,可以呼叫thread_yield執行執行緒自動放棄cpu,讓另外一個執行緒執行。

  執行緒通常是有益的,但是帶來了不小程式設計難度,執行緒的問題是:

  1. 父程序有多個執行緒,那麼開啟的子執行緒是否需要同樣多的執行緒

  2. 在同一個程序中,如果一個執行緒關閉了檔案,而另外一個執行緒正準備往該檔案內寫內容呢?

  因此,在多執行緒的程式碼中,需要更多的心思來設計程式的邏輯、保護程式的資料。

6)使用者級執行緒和核心級執行緒(瞭解)

  執行緒的實現可以分為兩類:使用者級執行緒(User-Level Thread)和核心線執行緒(Kernel-Level Thread),後者又稱為核心支援的執行緒或輕量級程序。在多執行緒作業系統中,各個系統的實現方式並不相同,在有的系統中實現了使用者級執行緒,有的系統中實現了核心級執行緒。 

使用者級執行緒

  核心的切換由使用者態程式自己控制核心切換,不需要核心干涉,少了進出核心態的消耗,但不能很好的利用多核Cpu。

  

  在使用者空間模擬作業系統對程序的排程,來呼叫一個程序中的執行緒,每個程序中都會有一個執行時系統,用來排程執行緒。此時當該程序獲取cpu時,程序內再排程出一個執行緒去執行,同一時刻只有一個執行緒執行。

核心級執行緒

   核心級執行緒:切換由核心控制,當執行緒進行切換的時候,由使用者態轉化為核心態。切換完畢要從核心態返回使用者態;可以很好的利用smp,即利用多核cpu。windows執行緒就是這樣的。

  

使用者級與核心級執行緒的對比

使用者級執行緒和核心級執行緒的區別

核心支援執行緒是OS核心可感知的,而使用者級執行緒是OS核心不可感知的。
使用者級執行緒的建立、撤消和排程不需要OS核心的支援,是在語言(如Java)這一級處理的;而核心支援執行緒的建立、撤消和排程都需OS核心提供支援,而且與程序的建立、撤消和排程大體是相同的。
使用者級執行緒執行系統呼叫指令時將導致其所屬程序被中斷,而核心支援執行緒執行系統呼叫指令時,只導致該執行緒被中斷。
在只有使用者級執行緒的系統內,CPU排程還是以程序為單位,處於執行狀態的程序中的多個執行緒,由使用者程式控制執行緒的輪換執行;在有核心支援執行緒的系統內,CPU排程則以執行緒為單位,由OS的執行緒排程程式負責執行緒的排程。
使用者級執行緒的程式實體是執行在使用者態下的程式,而核心支援執行緒的程式實體則是可以執行在任何狀態下的程式。

核心執行緒的優缺點

優點:當有多個處理機時,一個程序的多個執行緒可以同時執行。
缺點:由核心進行排程。

使用者級執行緒的優缺點

優點:
執行緒的排程不需要核心直接參與,控制簡單。
可以在不支援執行緒的作業系統中實現。
建立和銷燬執行緒、執行緒切換代價等執行緒管理的代價比核心執行緒少得多。
允許每個程序定製自己的排程演算法,執行緒管理比較靈活。
執行緒能夠利用的表空間和堆疊空間比核心級執行緒多。
同一程序中只能同時有一個執行緒在執行,如果有一個執行緒使用了系統呼叫而阻塞,那麼整個程序都會被掛起。另外,頁面失效也會產生同樣的問題。
缺點:
資源排程按照程序進行,多個處理機下,同一個程序中的執行緒只能在同一個處理機下分時複用

混合實現

  使用者級與核心級的多路複用,核心同一排程核心執行緒,每個核心執行緒對應n個使用者執行緒

  

linux作業系統的 NPTL  

歷史
在核心2.6以前的排程實體都是程序,核心並沒有真正支援執行緒。它是能過一個系統呼叫clone()來實現的,這個呼叫建立了一份呼叫程序的拷貝,跟fork()不同的是,這份程序拷貝完全共享了呼叫程序的地址空間。LinuxThread就是通過這個系統呼叫來提供執行緒在核心級的支援的(許多以前的執行緒實現都完全是在使用者態,核心根本不知道執行緒的存在)。非常不幸的是,這種方法有相當多的地方沒有遵循POSIX標準,特別是在訊號處理,排程,程序間通訊原語等方面。

很顯然,為了改進LinuxThread必須得到核心的支援,並且需要重寫執行緒庫。為了實現這個需求,開始有兩個相互競爭的專案:IBM啟動的NGTP(Next Generation POSIX Threads)專案,以及Redhat公司的NPTL。在2003年的年中,IBM放棄了NGTP,也就是大約那時,Redhat釋出了最初的NPTL。

NPTL最開始在redhat linux 9裡釋出,現在從RHEL3起核心2.6起都支援NPTL,並且完全成了GNU C庫的一部分。

設計
NPTL使用了跟LinuxThread相同的辦法,在核心裡面執行緒仍然被當作是一個程序,並且仍然使用了clone()系統呼叫(在NPTL庫裡呼叫)。但是,NPTL需要核心級的特殊支援來實現,比如需要掛起然後再喚醒執行緒的執行緒同步原語futex.

NPTL也是一個1*1的執行緒庫,就是說,當你使用pthread_create()呼叫建立一個執行緒後,在核心裡就相應建立了一個排程實體,在linux裡就是一個新程序,這個方法最大可能的簡化了執行緒的實現。

除NPTL的1*1模型外還有一個m*n模型,通常這種模型的使用者執行緒數會比核心的排程實體多。在這種實現裡,執行緒庫本身必須去處理可能存在的排程,這樣線上程庫內部的上下文切換通常都會相當的快,因為它避免了系統呼叫轉到核心態。然而這種模型增加了執行緒實現的複雜性,並可能出現諸如優先順序反轉的問題,此外,使用者態的排程如何跟核心態的排程進行協調也是很難讓人滿意。

二、執行緒和python

1)全域性直譯器鎖GIL

  Python程式碼的執行由Python虛擬機器(也叫直譯器主迴圈)來控制。Python在設計之初就考慮到要在主迴圈中,同時只有一個執行緒在執行。雖然 Python 直譯器中可以“執行”多個執行緒,但在任意時刻只有一個執行緒在直譯器中執行。
  對Python虛擬機器的訪問由全域性直譯器鎖(GIL)來控制,正是這個鎖能保證同一時刻只有一個執行緒在執行。

  在多執行緒環境中,Python 虛擬機器按以下方式執行:

  a、設定 GIL;

  b、切換到一個執行緒去執行;

  c、執行指定數量的位元組碼指令或者執行緒主動讓出控制(可以呼叫 time.sleep(0));

  d、把執行緒設定為睡眠狀態;

  e、解鎖 GIL;

  d、再次重複以上所有步驟。
  在呼叫外部程式碼(如 C/C++擴充套件函式)的時候,GIL將會被鎖定,直到這個函式結束為止(由於在這期間沒有Python的位元組碼被執行,所以不會做執行緒切換)編寫擴充套件的程式設計師可以主動解鎖GIL。

2)python執行緒模組的選擇

  Python提供了幾個用於多執行緒程式設計的模組,包括thread、threading和Queue等。thread和threading模組允許程式設計師建立和管理執行緒。thread模組提供了基本的執行緒和鎖的支援,threading提供了更高級別、功能更強的執行緒管理的功能。Queue模組允許使用者建立一個可以用於多個執行緒之間共享資料的佇列資料結構。
  避免使用thread模組,因為更高級別的threading模組更為先進,對執行緒的支援更為完善,而且使用thread模組裡的屬性有可能會與threading出現衝突;其次低級別的thread模組的同步原語很少(實際上只有一個),而threading模組則有很多;再者,thread模組中當主執行緒結束時,所有的執行緒都會被強制結束掉,沒有警告也不會有正常的清除工作,至少threading模組能確保重要的子執行緒退出後進程才退出。 

  thread模組不支援守護執行緒,當主執行緒退出時,所有的子執行緒不論它們是否還在工作,都會被強行退出。而threading模組支援守護執行緒,守護執行緒一般是一個等待客戶請求的伺服器,如果沒有客戶提出請求它就在那等著,如果設定一個執行緒為守護執行緒,就表示這個執行緒是不重要的,在程序退出的時候,不用等待這個執行緒退出。