Unix环境高级编程学习笔记(七) 多线程
 
 
線程概述
線程(thread)技術早在60年代就被提出,但真正應用多線程到操作系統中去,是在80年代中期,solaris是這方面的佼佼者。傳統的Unix也支持線程的概念,但是在一個進程(process)中只允許有一個線程,這樣多線程就意味著多進程。現在,多線程技術已經被許多操作系統所支持,包括Windows/NT,當然,也包括Linux。
為什么有了進程的概念后,還要再引入線程呢?使用多線程到底有哪些好處?什么的系統應該選用多線程?我們首先必須回答這些問題。
使用多線程的理由之一是和進程相比,它是一種非常"節儉"的多任務操作方式。我們知道,在Linux系統下,啟動一個新的進程必須分配給它獨立的地址空間,建立眾多的數據表來維護它的代碼段、堆棧段和數據段,這是一種"昂貴"的多任務工作方式。而運行于一個進程中的多個線程,它們彼此之間使用相同的地址空間,共享大部分數據,啟動一個線程所花費的空間遠遠小于啟動一個進程所花費的空間,而且,線程間彼此切換所需的時間也遠遠小于進程間切換所需要的時間。據統計,總的說來,一個進程的開銷大約是一個線程開銷的30倍左右,當然,在具體的系統上,這個數據可能會有較大的區別。
使用多線程的理由之二是線程間方便的通信機制。對不同進程來說,它們具有獨立的數據空間,要進行數據的傳遞只能通過通信的方式進行,這種方式不僅費時,而且很不方便。線程則不然,由于同一進程下的線程之間共享數據空間,所以一個線程的數據可以直接為其它線程所用,這不僅快捷,而且方便。當然,數據的共享也帶來其他一些問題,有的變量不能同時被兩個線程所修改,有的子程序中聲明為static的數據更有可能給多線程程序帶來災難性的打擊,這些正是編寫多線程程序時最需要注意的地方。
除了以上所說的優點外,不和進程比較,多線程程序作為一種多任務、并發的工作方式,當然有以下的優點:
1) 提高應用程序響應。這對圖形界面的程序尤其有意義,當一個操作耗時很長時,整個系統都會等待這個操作,此時程序不會響應鍵盤、鼠標、菜單的操作,而使用多線程技術,將耗時長的操作(time consuming)置于一個新的線程,可以避免這種尷尬的情況。
2) 使多CPU系統更加有效。操作系統會保證當線程數不大于CPU數目時,不同的線程運行于不同的CPU上。
3) 改善程序結構。一個既長又復雜的進程可以考慮分為多個線程,成為幾個獨立或半獨立的運行部分,這樣的程序會利于理解和修改。
一個線程所包含的信息呈現出了它在一個進程中的執行環境,它們包括線程ID,線程棧,時刻優先級和策略(a scheduling priority and policy),信號屏蔽字,error變量以及線程相關的特定數據(線程私有數據)。在一個進程中幾乎所有的東西都是可以共享的,包括代碼段,全局變量以及堆、棧,還包括文件描述符等。一個線程的線程ID是用于在進程中唯一確定的標識,和進程ID不同,它只有在該線程所在的進程中才有意義。
線程的創建
首先我們來看一下調用函數:
[cpp]?view plaincopy
 
它的第一個參數是O類型的,用于獲取新創建的線程ID,attr是線程屬性,默認時可賦值為空,start_rtn是一個函數指針,線程創建成功后,該函數將作為線程的入口函數開始運行,arg是傳遞給該線程函數的參數。
新創建的線程有權訪問它所在進程的地址空間,它將繼承父線程的浮點環境(floating-point environment)以及信號屏蔽字,不過,已經處于阻塞隊列中的信號將被清空。
新創建的線程默認將以分離模式運行,也就是說,當線程結束時,系統將自動回收其資源,并扔掉其結束狀態。當然,我們也可以通過設置其線程屬性來使線程以非分離模式運行,在此模式下,線程結束時,必須由其他線程調用join函數來釋放其資源并獲取其結束狀態。
我們來看看線程屬性的設置方式,以下兩個函數用于初始化以及銷毀線程屬性結構體(并非釋放內存):
[cpp]?view plaincopy這樣,在初始化后,屬性結構體就被初始化為默認值,下面的函數可用于獲取以及設置線程的分離屬性: [cpp]?view plaincopy
 
