摘要:父進程調用創(chuàng)建子進程。因而,一個進程的第一個線程會隨著這個進程的啟動而創(chuàng)建,這個線程被稱為該進程的主線程。另一方面,線程不可能獨立于進程存在。終止線程線程可以通過多種方式來終結同一個進程中的其他線程。
前言
不積跬步,無以至千里;不積小流,無以成江海。在學習Java多線程相關的知識前,我們首先需要去了解一點操作系統(tǒng)的進程、線程以及相關的基礎概念。
進程通常,我們把一個程序的執(zhí)行稱為一個進程。反過來講,進程用于描述程序的執(zhí)行過程。因此,程序和進程是一對概念,它們分別描述了一個程序的靜態(tài)和動態(tài)特征:除此之外,進程還操作系統(tǒng)進行資源分配的一個基本單位。
進程的衍生進程使用fork系統(tǒng)調用來創(chuàng)建。父進程調用fork創(chuàng)建子進程。每個子進程都是源自它的父進程的一個副本,它會獲得父進程的數(shù)據(jù)段、堆和棧的副本,并與父進程共享代碼段。每一份副本都是獨立的,子進程對屬于它的副本的修改對其父進程和兄弟進程(同父進程)都是不可見的,反之亦然。全盤復制父進程的數(shù)據(jù)是一種相當?shù)托У淖龇ā?Linux操作系統(tǒng)內核使用寫時復制(Copy on Write,常簡稱為COW)等技術來提高進程創(chuàng)建的效率。當然,剛創(chuàng)建的子進程也可以通過系統(tǒng)調用exec把一個新的程序加載到己的內存中,而原先在內存中的數(shù)據(jù)段、堆、棧以及代碼段就會被替換掉,在這之后,子進程執(zhí)行的就會是那個剛剛加載進來的新程序。
父進程被如果優(yōu)先于子進程結束,那么子進程就會被原來父進程的父進程“收養(yǎng)”。
為了管理進程,內核必須對每個進程的數(shù)據(jù)和行為進行詳細的記錄,包括進程的優(yōu)先級、狀態(tài)、虛擬地址范圍以及各種訪問權限等等。更具體地說,這些信息都會被記在每個進程的進程描述符中。進程描述符并不是一個簡單的符號,而是一個非常復雜的數(shù)據(jù)結構。保存在進程描述符中的進程ID (常稱為PID )是進程在操作系統(tǒng)中的唯一標識,其中進程ID為1的進程就是之前提到的內核啟動進程。進程id是一個非負整數(shù)且總是順序的編號,新創(chuàng)建的進程ID總是前一個進程ID遞增的結果。此外,進程ID也可以重復使用。當進程ID達到其最大限值時,內核會從頭開始查找閑置的進程ID并使用M先找到的那一個作為新進程的ID。另外,進程描述符中還會包含當前進程的父進程的ID (常稱為PPID )。
進程間的同步如果多個進程之間需要協(xié)作完成任務,那么進程間通信的方式就是需要重點考慮的事項之一。這種通信叫做IPC(Inter-Process Communication)。那么在Linux中,從處理機制的角度看,可以分為三大類方法:
基于通信的IPC
基于信號的IPC
基于同步的IPC
通信IPC
以數(shù)據(jù)為傳送手段的IPC
管道(pipe):用于傳輸字節(jié)流
消息隊列(message queue):用來傳輸結構化的對象
以共享內存為手段的IPC
共享內存區(qū)(share memory):最快的IPC方法
信號IPC操作系統(tǒng)的信號(signal)機制:唯一一種異步IPC方法。通過kill -l查看。
同步IPC信號量(semaphore)
進程的狀態(tài)在Linux中,每個進程在每個時刻只會有一種狀態(tài),分別有以下六種
可運行狀態(tài)(TASK_RUNNING)該進程立刻或正在CPU上運行。但是運行的時期是不確定的,由進程調度來決定。
可中斷的睡眠狀態(tài)(TASK_INTERRUPTABLE)如果一個進程正在等待某個事件到來時,會進入此狀態(tài)。這樣的進程會被放入對應的等待隊列中。當事件發(fā)生時,對應的等待隊列中的一個或多個進程就會被喚醒。
不可中斷的睡眠狀態(tài)(TASK_UNINTERRUPTIBLE)此種狀態(tài)可與中斷的睡眠狀態(tài)的唯一區(qū)別是它不可被打斷。這意味著此種狀態(tài)的進程不會對任何信號作出響應。更確切地講,發(fā)送給此狀態(tài)的進程的信號直到它狀態(tài)轉出才會被傳遞過去。處于此狀態(tài)的進程通常是在等待一個特殊的時間,比如等待同步的IO操作完成。
暫停狀態(tài)(TASK_STOPPED或TASK_TRACED)或跟蹤狀態(tài)向進程發(fā)送SIGSTOP信號,就會使該進程轉入暫停狀態(tài),除非該進程正處于不可中斷的睡眠狀態(tài)。
向正處于暫停的進程發(fā)送SIGCONT信號,會使用該進程轉向可運行狀態(tài)。處于該狀態(tài)的進程會暫停,并等待另一個進程(跟蹤它的那個進程)對它進行操作。例如,我們使用調試工具GDB在某個程序中設置一個斷點,而后對應的進程運行到該斷點處就會停下來。這時,該進程就處于跟蹤狀態(tài)。跟蹤狀態(tài)與暫停狀態(tài)非常類似。但是,向處于跟蹤狀態(tài)的進程發(fā)送SIGCONT信號并不能使它回復。只有當調試進程進行了相應的系統(tǒng)調用或退出后,它才能夠恢復。
僵尸狀態(tài)(TASK_DEAD-EXIT_ZOMBIE)處于此狀態(tài)的進程即將結束運行,該進程占用的絕大多數(shù)資源也都已經(jīng)被回收,不過還有一些信息未還是拿出,比如退出碼以及一些統(tǒng)計信息。之所以保留這些信息,主要是考慮到該進程的父進程可能需要它們。由于此時的進程主體已經(jīng)被刪除而只留下一個空殼,故此狀態(tài)才被稱為僵尸狀態(tài)。
退出狀態(tài)(TASK_DEAD-EXIT_DEAD)在進程退出的過程中,有可能連退出碼和統(tǒng)計信息都不需要保留。造成這種情況的原因可能是顯示地讓該進程的父進程忽略掉SIGCHLD信號(當一個進程消亡的時候,內核會給其父進程發(fā)送SIGCHLD信號以告知此情況),也可能是該進程已經(jīng)被分離(分離即讓子進程和父進程分別獨立運行)。分離后的子程序將不會再使用和執(zhí)行與父進程共享代碼段中的指令,而是加載并運行一個全新的程序。在這些情況下,該進程處于退出的時候就不會轉入僵尸狀態(tài),而會直接轉入退出狀態(tài)。處于退出狀態(tài)的進程會立即被干凈利落地結束掉,它占用的系統(tǒng)資源也會被操作系統(tǒng)自動回收。
線程內核為每個用戶進程分配的是虛擬內存而不是物理內存。同時,內核會把進程的虛擬內存劃分為若干頁(page),而物理內存單元的劃分由CPU負責。一個物理內存單元被稱為一個頁框(page freame)。不同進程的大多數(shù)頁都會與不同的頁框相對應。對應的時候那就是共享內存了。
線程可以視為進程中的控制流。一個進程至少包含一個線程,因為其他至少會有一個控制流持續(xù)運行。因而,一個進程的第一個線程會隨著這個進程的啟動而創(chuàng)建,這個線程被稱為該進程的主線程。當然,一個進程可以包含多個線程。這些線程都是由當前線程中已經(jīng)存在的線程創(chuàng)建出來的,創(chuàng)建的方法就是調用系統(tǒng)調用(pthread_create)。擁有多個線程的進程可以并發(fā)執(zhí)行多個任務,并且即時某個或某些任務被阻塞,也不會影響其他任務執(zhí)行,這可以大大改善程序的響應時間和吞吐量。另一方面,線程不可能獨立于進程存在。它的生命周期不可能逾越所屬進程的生命周期。
一個進程中的所有線程都擁有自己線程棧,并以此存儲自己的私有數(shù)據(jù)。這些線程的線程棧都包含在其所屬進程的虛擬內存地址中。不過要注意,一個進程中的很多資源都會被其中的所有線程共享,這些被線程共享的資源包含當前進程所持有文件描述符,等等。正因為如此,同一個進程的多個線程運行的一定是同一個程序,只不過具體的控制流程的執(zhí)行函數(shù)可能有所不同。在同一個進程的多個線程之間共享數(shù)據(jù)也是一件非常輕松和自然的事情。另外,創(chuàng)建一個新線程,也不會像創(chuàng)建一個新進程那樣耗時費力,因為在其所屬進程的虛擬內存地址中存儲的代碼、數(shù)據(jù)和資源都不需要被復制。
另外,操作系統(tǒng)和提供了一定的系統(tǒng)調用用于管理當前進程中的線程。
線程的標識和進程一樣,每個線程都有自己的ID(由內核分配),叫做線程ID或者TID。但是在操作系統(tǒng)范圍內不唯一,在所屬進程的范圍內唯一。
線程的控制任何一個線程都可以同一線程中的其他線程進行有限管理,如下:
創(chuàng)建線程主線程在其所屬進程啟動時創(chuàng)建。其他線程可以通過別的線程用pthread_create來創(chuàng)建——要傳入新線程將要執(zhí)行的函數(shù)以及傳入該函數(shù)的參數(shù)值。在創(chuàng)建成功的時候,該函數(shù)會返回線程的TID。
終止線程線程可以通過多種方式來終結同一個進程中的其他線程。其他一種方式就是調用系統(tǒng)調用pthread_cancel,其作用是取消掉給定線程ID代表的那個線程。更確切地講,它會向目標線程發(fā)送一個請求,要求它立刻終止執(zhí)行。但是該函數(shù)只是發(fā)送請求并即可返回。但是,該函數(shù)只是發(fā)送請求并立刻返回,而不會等待目標線程對該請求做出響應。至于目標線程什么時候對此做出線程、怎么樣的響應,則取決與另外的因素(比如線程目標的取消狀態(tài)及類型)。在默認情況下,目標線程總是會接受線程取消請求,不過等到時機成熟(執(zhí)行到某個取消點)的時候,目標線程才會響應線程的取消請求。
連接已終止的線程此操作由系統(tǒng)調用pthread_join來執(zhí)行,該函數(shù)會一直等待與給定的線程ID對應的那個線程終止,并把線程執(zhí)行的pthread_create函數(shù)的返回值告知調用線程。如果目標線程已經(jīng)處于終止狀態(tài),那么該函數(shù)會立即返回。這就像是把調用線程放置在了目標線程的后面,當目標線程把線程控制權交出時,調用線程會接過流程控制權并繼續(xù)執(zhí)行pthread_join函數(shù)調用之后的代碼。這也把這一操作稱為連接的緣由之一。實際上,如果一個線程可被連接,那么在它終止之前就必須連接,否則就會變成一個僵尸線程。僵尸線程不但會導致系統(tǒng)資源浪費,還會無意義減少其進程的可創(chuàng)建線程數(shù)量。
分離線程將一個線程分離后那么它將變得不可連接。而在默認情況下,一個線程總是可以被連接的。分離操作的另一個作用是讓操作系統(tǒng)內核在目標線程終止時自行進行清理和銷毀工作。注意,分離操作是不可逆的。也就是說,我們無法使一個不可連接的線程變回可連接的狀態(tài)。不過,對于一個已處于分離狀態(tài)的線程,執(zhí)行終止操作仍然會起作用。分離操作由系統(tǒng)調用pthread_detach來執(zhí)行,它接受一個代表了線程ID的參數(shù)值。
一個線程對自身也可以進行兩種控制:終止和分離。線程終止自身的方式有很多種。在線程執(zhí)行的start函數(shù)中執(zhí)行return語句,會使該線程隨著start函數(shù)的結束而終止。需要注意的是,如果在主線程中執(zhí)行了return語句,那么當前進程中的所有線程都會終止。另外,在任意線程中調用系統(tǒng)調用exit也會達到這種效果。還有一種終止自身的方式就是顯示調用pthread_exit。
而分離pthread_detach函數(shù)則是傳入自己的TID。
多線程與多進程在多個線程之間交換線程是非常簡單和自然的事,而在多個進程之間只能通過一些額外的手段(比如管道、消息隊列、信號量和共享內存區(qū))傳遞數(shù)據(jù)。顯然,使用這些額外手段會增加開發(fā)成本。不過,線程間交換數(shù)據(jù)雖然簡單但卻由于可能發(fā)生競態(tài)條件而不得不使用一些同步工具(比如互斥量和條件變量)加以保護。這些與業(yè)務邏輯無關的代碼會增加程序的復雜度,尤其在使用不當?shù)那闆r下還會引起災難。
通用概念 原子操作互斥量可以理解為我們常見的鎖。而條件變量所做的就是保證線程間共享的數(shù)據(jù)狀態(tài)改變時通知到其他因此而被阻塞的線程。條件變量總是與互斥量組合使用。當線程成功鎖定互斥量并訪問到共享數(shù)據(jù)時,共享數(shù)據(jù)的狀態(tài)并不一定滿足它的要求。下面就通過一個示例來描述條件變量的使用場景。
執(zhí)行過程不能中斷的操作稱為原子操作(atomic operation)。必須一個單一的匯編指令表示,而且需要得到芯片級別的支持。
臨界區(qū)臨界區(qū)(critical section)用來表示一種公共資源或者共享數(shù)據(jù),可以被多個線程使用。但是每一次,只有一個線程可以使它,一旦臨界區(qū)資源被占用,其他線程要想使用資源,就必須等待,即串行化訪問或執(zhí)行。
互斥保證只有一個進程或線程在臨界區(qū)內的做法只有一個——互斥(mutual exclusion。簡稱 mutex)。
同步和異步描述的是用戶線程與內核的交互方式:
同步(Synchrounous)是指用戶線程發(fā)起 I/O 請求后需要等待或者輪詢內核 I/O 操作完成后才能繼續(xù)執(zhí)行;
異步(Asynchrounous)是指用戶線程發(fā)起 I/O 請求后仍繼續(xù)執(zhí)行,當內核 I/O 操作完成后會通知用戶線程,或者調用用戶線程注冊的回調函數(shù)。
阻塞和非阻塞描述的是用戶線程調用內核 I/O 操作的方式:
阻塞(Blocking)是指 I/O 操作需要徹底完成后才返回到用戶空間;
非阻塞(Non-Blocking)是指 I/O 操作被調用后立即返回給用戶一個狀態(tài)值,無需等到 I/O 操作徹底完成。
一個 I/O 操作其實分成了兩個步驟:
發(fā)起 I/O 請求
實際的 I/O 操作。
阻塞 I/O 和非阻塞 I/O 的區(qū)別在于第一步,發(fā)起 I/O 請求是否會被阻塞。如果阻塞直到完成那么就是傳統(tǒng)的阻塞 I/O ,如果不阻塞,那么就是非阻塞 I/O 。 同步 I/O 和異步 I/O 的區(qū)別就在于第二個步驟是否阻塞,如果實際的 I/O 讀寫阻塞請求進程,那么就是同步 I/O 。
并發(fā)(Concurrency)和并行(Parallelism)并發(fā)和并行往往被人所混淆。它們都可以表示兩個或多個任務一起執(zhí)行,但是偏重點有些不同。并發(fā)偏重于多個任務交替執(zhí)行,而多個任務有可能還是串行。而并行則是真正意義上的“同時執(zhí)行”。
嚴格來說,并行的多個任務是真實的同時執(zhí)行,而對并發(fā)來說,這個過程這是交替的,一會兒運行任務A一會兒執(zhí)行任務B,系統(tǒng)會不停地在兩者間切換。但對于外部觀察者來說,即使多個任務之間是串行并發(fā)的,也會造成多任務間是并行執(zhí)行的錯覺。
死鎖(DeadLock)、饑餓(Starvation)和活鎖(Livelock)死鎖、饑餓和活鎖都屬于多線程的活躍性問題,如果發(fā)生上述情況,那么相關線程可能就不再活躍,也就是說它可能很難繼續(xù)往下執(zhí)行了。
死鎖應該是最糟糕的一種情況了,雖然別的情況也沒有好到哪兒去。
死鎖:多個線程互相等待多方釋放資源而一直沒有執(zhí)行。
饑餓:一個或多個線程因為種種原因無法獲取所得的需要資源,導致一直無法執(zhí)行。導致的原因往往是當前線程優(yōu)先級不高導致沒有資源,或某線程一直占著關鍵資源不放。
活鎖:多個線程都釋放資源給別的線程使用,導致沒有線程拿到資源而正常執(zhí)行。
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://systransis.cn/yun/66071.html
摘要:系統(tǒng)級線程核心級線程由操作系統(tǒng)內核進行管理。值得注意的是多線程的存在,不是提高程序的執(zhí)行速度。實現(xiàn)多線程上面說了一大堆基礎,理解完的話。虛擬機的啟動是單線程的還是多線程的是多線程的。 前言 之前花了一個星期回顧了Java集合: Collection總覽 List集合就這么簡單【源碼剖析】 Map集合、散列表、紅黑樹介紹 HashMap就是這么簡單【源碼剖析】 LinkedHashMa...
摘要:網(wǎng)易跨境電商考拉海購在線筆試現(xiàn)場技術面面。如何看待校招面試招聘,對公司而言,是尋找勞動力對員工而言,是尋找未來的同事。 如何準備校招技術面試 標簽 : 面試 [TOC] 2017 年互聯(lián)網(wǎng)校招已近尾聲,作為一個非 CS 專業(yè)的應屆生,零 ACM 經(jīng)驗、零期刊論文發(fā)表,我通過自己的努力和準備,從找實習到校招一路運氣不錯,面試全部通過,謹以此文記錄我的校招感悟。 寫在前面 寫作動機 ...
閱讀 3947·2021-11-16 11:44
閱讀 3128·2021-11-12 10:36
閱讀 3383·2021-10-08 10:04
閱讀 1270·2021-09-03 10:29
閱讀 409·2019-08-30 13:50
閱讀 2623·2019-08-29 17:14
閱讀 1745·2019-08-29 15:32
閱讀 1090·2019-08-29 11:27