線程編程知識[轉載]

  索引:
  單線程複製模型 
  安全複製pthread_atfork 
  多線程複製模型 
  線程環境的exec()和exit() 
  非局部跳轉語句setjmp()和longjmp() 
  信號的擴展 
  線程安全函數 
  接口的多線程安全性級別 
  對應於不安全接口的可重入函數 
  多線程程序設計時常見問題 
  
  --------------------------------------------------------------------------------
  
  1.單線程複製模型
  概念
  POSIX線程接口的fork()是單線程複製模型,即函數創建一個新的進程,複製父進程的地址空間,但在子進程中只複製父進程中調用複製fork()函數的線程。
  
  當需要在子進程中立即調用exec()函數時,使用這種模型就不需要複製所有的線程,這樣可以減少資源浪費。
  
  在子進程中,在fork()函數調用和exec()函數調用之間,不要調用任何庫函數,因爲在子進程調用一個庫函數時,可能使用了父進程在調用fork()函數前已鎖定的鎖而導致死鎖。所以在子進程執行exec()函數之前只能執行異步信號安全(Async-Signal-Safe)的操作。
  
  防止死鎖
  爲了防止死鎖,要保證複製進程時沒有上述情況。最簡單的方法就是由調用fork函數的線程預先鎖定所有子進程中可能使用的鎖。但顯然對於在printf()函數中使用的鎖,程序是無法鎖定的。這時必須保證在調用fork()函數時,沒有printf()函數被調用。
  
  爲了管理好設計的庫中的鎖,須注意以下幾點:
  
  標誌出庫中使用的所有鎖; 
  確定出鎖的鎖定順序,如果鎖定次序不能固定,必須小心處理鎖定過程,避免死鎖; 
  在複製進程前鎖定庫中所有的鎖。 
  在POSIX線程中,可以在庫的init()函數中調用pthread_atfork(f1,f2,f3)函數,這種情況下,使用庫的程序員在複製單線程時不需要逐個鎖定庫中的鎖。f1()、f2()、f3()函數的作用是這樣的:
  
  f1():這個函數將會在進程複製前被調用; 
  f2():這個函數將會在進程複製後的子進程中被調用; 
  f3():這個函數將會在進程複製後的父進程中被調用。 
  詳情參見pthread_atfork()的說明。
  
  虛複製vfork
  標準的vfork()函數對多線程來說是不安全的,它一樣只複製當前線程到子進程,但vfork()函數不爲子進程複製內存空間。
  
  使用vfork()函數時,務必小心的是在子進程調用exec()函數前,子進程和父進程擁有同一個地址空間,只有在子進程終止或調用了exec()函數後,父進程才能獨佔自己的地址空間。所以在子進程中千萬不要改變父進程的狀態。
  
  全局狀態
  在調用了fork()函數後必須小心使用全局狀態。
  
  例如,當一個線程正在順序的讀文件時,進程中的另一個線程成功的調用了fork函數,這時每個進程都會有一個線程在讀文件。因爲在fork函數後文件的當前位置的指針也是共享的,所以就會出現父進程讀到一些數據而子進程讀到卻是另外一些數據。這導致了順序讀訪問讀出的數據中有遺漏。
  
   
  
  2.安全複製pthread_atfork
  #include 
  int pthread_atfork(void (*prepare)(void), void(*parent)(void), \
   void(*child)(void));
  返回值:函數成功返回0;任何其他返回值都表示錯誤
  函數指定了在fork()函數前後被當前線程調用的函數。其中:
  
  參數prepare指定的函數在fork()被調用之前被調用、參數parent指定的函數在fork()函數從父進程中返回後被調用、參數child指定的函數在fork()函數從子進程中返回後被調用。
  
  這3個參數中的任何一個都可以被指定爲NULL。若要連續多次調用pthread_atfork(),調用順序是十分重要的。
  
  pthread_atfork()函數指定的3個函數一般完成下列功能:prepare函數鎖定庫中所有的鎖,parent和child函數釋放這些鎖。這就保證了在fork函數之前,當前線程(調用fork()函數的線程)佔用庫中使用的所有鎖,而在fork函數之後父進程和子進程將分別釋放各自被鎖定的鎖,從而在子進程中不會發生死鎖。
  
   
  
  3.多線程複製模型
  POSIX線程標準不支持這種複製。
  
   
  
  4.線程環境的exec()和exit()
  exec()函數和exit()函數所完成的功能和它們在單線程程序中完成的功能是一樣的。在多線程程序中調用這兩個函數將會結束所有線程。這兩個函數在釋放完所有程序運行所用的資源(包括所有線程)之前將會阻塞。
  
  當exec()函數重建進程之後,它將創建一個輕進程(LWP)。進程的啓動代碼將創建初始線程。如果初始線程返回,它將調用exit()函數最終終止進程。
  
  如果在一個多線程的進程中調用exec()函數將會終止所有的線程,並裝載執行新的可執行代碼。這兩個函數將不會調用任何析構函數。
  
   
  
  5.非局部跳轉語句setjmp()和longjmp()
  setjmp()函數和longjmp()函數的有效範圍被限制在一個線程中,大部分情況下在線程中轉移已經夠用了。
  
  這限制了當一個信號發生時,接受到信號的線程在處理信號時,只能利用longjmp()函數跳轉到同一個線程中調用了setjmp()函數的地方。
  
  sigsetjmp()和siglongjmp()會保存和恢復線程的信號掩碼,但setjmp()和longjmp()函數則不會。當你使用信號處理函數時,最好使用sigsetjmp()和siglongjmp()函數。
  
   
  
  6.信號的擴展
  (1).新的語義
  傳統UNIX系統中的信號機制自然地擴展到了多線程的UNIX系統中。這種擴展的信號機制的主要特點是信號是發往進程的,而信號掩碼則是針對線程的。
  
  進程範圍的信號響應可用通過傳統的信號函數signal()、sigaction()等等來實現。
  
  當一個信號處理程序被設置爲SIG_DFL或SIG_IGN時,收到信號後的處理將針對整個進程,影響所有進程中的線程。對於那些沒有處理函數的信號,由於收到信號後的處理將針對整個進程,所以哪個線程收到了信號是不重要的。
  
  每一個線程都有它們自己的信號掩碼。這使線程可以不響應某些信號。所有一個進程中的線程都共享由sigaction()和它的變體函數所設置的一套信號處理函數。
  
  一個進程中的線程不能針對另一個進程中的某個指定線程發送信號。由kill()函數或sigsend()函數發給一個進程的信號可以被進程中的任何一個線程捕獲並響應。
  
  非綁定線程不能使用非缺省的信號堆棧,一個綁定線程可以使用非缺省的信號堆棧,這是由於非缺省的信號堆棧是和執行資源相關聯的。
  
  一個應用程序可以在基於進程的信號處理的基礎上建立針對線程的信號處理。方式之一就是在信號處理函數中取當前線程的標識符作爲索引來查找處理函數表(這個表記錄了各個線程各自的處理信號函數)。
  
  對多線程程序來說,特別重要的情況是信號對pthread_cond_wait()函數的影響。這個函數往往在其他線程調用了pthread_cond_signal()或pthread_cond_broadcast()函數後返回,但是如果等待pthread_cond_wait()函數返回的線程收到的一個傳統的UNIX信號,這個線程將從pthread_cond_wait()函數中返回,並返回錯誤代碼EINTR。
  
  (2).同步信號
  信號可以被分成兩類:陷阱和異常(同步產生的信號)、中斷(異步產生的信號)。
  
  陷阱(如SIGILL,SIGFPE,SIGSEGV)是由於線程自身的行爲產生的,如除0。一個陷阱只能被產生它的線程處理。可以同時有幾個線程產生和處理同一種陷阱。
  
  對於同步信號來說,將可將其看作是針對線程的,因爲處理信號的線程就是產生信號的線程。
  
  然而,如果線程沒有指定信號處理函數來響應同步信號,信號將會被收到同步信號的其他線程所響應。
  
  由於這樣的同步信號往往意味着對於整個進程來說有嚴重的錯誤,不僅僅是對線程而言,所以這時退出進程往往是明智的選擇。
  
  (3).異步信號
  中斷(例如SIGINT和SIGIO)都是異步產生的,往往都產生於進程之外。這類信號有可能是由其他線程產生的,或是進程外的動作產生的。
  
  一箇中斷可以被信號掩碼設爲允許狀態的任何線程所響應。如果多於一個線程可以響應該信號,那麼系統將挑選一個線程來響應該信號。
  
  當多個相同的信號發生時,有可能每一個信號都由不同的線程來處理。只要可以處理該信號的線程數足夠。如果所有的線程都用信號掩碼禁止了對該信號的響應,那麼信號將會被掛起直到有線程解開相應的掩碼。
  
  (4).持續語義
  持續語義是傳統的處理信號的方式。也就是說,當信號處理返回時,控制將返回到被信號中斷的程序那裏繼續運行。
  
  (5).函數pthread_sigsetmask
  設置線程的信號掩碼。
  
  當創建一個新線程時,它的初始化掩碼是從它的創建者那裏繼承的。
  
  在多線程程序中調用sigprocmask()函數等價於調用pthread_sigsetmask()函數。
  
  (6).函數pthread_kill
  向某線程發一個信號。這和向一個進程發信號是不同的,當一個信號被髮往進程時,信號可以被進程中的任何一個線程所捕獲。而pthread_kill()函數發的信號只能被指定的線程所捕獲。
  
  只能用pthread_kill()函數向本進程內的線程發信號。
  
  需要注意的是,當收到信號後處理函數(或SIG_DFL,SIG_IGN)進行的操作仍是全局的,針對進程的。例如:向一個線程發信號SIGXXX,進程對信號SIGXXX的處理是退出進程,則當目標線程收到信號後,整個進程將退出。
  
  (7).函數sigwait
  #include 
  int sigwait(const sigset_t *set, int *sig);
  當信號到達時,sigwait()清除掛起的信號,並把傳來的信號值賦給參數sig。
  
  sigwait()函數使當前線程保持等待直到某些信號發生。當線程在等待這些信號時,相應的信號掩碼將被打開。但sigwait()函數返回後,相應的信號掩碼被重新恢復成原來的狀態。
  
  所有指定的信號必須被所有的線程屏蔽,包括當前線程:否則sigwait()函數無法正常工作。注意sigwait()函數會自行打開指定信號的信號掩碼。
  
  可以有許多線程同時調用sigwait()函數,但當一個信號發生時,只會有一個線程收到信號並返回。
  
  用sigwait()函數可以讓一個線程專門處理異步信號,使其他線程不用考慮異步信號的處理。可以創建一個線程一直等待異步信號。而在進程的其他線程中則用信號掩碼阻塞異步信號,這樣程序在處理信號的問題上將會很安全。
  
  注意:sigwait()函數不能用在同步信號上。
  
  (8).函數sigtimedwait
  sigtimedwait()和sigwait()函數大體相同,除了一點:在等待了一段時間以後,如果等不到信號sigtimedwait函數將會返回錯誤代碼。
  
  (9).異步信號安全性
  和線程安全性相同的一個概念是異步信號安全性。異步信號安全的操作可以保證不會影響被信號中斷的操作。
  
  異步信號安全問題發生在信號處理函數有可能對被信號中斷的操作產生影響的時候。這個問題無法用同步原語解決,因爲任何試圖在信號處理函數和操作之間進行的同步都會立即產生死鎖。
  
  爲了防止被中斷操作和信號處理程序之間的衝突,必須保證相應的操作不會被中斷(可以通過在關鍵時刻屏蔽信號)或在信號處理函數中只調用異步信號安全函數。
  
  由於設置線程的信號掩碼(屏蔽信號)是一種耗費很少的用戶層操作。你可以簡單的通過信號屏蔽來實現異步信號安全。
  
  POSIX異步信號安全函數如下,任何信號處理函數久可以安全的調用這些函數:
  
  _exit() fstat() read() sysconf() access() 
  getegid() rename() tcdrain() alarm() geteuid() 
  rmdir() tcflow() cfgetispeed() getgid() setgid() 
  tcflush() cfgetospeed() getgroups() setpgid() tcgetattr() 
  cfsetispeed() getpgrp() setsid() tcgetpgrp() cfsetospeed() 
  getpid() setuid() tcsendbreak() chdir() getppid() 
  sigaction() tcsetattr() chmod() getuid() sigaddset() 
  tcsetpgrp() chown() kill() sigdelset() time() 
  close() link() sigemptyset() times() creat() 
  lseek() sigfillset() umask() dup2() mkdir() 
  sigismember() uname() dup() mkfifo() sigpending() 
  unlink() execle() open() sigprocmask() utime() 
  execve() pathconf() sigsuspend() wait() fcntl() 
  pause() sleep() waitpid() fork() pipe() 
  stat() write() 
  
  (10).中斷對條件變量的等待
  在POSIX線程中,pthread_cond_wait()函數遇到信號時會返回,但不會返回錯誤代碼,這種情況下,函數就像條件變量被喚醒一樣返回0。
  
   
  
  7.線程安全函數
  線程安全的函數是指:當這個函數同時被多個線程調用時,函數仍然能確保其在邏輯上的正確性。實際上,可以將線程安全性分爲三個級別:
  
  不安全的; 
  線程安全的-串行化的(Serializable); 
  線程安全的-多線程安全的(MT-safe); 
  一個不安全的函數可以通過在調用函數時使用互斥鎖來保證函數的線程安全性。
  
  串行化的函數用互斥鎖來防止函數被幾個線程併發調用。
  
  多線程安全的函數是指:函數是線程安全的(不存在數據競爭)同時不會對函數的性能有消極的影響(與串行化比較)。
  
   
  
  8.接口的多線程安全性級別
  安全的(Safe) 函數可以在多線程程序中調用 
  大致安全的(Safe with exceptions) 除了一些特殊情況 
  不安全的(Unsafe) 在多線程程序中使用這個函數是不安全的,除非由程序保證一次只有一個線程調用相應函數庫中的函數
  多線程安全的(MT-Safe) 這個函數對於多線程程序來說是安全的,而且它支持一定程度的併發執行 
  大致多線程安全的(MT-Safe withexceptions) 除了一些特殊情況 
  異步信號安全的(Asyn-Signal-Safe) 可以在信號處理函數中調用這個函數,一個在異步信號安全函數中運行的線程不會由於被信號中斷後調用同一個函數而死鎖 
  單線程複製安全的(Fork1-Safe) 這樣的函數或函數庫在有線程調用fork()函數時,會解鎖它們已鎖定的鎖 
  
   
  
  9.對應於不安全接口的可重入函數
  對於不安全接口中的大多數函數來說,都有一個多線程安全的函數與之對應。這個多線程安全函數的函數名往往是原來的非安全函數的函數名加上一個“_r”的後綴。
  
   
  
  10.多線程程序設計時常見問題
  把一個指向線程堆棧中變量的指針做爲線程的輸入參數傳遞到子線程中。
  
  不用同步機制保護對全局變量的訪問。
  
  在兩個線程中按不同的次序訪問兩個互斥鎖造成的死鎖。
  
  鎖定一個已經被本線程鎖定了的互斥鎖。
  
  同步保護中存在有隱藏的缺口,往往是由於在保護區中調用的函數將互斥鎖釋放後再重新鎖定。結果是從程序上看似乎提供了同步保護,其實卻沒有保護好。
  
  在線程運行時發生的信號未被合理處理,最好是用一個線程調用sigwait函數來專門處理信號。
  
  使用setjmp()和longjmp(),但是在調用longjmp函數後沒有釋放互斥鎖。
  
  從pthread_cond_wait()或pthread_cond_timedwait()函數中返回時,沒有重新判斷一下條件是否滿足。
  
  忘了缺省的線程都是PTHREAD_CREATE_JOINABLE的,而且必須用pthread_join()函數來歸還資源。注意pthread_exit()函數並不釋放線程的資源。
  
  使用大量嵌套或遞歸函數或使用大的局部數組可能會造成堆棧溢出,因爲多線程程序的堆棧大小更有限。
  
  指定的堆棧過小,或使用非默認堆棧。
  
  可以參考solaris的多線程多進程問題:  http://docs.oracle.com/cd/E19253-01/819-7051/gen-12013/
發佈了3 篇原創文章 · 獲贊 14 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章