在設置時,第二個參數分離狀態只能是這兩種常量:PTHREAD_CREATE_DETACHED(分離模式)和PTHREAD_CREATE_JOINABLE。
當然,我們也可以在運行期間修改線程的分離屬性,以下函數可以在線程運行時將其改變為分離模式:
[cpp]?view plaincopy線程屬性不止用于分離屬性,我們知道,由于所有的線程都使用同一個地址空間,每一個線程棧的大小就受到了限制,根據應用的業務邏輯,我們可能需要修改一個線程的棧的默認大小,例如,當存在的線程過多,我們就希望它的棧能夠小一點,而如果某個線程將要執行的業務邏輯需要進行很深的遞歸調用,我們就希望其運行棧能夠大一點。通過修改屬性的方式也能對這些進行設置:
[cpp]?view plaincopystackaddr是棧的首地址,stacksize則是棧的大小。當然,許多時候,我們不希望自己來處理內存的分配等事宜,也可以通過以下的函數只指定棧的大小:
[cpp]?view plaincopy一個屬性對象在被設置之后可以用于多個線程的創建,同時,當屬性對象使用完畢后,我們再對屬性對象的更改或是destroy都不會影響到那些已經創建完成的線程。
 
線程終止
 
 以下是線程正常終止的三種方式:
1. 線程從線程入口函數處返回,其返回值將是該線程的終止狀態。
2. 線程調用pthread_exit函數終止當前線程,該函數的參數將被作為終止狀態。
3. 該線程被相同進程中的其他線程所取消。
 
由于任意一個線程調用exit系列函數都將終止整個進程,因此當我們只想要終止線程時可以使用pthread_exit函數:
[cpp]?view plaincopy如果線程被取消了,則其終止狀態將是:PTHREAD_CANCELED。那么,我們該如何獲得線程的終止狀態呢?前面我們講過線程的兩種運行模式:分離模式和接合(join)模式。當處于分離狀態終止時,其終止狀態將自動被系統所舍棄,只有處于接合模式下,我們才能獲得其終止狀態,參看以下函數:
[cpp]?view plaincopy這個方法將阻塞直到它所指定的線程終止,參數rval_ptr是O類型的,用于獲取線程的終止狀態。
類似于atexit函數一樣,我們也可以為線程提供清理函數,以下是線程清理函數的注冊函數;
[cpp]?view plaincopy這兩個函數分別用于增加與刪除清理函數,清理函數可以不止一個,其組織形式按照棧的結構組織,所以增加刪除都是在棧頂操作,并且其調用順序也是與其添加順序相反的。出口函數在以下三種情況下會被調用:
1. 調用pthread_exit函數
2. 響應對線程的取消請求
3. 使用非0參數調用pthread_cleanup_pop函數。
從上面可以看出來,有一點也許會另我們意外,那就是,當線程從入口函數處正常返回時,清理函數并不會得到調用。
 
pthread_cleanup_push用于增加清理函數,我們來看一下pthread_cleanup_pop函數,它的作用是刪除最后一個被注冊的清理函數,如果其調用參數非0,那么在刪除該清理函數的同時,它也將得到調用。有一點需要注意的是,由于pthread_cleanup_push和pthread_cleanup_push可能被宏來實現,所以我們必須成對的使用它們,否則會報編譯錯誤。下面是linux的實現方式:
[cpp]?view plaincopy線程取消(cancellation)
 
     [cpp]?view plaincopy     該函數的默認效果是取消tid線程,這使得該線程仿佛自己調用了pthread_exit,使用PTHREAD_CANCELED作為其結束狀態。不過實際上,一個線程不必馬上對該取消請求進行響應,甚至可以忽略該請求。
在默認情況下,只有當線程運行到取消點(cancellation point)時才會對取消請求進行響應,一個取消點是指線程檢查其是否已經被取消的地方,POSIX.1定義了如下的一些函數作為取消點,當這些函數被調用時,將檢查是否被取消,從而作出響應:
 
