Linux 的多線程編程的高效開發經驗

來源:互聯網
上載者:User

本文轉自:http://www.ibm.com/developerworks/cn/linux/l-cn-mthreadps/index.html

建議1:linux預設不支援遞迴鎖

建議2:條件變數觸發時當沒有遇到等待的線程,則馬上又變為非觸發的。所以最好加一個等待線程個數的變數,進行判斷

建議3:條件返回時的互斥鎖的解鎖問題

建議3:pthread_cond_timedwait等待的時間是絕對時間

建議4:回收線程資源:pthread_join 或 pthread_detach

 

Linux 平台上的多線程程式開發相對應其他平台(比如 Windows)的多線程 API 有一些細微和隱晦的差別。不注意這些 Linux 上的一些開發陷阱,常常會導致程式問題不窮,死結不斷。本文中我們從 5 個方面總結出 Linux 多線程編程上的問題,並分別引出相關改善的開發經驗,用以避免這些的陷阱。我們希望這些經驗可以協助讀者們能更好更快的熟悉 Linux 平台的多線程編程。

我們假設讀者都已經很熟悉 Linux 平台上基本的線程編程的 Pthread 庫 API 。其他的第三方用以線程編程的庫,如 boost,將不會在本文中提及。本文中主要涉及的題材包括線程開發中的線程管理,互斥變數,條件變數等。進程概念將不會在本文中涉及。

 

多線程開發在 Linux 平台上已經有成熟的 Pthread 庫支援。其涉及的多線程開發的最基本概念主要包含三點:線程,互斥鎖,條件。其中,線程操作又分線程的建立,退出,等待 3 種。互斥鎖則包括 4 種操作,分別是建立,銷毀,加鎖和解鎖。條件操作有 5 種操作:建立,銷毀,觸發,廣播和等待。其他的一些線程擴充概念,如號誌等,都可以通過上面的三個基本元素的基本操作封裝出來。

線程,互斥鎖,條件在 Linux 平台上對應的 API 可以用表 1 歸納。為了方便熟悉 Windows 線程編程的讀者熟悉 Linux 多線程開發的 API,我們在表中同時也列出 Windows SDK 庫中所對應的 API 名稱。

 

對象 操作 Linux Pthread API Windows SDK 庫對應 API
線程 建立 pthread_create CreateThread
退出 pthread_exit ThreadExit
等待 pthread_join WaitForSingleObject
互斥鎖 建立 pthread_mutex_init CreateMutex
銷毀 pthread_mutex_destroy CloseHandle
加鎖 pthread_mutex_lock WaitForSingleObject
解鎖 pthread_mutex_unlock ReleaseMutex
條件 建立 pthread_cond_init CreateEvent
銷毀 pthread_cond_destroy CloseHandle
觸發 pthread_cond_signal SetEvent
廣播 pthread_cond_broadcast SetEvent / ResetEvent
等待 pthread_cond_wait / pthread_cond_timedwait SingleObjectAndWait

 

多線程開發在 Linux 平台上已經有成熟的 Pthread 庫支援。其涉及的多線程開發的最基本概念主要包含三點:線程,互斥鎖,條件。其中,線程操作又分線程的建立,退出,等待 3 種。互斥鎖則包括 4 種操作,分別是建立,銷毀,加鎖和解鎖。條件操作有 5 種操作:建立,銷毀,觸發,廣播和等待。其他的一些線程擴充概念,如號誌等,都可以通過上面的三個基本元素的基本操作封裝出來。

 

 

互斥鎖是多線程編程中基本的概念,在開發中被廣泛使用。其調用次序層次清晰簡單:建鎖,加鎖,解鎖,銷毀鎖。但是需要注意的是,與諸如 Windows 平台的互斥變數不同,在預設情況下,Linux 下的同一線程無法對同一互斥鎖進行遞迴加速,否則將發生死結。

所謂遞迴加鎖,就是在同一線程中試圖對互斥鎖進行兩次或兩次以上的行為。其情境在 Linux 平台上的代碼可由清單 1 所示。

 

// 通過預設條件建鎖    pthread_mutex_t *theMutex = new pthread_mutex_t;     pthread_mutexattr_t attr;     pthread_mutexattr_init(&attr);     pthread_mutex_init(theMutex,&attr);     pthread_mutexattr_destroy(&attr);     // 遞迴加鎖    pthread_mutex_lock (theMutex);     pthread_mutex_lock (theMutex);     pthread_mutex_unlock (theMutex);     pthread_mutex_unlock (theMutex);

 

