天天看點

程序控制線程控制

線程控制

12.1 簡介

線程控制主要涉及線程屬性、同步原語屬性、同一線程中多個線程之間如何保持資料的私有性、基于程序的系統調用如何與線程進行互動等内容。

12.2 線程限制

Sysconf函數可以查詢相關的線程限制。其具體内容如下表格:

限制名稱 描述 Name參數
PTHREAD_DESTRUCTOR_ITERATIONS 線程退出時作業系統實作試圖銷毀線程特定資料的最大次數 _SC_THREAD_DESTRUCTOR_ITERATIONS
PTHREAD_KEYS_MAX 程序可以建立的鍵的最大數目 _SC_THREAD_KEYS_MAX
PTHREAD_STACK_MIN 一個線程的棧可用的最小位元組數 _SC_THREAD_STACK_MIN
PTHREAD_THREADS_MAX 程序可以建立的最大線程數 _SC_THREAD_THREADS_MAX

線程的限制的使用是為了增強應用程式在不同的作業系統實作之間的可移植性。

12.3 線程屬性

Pthread接口允許我們通過設定每個對象關聯的不同屬性來細調線程和同步對象的行為。通常,管理這些屬性的函數都遵循相同的模式。

每個對象與它自己類型的屬性進行關聯(線程與線程屬性關聯,互斥量與互斥量屬性關聯等等)。一個屬性對象可以代表多個屬性。屬性對象對應用程式來說是不透明的。這意味着應用程式并不需要了解有關屬性對象内部結構的詳細細節,這樣可以增強應用程式的可移植性。取而代之的是,需要提供相應的函數來管理這些屬性對象。

有一個初始化函數,把屬性設定為預設值。

還有一個銷毀屬性對象的函數。如果初始化函數配置設定了與屬性對象關聯的資源,銷毀函數負責釋放這些資源。

每個屬性都有一個從屬性對象中擷取屬性值的函數。由于函數成功時會傳回0,失敗時會傳回錯誤編号,是以可以通過把屬性值存儲在函數的某一個參數指定的記憶體單元中,把屬性值傳回給調用者。

每個屬性都有一個設定屬性值的函數。在這種情況下,屬性值作為參數按值傳遞。

對程序來說,虛位址空間的大小是固定的。因為程序隻有一個棧,是以他的大小通常不是問題。但對于線程來說,同樣大小的虛位址空間必須被所有的線程棧共享。如果應用程式使用了許多線程,以緻這些線程棧的累計大小超過了可用的虛拟位址空間,就需要減少預設的線程棧大小。另一方面,如果線程調用的函數配置設定了大量的自動變量,或者調用的函數涉及許多很深的棧幀(stack frame),那麼需要的棧大小可能要比預設的大。

如果線程棧的虛位址空間都用完,則可以使用malloc和mmap來為可替代的棧配置設定空間,并調用pthread_attr_setstack函數來改變建立線程的棧位置。

12.4 同步屬性

就像線程具有屬性一樣,線程的同步對象也有屬性。如互斥量屬性、讀寫鎖屬性、條件變量屬性、屏障屬性等。

  • 互斥量屬性

    值得注意的屬性有三個:程序共享屬性、健壯屬性以及類型屬性。

    程序共享屬性是可選的。存在這樣的機制:允許互相獨立的多個程序把同一個記憶體資料映射到它們各自獨立的位址空間中。就像多個程序通路共享資料一樣,多個程序通路共享資料通常也需要同步。

    互斥量健壯屬性與在多個程序間共享的互斥量有關。這意味着,當持有互斥量的程序終止時,需要解決互斥量狀态恢複的問題。這種情況發生時,互斥量處于鎖定狀态,恢複起來很困難。其他阻塞在這個鎖的程序将會一直阻塞下去。

  • 讀寫鎖屬性

    讀寫鎖與互斥量類似,也有屬性。讀寫鎖支援的唯一屬性是程序共享屬性。它與互斥量的程序共享屬性是相同的。就向互斥量的程序共享屬性一樣,有一對函數用于讀取和設定讀寫鎖的程序共享屬性。

  • * 條件變量屬性*

    Single UNIX Specification目前定義了條件變量的兩個屬性:程序共享屬性和時鐘屬性。與其他的屬性對象一樣,有一對函數用于初始化和反初始化條件變量的屬性。

    與其他的同步屬性一樣,條件變量支援程序共享屬性。它控制着條件變量是可以被單程序的多個線程使用,還是可以被多程序的線程使用。

    時鐘屬性,注意是條件變量的逾時參數中所使用的。但注意:Single UNIX Specification并沒有為其他有逾時等待函數的屬性對象定義時鐘屬性。

  • 屏障屬性

    屏障也有屬性。目前定義的屏障屬性隻有程序共享屬性,它控制這屏障可以被多程序的線程使用,還是隻能被初始化屏障的程序内的多線程使用。