當然,還有一些其他的函數也有可能作為取消點,不過那些是可選的,依照具體的實現而不同,不具備可移植性。實際上,我們也可以自己定義取消點。請看下面的函數:
[cpp]?view plaincopy當取消請求發生時,默認情況下,它會被阻塞,直到線程對它進行響應,該函數調用時,如果已有取消請求被阻塞住,并且線程取消功能并沒有被關閉(這個等會兒解釋)的話,該線程將被取消。
前面說的這些都是默認操作,實際上我們也可以修改取消的狀態和類型。先說取消類型(cancellation type),通過對該屬性進行設置可以決定線程是否在取消點才被取消,請看函數聲明:
[cpp]?view plaincopytype參數只能是如下常量之一:PTHREAD_CANCEL_DEFERRED(這個是默認的) or PTHREAD_CANCEL_ASYNCHRONOUS。使用第一個常量,則線程只有到取消點才會檢查取消請求,但后者則決定線程可以在任何時候被取消,不必等到取消點。oldtype是一個O類型參數,用于獲取歷史取消類型。
我們也可以修改其取消狀態使線程忽略其他線程的取消請求,使用如下函數:
[cpp]?view plaincopy通過此函數可以設置線程是否對線程取消進行響應,state只能是如下常量之一:PTHREAD_CANCEL_ENABLE 或是 PTHREAD_CANCEL_DISABLE。需要注意的是,當取消狀態被設置為PTHREAD_CANCEL_DISABLE時,取消請求并沒有被舍棄,它只是被阻塞住了,直到當該功能再此被啟用時,如果有阻塞的取消請求,線程將會被取消。
 
多線程下的信號量機制
每一個線程都有它自己的信號量屏蔽字,但是信號處理方式(signal disposition)卻是在進程內共享的。如果一個信號量是有硬件錯誤或是時鐘到點導致的,那么該信號將被發送給發生這些事件的線程,而如果不是這些情況引發的信號,它們將被發送給該進程下的任意一個線程。在多線程環境下使用sigprocmask函數修改信號屏蔽字的行為是未定義的,我們應該使用另外一個函數代替,那就是pthread_sigmask。
[cpp]?view plaincopy他的使用方式和sigprocmask函數是一致的,這里就不再多作討論了。
在多線程環境下,我們通常可以指定某個特定的線程來專門完成信號處理的工作,從而可以防止因其他工作線程被打斷而引發的異常情況,先來看一個函數:
[cpp]?view plaincopy
 
set參數的類型是我們前面將信號機制時所提到過的信號集,在這里它被用來指定我們想要處理的信號,而第二個參數是O類型參數,當該函數返回時,它存有我們實際接收到的信號number。當該函數調用后,線程會被阻塞,直到有它所等待的信號發生(實際上,該函數也是可能會被其他信號所中斷的)。如果在調用時,已經有它要等待的信號在阻塞隊列里了,那么該函數將立即返回,而不需要再阻塞。在返回之前,sigwait函數將移除掉阻塞隊列中它所等待的信號。為避免可能出現的空檔,造成錯誤的信號處理行為,線程在調用sigwait函數之前應該先把它要等待的信號給阻塞調。而在掉用sigwait函數的時候,它將自動unblock這些信號并開始等待直到那些信號中的一個被交付。在該函數返回以前,這些被unblock了的信號會被再次自動恢復阻塞。如果多個線程在調用wigwait時等待了相同的信號,當該信號發生后,只有一個線程會獲得該信號并從阻塞中返回。
在linux的實現中,必須要注意的是,由于linux中實際上沒有真正的線程,它所謂的線程實際上只是一個輕量級的進程,所以,當信號發生時,如果主線程沒有阻塞這個信號,其他線程是sigwait不到那個信號的。因此,在使用sigwait函數時,我們最好讓其他線程都把那些信號都給阻塞住。
 
實際上,我們也可以將信號發送給某個特定的線程,類似于kill,其函數聲明如下:
[cpp]?view plaincopy該函數的作用就是向指定的線程發送指定的信號量,這里有一個小技巧,當我們指定信號量為0時,該函數可以用來檢測該線程是否存在。如果接受到信號的線程對信號的默認操作是終止進程的話,那么整個進程都將被終止。
對于信號量,還有一點值得注意的是,alarm timers是屬于進程的資源,所有的線程都共享了同一個alarm,所以,對于同一個進程中的多個線程來說,不用擔心其他線程的干擾而使用alarm timers的方法是不存在的。
參考文獻
《Linux下的多線程編程》 姚繼鋒
總結
以上是生活随笔為你收集整理的Unix环境高级编程学习笔记(七) 多线程的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: odps新手上路之安装Eclipse开发
- 下一篇: 线程使用