在以上代碼情境中,問題將出現在第二次加鎖操作。由於在預設情況下,Linux 不允許同一線程遞迴加鎖,因此在第二次加鎖操作時線程將出現死結。

Linux 互斥變數這種奇怪的行為或許對於特定的某些情境會所有用處,但是對於大多數情況下看起來更像是程式的一個 bug 。畢竟,在同一線程中對同一互斥鎖進行遞迴加鎖在尤其是二次開發中經常會需要。

這個問題與互斥鎖的中的預設 recursive 屬性有關。解決問題的方法就是顯式地在互斥變數初始化時將設定起 recursive 屬性。基於此,以上代碼其實稍作修改就可以很好的運行,只需要在初始化鎖的時候加設定一個屬性。請看清單 2 。

 

pthread_mutexattr_init(&attr);     // 設定 recursive 屬性    pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_RECURSIVE_NP);     pthread_mutex_init(theMutex,&attr);

 

因此,建議盡量設定 recursive 屬性以初始化 Linux 的互斥鎖,這樣既可以解決同一線程遞迴加鎖的問題,又可以避免很多情況下死結的發生。這樣做還有一個額外的好處,就是可以讓 Windows 和 Linux 下讓鎖的表現統一。

 

條件變數的置位和複位有兩種常用模型:第一種模型是當條件變數置位(signaled)以後,如果當前沒有線程在等待,其狀態會保持為置位(signaled),直到有等待的線程進入被觸發,其狀態才會變為複位(unsignaled),這種模型的採用以 Windows 平台上的 Auto-set Event 為代表。其狀態變化 1 所示:

第二種模型則是 Linux 平台的 Pthread 所採用的模型,當條件變數置位(signaled)以後,即使當前沒有任何線程在等待,其狀態也會恢複為複位(unsignaled)狀態。其狀態變化 2 所示:

具體來說,Linux 平台上 Pthread 下的條件變數狀態變化模型是這樣工作的:調用 pthread_cond_signal() 釋放被條件阻塞的線程時,無論存不存在被阻塞的線程,條件都將被重新複位,下一個被條件阻塞的線程將不受影響。而對於 Windows,當調用 SetEvent 觸發 Auto-reset 的 Event 條件時,如果沒有被條件阻塞的線程,那麼條件將維持在觸發狀態,直到有新的線程被條件阻塞並被釋放為止。

這種差異性對於那些熟悉 Windows 平台上的條件變數狀態模型而要開發 Linux 平台上多線程的程式員來說可能會造成意想不到的尷尬結果。試想要實現一個旅客坐出租車的程式:旅客在路邊等出租車,調用條件等待。出租車來了,將觸發條件,旅客停止等待並上車。一個出租車只能搭載一波乘客,於是我們使用單一觸發的條件變數。這個實現邏輯在第一個模型下即使出租車先到,也不會有什麼問題,其過程 3 所示:

 

然而如果按照這個思路來在 Linux 上來實現,代碼看起來可能是清單 3 這樣。

…… // 提示出租車到達的條件變數 pthread_cond_t taxiCond;  // 同步鎖 pthread_mutex_t taxiMutex;  // 旅客到達等待出租車 void * traveler_arrive(void * name) {     cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl;     pthread_mutex_lock(&taxiMutex);     pthread_cond_wait (&taxiCond, &taxtMutex);     pthread_mutex_unlock (&taxtMutex);     cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl;     pthread_exit( (void *)0 );  }  // 出租車到達 void * taxi_arrive(void *name) {     cout<< ” Taxi ” <<(char *)name<< ” arrives. ” <<endl;     pthread_cond_signal(&taxtCond);     pthread_exit( (void *)0 );  }  void main() {      // 初始化    taxtCond= PTHREAD_COND_INITIALIZER;     taxtMutex= PTHREAD_MUTEX_INITIALIZER;     pthread_t thread;     pthread_attr_t threadAttr;     pthread_attr_init(&threadAttr);     pthread_create(&thread, & threadAttr, taxt_arrive, (void *)( ” Jack ” ));     sleep(1);     pthread_create(&thread, &threadAttr, traveler_arrive, (void *)( ” Susan ” ));     sleep(1);     pthread_create(&thread, &threadAttr, taxi_arrive, (void *)( ” Mike ” ));     sleep(1);     return 0;  }

好的,運行一下,看看結果如清單 4 。

Taxi Jack arrives.     Traveler Susan needs a taxi now!     Taxi Mike arrives.     Traveler Susan now got a taxi.

其過程 4 所示:

通過對比結果,你會發現同樣的邏輯,在 Linux 平台上啟動並執行結果卻完全是兩樣。對於在 Windows 平台上的模型一, Jack 開著出租車到了月台,觸發條件變數。如果沒顧客,條件變數將維持觸發狀態,也就是說 Jack 停下車在那裡等著。直到 Susan 小姐來了月台,執行等待條件來找出租車。 Susan 搭上 Jack 的出租車離開,同時條件變數被自動複位。

但是到了 Linux 平台,問題就來了,Jack 到了月台一看沒人,觸發的條件變數被直接複位,於是 Jack 排在等待隊列裡面。來遲一秒的 Susan 小姐到了月台卻看不到在那裡等待的 Jack,只能等待,直到 Mike 開車趕到,重新觸發條件變數,Susan 才上了 Mike 的車。這對於在排隊系統前面的 Jack 是不公平的,而問題癥結是在於 Linux 平台上條件變數觸發的自動複位引起的一個 Bug 。

條件變數在 Linux 平台上的這種模型很難說好壞。但是在實際開發中,我們可以對代碼稍加改進就可以避免這種差異的發生。由於這種差異只發生在觸發沒有被線程等待在條件變數的時刻,因此我們只需要掌握好觸發的時機即可。最簡單的做法是增加一個計數器記錄等待線程的個數,在決定觸發條件變數前檢查下該變數即可。改進後 Linux 函數如清單 5 所示。

…… // 提示出租車到達的條件變數 pthread_cond_t taxiCond;  // 同步鎖 pthread_mutex_t taxiMutex;  // 旅客人數,初始為 0  int travelerCount=0;  // 旅客到達等待出租車 void * traveler_arrive(void * name) {     cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl;     pthread_mutex_lock(&taxiMutex);     // 提示旅客人數增加    travelerCount++;     pthread_cond_wait (&taxiCond, &taxiMutex);     pthread_mutex_unlock (&taxiMutex);     cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl;     pthread_exit( (void *)0 );  }  // 出租車到達 void * taxi_arrive(void *name)  {     cout<< ” Taxi ” <<(char *)name<< ” arrives. ” <<endl;  while(true)  {         pthread_mutex_lock(&taxiMutex);         // 當發現已經有旅客在等待時,才觸發條件變數        if(travelerCount>0)         {             pthread_cond_signal(&taxtCond);             pthread_mutex_unlock (&taxiMutex);             break;         }         pthread_mutex_unlock (&taxiMutex);     }     pthread_exit( (void *)0 );  }

因此我們建議在 Linux 平台上要出發條件變數之前要檢查是否有等待的線程,只有當有線程在等待時才對條件變數進行觸發。

 

在 Linux 調用 pthread_cond_wait 進行條件變數等待操作時,我們增加一個互斥變數參數是必要的,這是為了避免線程間的競爭和饑餓情況。但是當條件等待返回時候,需要注意的是一定不要遺漏對互斥變數進行解鎖。

Linux 平台上的 pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) 函數返回時,互斥鎖 mutex 將處於鎖定狀態。因此之後如果需要對臨界區資料進行重新訪問,則沒有必要對 mutex 就行重新加鎖。但是,隨之而來的問題是,每次條件等待以後需要加入一步手動的解鎖操作。正如前文中乘客等待出租車的 Linux 代碼如清單 6 所示:

void * traveler_arrive(void * name) {     cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl;     pthread_mutex_lock(&taxiMutex);     pthread_cond_wait (&taxiCond, &taxtMutex);     pthread_mutex_unlock (&taxtMutex);     cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl;     pthread_exit( (void *)0 );  }

這一點對於熟悉 Windows 平台多線程開發的開發人員來說尤為重要。 Windows 上的 SignalObjectAndWait() 函數是常與 Linux 平台上的 pthread_cond_wait() 函數被看作是跨平台編程時的一對等價函數。但是需要注意的是,兩個函數退出時的狀態是不一樣的。在 Windows 平台上,SignalObjectAndWait(HANDLE a, HANDLE b, …… ) 方法在調用結束返回時的狀態是 a 和 b 都是置位(signaled)狀態,在普遍的使用方法中,a 經常是一個 Mutex 變數,在這種情況下,當返回時,Mutex a 處於解鎖狀態(signaled),Event b 處於置位狀態(signaled), 因此,對於 Mutex a 而言,我們不需要考慮解鎖的問題。而且,在 SignalObjectAndWait() 之後,如果需要對臨界區資料進行重新訪問,都需要調用 WaitForSingleObject() 重新加鎖。這一點剛好與 Linux 下的 pthread_cond_wait() 完全相反。