12.5 重入

線程在遇到重入問題時與信号處理程式是類似的。在這兩種情況下,多個控制線程在相同的時間有可能調用相同的函數。

如果一個函數在相同的時間點可以被多個線程安全地調用,就稱該函數是線程安全的。

作業系統支援線程安全函數特性。很多函數并不是線程安全的,因為他們傳回的資料存放在靜态的記憶體緩沖區中。通過修改接口,要求調用者自己提供緩沖區可以使函數變為線程安全。

如果一個函數對多個線程來說是可重入的,就說這個函數是線程安全的。但這并不能說明對信号處理程式來說該函數也是可重入的。如果函數對異步信号處理程式的重入是安全的,那麼就可以說函數是異步信号安全的。

由于pthread函數并不保證是異步信号安全的,是以不能把pthread函數用于其他函數,讓該函數稱為異步信号安全的。

12.6 線程特定資料

線程特定資料(thread-specific data),也稱為線程私有資料(thread-private data),是存儲和查詢某個特定線程相關資料的一種機制。把這種資料稱為線程特定資料或線程私有資料的原因是,希望每個線程可以通路它自己單獨的資料副本,而不需要擔心與其他線程的同步通路問題。

線程模型促進了程序中資料和屬性的共享,許多人在設計線程模型時會遇到各種麻煩。那麼為什麼有人想在這樣的模型中促進阻止共享的接口呢?其原因如下:

有時候需要維護基于每個線程(per-thread)的資料。因為線程ID并不能保證是小而連續的整數,是以就不能簡單的配置設定一個每線程資料數組,用線程ID作為數組索引。即使線程ID确實是小而連續的整數,可能還希望有一些額外的保護,防止某個線程的資料與其他線程的資料相混淆。

線程私有資料提供了讓基于程序的接口适應多線程環境的機制。典型舉例是errno。以前的接口(線程出現以前)把errno定義為程序上下文中全局可通路的整數。系統調用和庫例程在調用或執行失敗時設定errno,把它作為操作失敗時的附屬結果。為了讓線程也能夠使用那些原本基于程序的系統調用和庫例程,errno被重新定義為線程私有資料。這樣,一個線程做了重置errno的操作不會影響程序中其他線程的errno值。

一個程序中的所有線程都可以通路這個程序的整個位址空間。除了使用寄存器以外,一個線程沒辦法阻止另一個線程通路它的資料。線程特定資料也不例外。雖然底層的實作部分并不能阻止這種通路能力,但管理線程特定資料的函數可以提高線程通路的資料獨立性,使得線程不太容易通路到其他線程的線程特定資料。

在配置設定線程特定資料之前,需要建立與該資料關聯的鍵。這個鍵将用于擷取對線程特定資料的通路。建立的鍵存儲在記憶體單元中,這個鍵可以被程序中的所有線程使用,但每個線程把這個鍵與不同的線程特定資料位址進行關聯。建立新鍵時,每個線程的資料位址都設為空值。此外,線程還為該鍵關聯一個析構函數,用于釋放記憶體等。線程可以為線程特定資料配置設定多個鍵,每個鍵都可以有一個析構函數與它關聯。每個鍵的析構函數可以互不相同,當然,所有的鍵也可以使用相同的析構函數。每個作業系統可以對程序可配置設定的鍵的數量進行限制。

