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