Linux 對於 Windows 的這一點額外解鎖的操作區別很重要,一定得牢記。否則從 Windows 移植到 Linux 上的條件等待操作一旦忘了結束後的解鎖操作,程式將肯定會發生死結。

 

逾時是多線程編程中一個常見的概念。例如,當你在 Linux 平台下使用 pthread_cond_timedwait() 時就需要指定逾時這個參數,以便這個 API 的調用者最多隻被阻塞指定的時間間隔。但是如果你是第一次使用這個 API 時,首先你需要瞭解的就是這個 API 當中逾時參數的特殊性(就如本區段標頭所提示的那樣)。我們首先來看一下這個 API 的定義。 pthread_cond_timedwait() 定義請看清單 7 。

int pthread_cond_timedwait(pthread_cond_t *restrict cond,               pthread_mutex_t *restrict mutex,               const struct timespec *restrict abstime);

參數 abstime 在這裡用來表示和逾時時間相關的一個參數,但是需要注意的是它所表示的是一個絕對時間,而不是一個時間間隔數值,只有當系統的目前時間達到或者超過 abstime 所表示的時間時,才會觸發逾時事件。這對於擁有 Windows 平台線程開發經驗的人來說可能尤為困惑。因為 Windows 平台下所有的 API 等待參數(如 SignalObjectAndWait,等)都是相對時間,

假設我們指定相對的逾時時間參數如 dwMilliseconds (單位毫秒)來調用和逾時相關的函數,這樣就需要將 dwMilliseconds 轉化為 Linux 下的絕對時間參數 abstime 使用。常用的轉換方法如清單 8 所示:

/* get the current time */     struct timeval now;     gettimeofday(&now, NULL);     /* add the offset to get timeout value */     abstime ->tv_nsec = now.tv_usec * 1000 + (dwMilliseconds % 1000) * 1000000;     abstime ->tv_sec = now.tv_sec + dwMilliseconds / 1000;

Linux 的絕對時間看似簡單明了,卻是開發中一個非常隱晦的陷阱。而且一旦你忘了時間轉換,可以想象,等待你的錯誤將是多麼的令人頭疼:如果忘了把相對時間轉換成絕對時間,相當於你告訴系統你所等待的逾時時間是過去式的 1970 年 1 月 1 號某個時間段,於是作業系統毫不猶豫馬上送給你一個 timeout 的傳回值,然後你會舉著拳頭抱怨為什麼另外一個同步線程耗時居然如此之久,並一頭紮進尋找耗時原因的深淵裡。

 

在 Linux 平台下,當處理線程結束時需要注意的一個問題就是如何讓一個線程善始善終,讓其所佔資源得到正確釋放。在 Linux 平台預設情況下,雖然各個線程之間是相互獨立的,一個線程的終止不會去通知或影響其他的線程。但是已經終止的線程的資源並不會隨著線程的終止而得到釋放,我們需要調用 pthread_join() 來獲得另一個線程的終止狀態並且釋放該線程所佔的資源。 Pthread_join() 函數的定義如清單 9 。

int pthread_join(pthread_t th, void **thread_return);

調用該函數的線程將掛起,等待 th 所表示的線程的結束。 thread_return 是指向線程 th 傳回值的指標。需要注意的是 th 所表示的線程必須是 joinable 的,即處於非 detached(游離)狀態;並且只可以有唯一的一個線程對 th 調用 pthread_join() 。如果 th 處於 detached 狀態,那麼對 th 的 pthread_join() 調用將返回錯誤。

如果你壓根兒不關心一個線程的結束狀態,那麼也可以將一個線程設定為 detached 狀態,從而來讓作業系統在該線程結束時來回收它所佔的資源。將一個線程設定為 detached 狀態可以通過兩種方式來實現。一種是調用 pthread_detach() 函數,可以將線程 th 設定為 detached 狀態。其申明如清單 10 。

int pthread_detach(pthread_t th);

另一種方法是在建立線程時就將它設定為 detached 狀態,首先初始化一個線程屬性變數,然後將其設定為 detached 狀態,最後將它作為參數傳入線程建立函數 pthread_create(),這樣所建立出來的線程就直接處於 detached 狀態。方法如清單 11 。

………………………………… ..     pthread_t       tid;     pthread_attr_t  attr;     pthread_attr_init(&attr);     pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);     pthread_create(&tid, &attr, THREAD_FUNCTION, arg);

總之為了在使用 Pthread 時避免線程的資源線上程結束時不能得到正確釋放,從而避免產生潛在的記憶體流失問題,在對待線程結束時,要確保該線程處於 detached 狀態,否著就需要調用 pthread_join() 函數來對其進行資源回收。

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.