12.7 取消選項

線程取消選項的調用并不等待線程終止。預設情況下,線程在取消請求發出以後還是繼續運作,直到線程到達某個取消點。取消點是線程檢查它是否被取消的一個位置,如果取消了,則按照請求行事。預設的取消選項是推遲取消。

異步取消與推遲取消不同,因為使用異步取消時,線程可以在任意時間撤銷,不是非得遇到取消點才能被取消。

12.8 線程和信号

即使是在基于程序的程式設計規範中,信号的處理有時候也是很複雜的。把線程引入程式設計規範,就使信号的處理變得更加複雜。

每個線程都有自己的信号屏蔽字,但是信号的處理是程序中所有線程共享的。這意味着單個線程可以阻止某種信号,但當某個線程修改了與某個給定信号相關的處理行為後,所有的線程都必須共享這個處理行為的改變。這樣,如果一個線程選擇忽略某個給定信号,那麼另一個線程就可以通過以下兩種方式撤銷線程的信号選擇:恢複信号的預設處理行為,或者為信号設定一個新的信号處理程式。

程序中的信号是遞送到單個線程的。如果一個信号與硬體故障相關,那麼該信号一般會被發送到引起該事件的線程中去,而其他的信号則被發送到任意一個線程。

為了防止信号中斷線程,可以把信号加到每個線程的信号屏蔽字中。然後可以安排專用線程處理信号。這些專用線程可以進行函數調用,不需要擔心在信号處理程式中調用哪些函數是安全的,因為這些函數調用來自正常的線程上下文,而非會中斷線程正常執行的傳統信号處理函數。

鬧鐘定時器是程序資源,并且所有的線程共享相同的鬧鐘。是以,程序中的多個線程不可能互不幹擾(互不合作)地使用鬧鐘定時器。當建立線程進行信号處理時,建立線程繼承了現有的信号屏蔽字。

12.9 線程和fork

當線程調用fork時,就是為子程序建立了整個程序位址空間的副本。*為寫時複制,子程序與父程序是完全不同的程序,隻要兩者都沒有對記憶體内容作出改動,父程序和子程序之間還可以共享記憶體的副本。*

子程序通過繼承整個位址空間的副本,還從父程序那兒繼承了每個互斥量,讀寫鎖和條件變量的狀态。如果父程序包含一個以上的程序,子程序在fork傳回以後,如果緊接着不是馬上調用exec的話,就需清理鎖的狀态。

在子程序内部,隻有一個線程,它是由父程序中調用fork的線程的副本構成的。如果父程序中的線程占有鎖,子程序将同樣占有這些鎖。問題是子程序并不包含占有鎖的線程的副本,是以子程序沒有辦法知道它占有了哪些鎖、需要釋放哪些鎖。

如果子程序從fork傳回以後馬上調用其中一個exec函數,就可以避免這樣的問題。這種情況下,舊的位址空間就被丢棄,是以鎖的狀态無關緊要。但如果子程序需要繼續做處理工作的話,這種政策就行不通,還需要使用其他政策。

在多線程的程序中,為了避免不一緻狀态的問題,在fork傳回和子程序調用其中一個exec函數之間,子程序隻能調用異步信号安全的函數。這就限制了在調用exec之前子程序能做什麼,但不涉及子程序中鎖狀态的問題。

要清除鎖狀态,可以通過調用pthread_atfork函數建立fork處理程式。

用pthread_atfork函數最多可以安裝3個幫助清理鎖的函數。Prepare fork處理程式由父程序在fork建立子程序前調用。這個fork處理程式的任務是擷取父程序定義的所有鎖。Parent fork處理程式是在fork建立子程序以後、傳回之前在父程序上下文中調用。這個fork處理程式的任務是對prepare fork處理程式擷取的所有鎖進行解鎖。Child fork處理程式在fork傳回之前在子程序上下文中調用。與parent fork處理程式一樣,child fork處理程式也必須釋放prepare fork處理程式擷取的所有鎖。

注意,不會出現加鎖一次解鎖兩次的情況,雖然看起來也許會出現。子程序位址空間在建立時就得到了父程序定義的所有鎖的副本。因為prepare fork處理程式擷取了所有的鎖,父程序中的記憶體和子程序中的記憶體内容在開始的時候是相同的。當父程序和子程序對它們鎖的副本程序解鎖的時候,新的記憶體是配置設定給子程序的,父程序的記憶體内容是複制給子程序的記憶體中(寫時複制),是以就會陷入這樣的假象,看起來父程序對它所有的鎖的副本進行了加鎖,子程序對它所有的鎖的副本進行了加鎖。父程序和子程序對不同記憶體單元的重複的鎖都進行了解鎖操作,就好像出現了下列事件序列:

父程序擷取了所有的鎖

子程序擷取了所有的鎖

父程序釋放了它的鎖

子程序釋放了它的鎖

**使用多個fork處理程式時,處理程式的調用順序并不相同。**Parent和child fork處理程式是以它們注冊時的順序進行調用的,而prepare fork處理程式的調用順序與它們注冊時的順序相反。這樣可以允許多個子產品注冊它們自己的fork處理程式,而且可以保持鎖的層次。

假設子產品A調用子產品B中的函數,而且每個子產品有自己的一套鎖。如果鎖的層次式A在B之前,子產品B必須在子產品A之前設定它的fork處理程式。當父程序調用fork時,就會執行以下步驟,假設子程序在父程序之前運作:

調用子產品A的prepare fork處理程式擷取子產品A的所有鎖

調用子產品B的prepare fork處理程式擷取子產品B的所有鎖

建立子程序

調用子產品B中的child fork處理程式釋放子程序中子產品B的所有鎖

調用子產品A中的child fork處理程式釋放子程序中子產品A的所有鎖

Fork函數傳回到子程序

調用子產品B中的parent fork處理程式釋放父程序中子產品B的所有鎖

調用子產品A中的parent fork處理程式釋放父程序中子產品A的所有鎖

Fork函數傳回到父程序

如果fork處理程式是用來清理鎖狀态的,那麼又由誰負責清理條件變量的狀态呢?在有些作業系統的實作中,條件變量可能并不需要做任何清理。但是有些作業系統實作把鎖作為條件變量實作的一部分,這種情況下的條件變量就需要清理。問題是目前不允許清理鎖狀态的接口。如果鎖是嵌入到條件變量的資料結構中的,那麼在調用fork之後就不能使用條件變量,因為換沒有可移植的方法對鎖進行狀态清理。另外,如果作業系統的實作是使用全局鎖保護程序中的所有的條件變量資料結構,那麼作業系統實作本身可以在fork庫例程中做清理的工作,但是應用程式不應該依賴作業系統實作中類似這樣的細節。

雖然pthread_atfork機制的意圖是使fork之後的鎖狀态保持一緻,但還是存在一些不足之處,隻能在有限情況下可用

沒有很好的辦法對較複雜的同步對象(如條件變量或者屏障)進行狀态的重新初始化

某些錯誤檢查的互斥量實作在child fork處理程式試圖對被父程序加鎖的互斥量進行解鎖時會産生錯誤

遞歸互斥量不能在child fork處理程式中清理,因為沒有辦法确定該互斥量被加鎖的次數

如果子程序隻允許調用異步資訊安全的函數,child fork處理程式就不可能清理同步對象,因為用于操作清理的所有函數都不是異步信号安全的。實際的問題是同步對象在某個線程調用fork時可能處于中間狀态,除非同步對象處于一緻狀态,否則無法被清理。

如果應用程式在信号處理程式中調用了fork(這是合法的,因為fork本身是異步信号安全的),pthread_atfork注冊的fork處理程式隻能調用異步信号安全的函數,否則結果将是未定義的。

12.10 線程和I/O

Pread和pwrite函數,在多線程環境下非常有用,因為程序中所有線程共享相同的檔案描述符。Pread函數可以解決并發線程對同一檔案的讀操作問題,pwrite函數可以解決并發線程對同一檔案進行寫操作的問題。

參考文獻 :Unix環境進階程式設計

繼續閱讀