12 posix 線程

問題? 用fork調(diào)用來創(chuàng)建新的進程代價太高。
如果能讓一個進程同時做兩件事情或至少看起來是這樣將會非常有用。而且,你可能有兩件更多的事情以一種非常緊密的方式同時發(fā)生。這就是需要線程發(fā)揮租用的時候了。

大綱:
》在進程中創(chuàng)建新線程;
》在一個進程中同步線程之間的數(shù)據(jù)訪問;
》修改線程的屬性;
》在同一個進程中,從一個線程控制另外一個線程。

12.1什么是線程##

一個程序中的多個執(zhí)行路線就叫做 線程(thread);
定義:線程是一個進程內(nèi)部的一個控制序列。


弄清楚fork系統(tǒng)調(diào)用和創(chuàng)建線程之間的區(qū)別很重要;
==》 當(dāng)進程使用fork進行調(diào)用時,將創(chuàng)建該進程的一份新的副本。這個新進程擁有自己的變量和自己的PID,它的時間調(diào)度也是獨立的,它的執(zhí)行(通常)幾乎完全獨立于父進程。當(dāng)在進程中創(chuàng)建一個新進程時,新的執(zhí)行線程將擁有自己的棧(因此有自己的局部變量),但與它的創(chuàng)建者共享全局變量、文件描述符、信號處理函數(shù)和當(dāng)前目錄狀態(tài)。
PS:一般我們用fork來創(chuàng)建一個進程的時候,這個進程中就只有一個線程(可能在ios上就是主線程(ios上應(yīng)該是經(jīng)過處理和限制過的))。

對于單核CPU,線程的同時執(zhí)行只是一個聰明但非常有效的幻覺。

Linux的線程實現(xiàn)版本和POSIX標(biāo)準(zhǔn)之間還是存在著細微的差別,最明顯的是關(guān)于信號處理部分,這些差別中的大部分收到底層Linux內(nèi)核的限制,而不是函數(shù)庫實現(xiàn)所強加的。
PS: 信號量是和linux以及UNIX的內(nèi)核有關(guān)的;

優(yōu)化linux對線程的主持:增強linux線程的性能和刪除一些不需要的限制,其中大部分的工作都是集中在“如何將用戶級的線程映射到內(nèi)核級的線程”。
這些項目中有兩個重要的項目:
下一代POSIX線程(New Generation POSIX Thread,簡寫為NGPT)
本地POSIX線程庫(Native POSIX Thread Library 簡寫:NPTL).
都是通過修改linux上的內(nèi)容進行修改支持新的函數(shù)庫;
后來重心放在NPTL,所以NPTL 這個將會成為下一代標(biāo)準(zhǔn)。

線程的優(yōu)點和缺點##

(雖然linux在創(chuàng)建進程方面的效率也很高)
優(yōu)點:
1)有時,讓程序看起來好像是在同時運行兩件事情是很有用的。

eg:
(1)在編輯文檔的同時對文檔中的單詞個數(shù)進行實時統(tǒng)計。
這個是后一個線程負(fù)責(zé)處理用戶的輸入并執(zhí)行文本編輯工作,另外一個(它也可以看到相同的文檔內(nèi)容)則不斷刷新單詞計數(shù)變量……甚至還有第三個線程。
第一個線程通過這個共享的技術(shù)變量讓用戶隨時了解自己的工作進展的情況。
(2)一個多線程的數(shù)據(jù)庫服務(wù),這是一種明顯的單進程服務(wù)多用戶的情況。它會響應(yīng)一些請求的同時阻塞另外一些請求,使之等待磁盤操作,從而改善整體上的數(shù)據(jù)吞吐量。對數(shù)據(jù)庫來說,這個明顯的多任務(wù)工作如果用多進程的方式倆完成將很艱難做到高效,因為各個不同的進程必須緊密合作才能滿足加鎖和數(shù)據(jù)一致性方面,而用多線程來完成就比用更多進程要容易多。

2)一個混雜著輸入、計算和輸出的應(yīng)用程序,可以將這幾個部分分離為3個線程來執(zhí)行,從而改善程序執(zhí)行的性能。
當(dāng)輸入或輸出線程等待連接時,另外一個線程可以繼續(xù)執(zhí)行。因此,如果一個進程在任一時刻最多只能夠一件事情的話,線程可以讓它在等待連接之列的事情的同時做一些其他有用的事情。一個需要同時處理多個網(wǎng)絡(luò)連接的服務(wù)器應(yīng)用程序也是一個天生適用于多線程的例子。

3)線程之間切換需要操作系統(tǒng)做的工作要比進程之間的切換少得多,因此多個線程對資源的需求要遠小于多個進程。如果一個程序在邏輯上需要有多個執(zhí)行線程,那么在單處理器系統(tǒng)上把它運行為一個多線程程序才更符合實際情況。

缺點:
1)編寫多線程程序需要仔細設(shè)計。
(多線程程序中,因時序山的細微偏差或無意造成的變量共享而引發(fā)錯誤的可能性很大)
2)對多線程的程序的調(diào)試要比單線程程序的調(diào)試?yán)щy得多,因為線程之間的交互非常難以控制。
3)將大量計算分成兩個部分,并把這兩個部分作為兩個不同的線程來運行的程序在一臺單處理器機器上并不一定運行的更快,除非計算確實允許它的不同部分可以被同時計算,而且運行它的機器擁有多個處理器來支持真正的多處理。

第一個線程程序##

第一個線程代碼

運行結(jié)果

線程函數(shù)在頭文件<pthread.h>中,一般都是以pthread_開頭。
并且在編譯程序的時候需要用到選項-lpthread來鏈接線程庫(可以下面 的編譯指令);

在最初設(shè)計UNIX /POSIX庫歷程時,我們假設(shè)的是每個進程只有一個可執(zhí)行線程。一個明顯的例子就是errno,該變量就是用來用于獲取某個變量失敗之后的錯誤信息。在一個多線程里面,默認(rèn)只有一個errno的變量供所有的變量共享。造成了一個線程在獲取剛才的錯誤信息的時候,很容就被其他先線程所修改。類似的還有fputs之類的函數(shù),這個函數(shù)通常是用一個全局性區(qū)域來緩存輸出數(shù)據(jù)。
PS:一個進程只有一個變量共享給該進程的多個線程使用,就造成該變量值的改變。

這個問題應(yīng)該怎么樣修改?可以在不同的線程創(chuàng)建不同的errno。這個進程沒有什么區(qū)別?? 局部變量的相對于線程而不是進程。
解決方案:需要使用被稱為可重入的例程??芍厝氪a可以被多次調(diào)用而仍然正常工作,這些調(diào)用可以來自不同的線程以及也可以來自不同的嵌套調(diào)用。所以,代碼中可重入的部分通常只使用局部變量,這使得該代碼的調(diào)用都會獲得唯一一份數(shù)據(jù)副本。
【這個應(yīng)該就是前面所說的使用局部變量來處理每一個線程都有一份副本】

&& 宏定義的內(nèi)容是
編寫多線程時,需要定義_REENTRANT來告訴編譯器我們需要進行可重入功能,這個宏的定義必須位于程序中的任何#include 語句之前。 它將為我們做3件事情,并且做得非常優(yōu)雅,以至于我們一般不需要知道它到底做了哪些事。
1)它會對部分函數(shù)重新定義它們的可安全重入的版本,這些函數(shù)的名字一般不會發(fā)生改變,只是會在函數(shù)名后面臺添加_r字符串。例如:函數(shù)名字gethostbyname將變?yōu)間ethostbyname_r。
2)stdio.h 中原來宏的形式實現(xiàn)的一些函數(shù)將變成安全重入的函數(shù);
3)在errno.h 中定義的變量errno現(xiàn)在將成為一個函數(shù)調(diào)用,它能夠以一種多線程安全的方式來獲取真正的errno值。
在程序中包含頭文件pthread.h 還將向我們提供一些其他的將在代碼中使用到的定義和函數(shù)原型,就如同頭文件stdio.h 為標(biāo)準(zhǔn)輸入和標(biāo)準(zhǔn)輸出例程所提供的定義一樣。
最后,需要確保在程序中包含了正確的線程頭文件,并且在編譯器程序時鏈接了實現(xiàn)pthread函數(shù)的正確的線程庫。

PS: 基本上明白了多線程函數(shù)的使用以及調(diào)用的

上面函數(shù)的解析
創(chuàng)建一個線程(類似于進程中的fork() 創(chuàng)建進程)

int pthread_create(pthread_t _Nullable * _Nonnull __restrict,
                   const pthread_attr_t * _Nullable __restrict,
                   void * _Nullable (* _Nonnull)(void * _Nullable),
                   void * _Nullable __restrict);

// 第一個參數(shù): 指向pthread_t 類型數(shù)據(jù)的指針,線程創(chuàng)建的時候,這個指針指向的變量中將被寫入一個標(biāo)示符,應(yīng)用這個標(biāo)示符來引用新的線程
// 第二個參數(shù): 用于設(shè)置線程屬性 (一般不需要特殊的屬性,所以這里設(shè)置為NULL)
// 第三個參數(shù):告訴線程將要啟動執(zhí)行的函數(shù) (調(diào)用了thread_function)
// 第四個參數(shù):傳遞給第三個參數(shù)函數(shù)的的參數(shù)(就是傳給thread_function這個函數(shù)的參數(shù))
void * (*)(void *)
第三個參數(shù)的內(nèi)容要求:我們必須傳遞一個函數(shù)地址,該函數(shù)以一個指向void的指針為參數(shù),返回的也是一個指向void的指針為參數(shù),返回的也是一個指向void的指針。
【用fork調(diào)用后,父子進程將在同意而位置繼續(xù)執(zhí)行下去,只是fork調(diào)用返回值是不同的,
&&&&&&
但是對于新線程來說,我們必須明確提供給他一個函數(shù)指針,新線程將在這個新位置開始執(zhí)行。】
創(chuàng)建線程的函數(shù)若是成功就會返回0,若是失敗就會返回錯誤代碼。

pthread_create和大多數(shù)pthread_系列函數(shù)一樣,在失敗的時并沒有遵循UNIX函數(shù)的慣例返回-1,這種情況在UNIX函數(shù)中屬于一少部分。

void pthread_exit(void *retval)
線程通過調(diào)用pthread_exit函數(shù)終止執(zhí)行,就如同進程在結(jié)束調(diào)用exit函數(shù)一樣。這個函數(shù)的作用是,終止調(diào)用它的線程并返回一個指向某個對象的指針。注意,絕不能夠用它來返回一個指向局部變量的指針,因為線程調(diào)用該函數(shù)后,這個局部變量就不在存在了,這將引起嚴(yán)重的程序漏洞。

int pthread_join(pthread_t th, void **thread_return);
這個參數(shù)指定了將要等待的線程,線程通過pthread_create返回的標(biāo)識符來指定。第二個參數(shù)是一個指針,它指向另外一個指針,而后者指向線程的返回值。與pthread_create類似,這個函數(shù)在成功時返回0,失敗的時候返回錯誤碼。

基本步驟:
(1)編譯這個程序的時候,我們首先定義宏定義_REENTRANT.在少數(shù)系統(tǒng)上,可能還需要定義宏:_POSIX_C_SOURCE ,但一般不需要定義它。
(2)接下來必須鏈接真確的線程庫。(如實使用老的Linux版本,默認(rèn)的線程庫不是NPTL,需要升級linux)簡單的檢查方法是查看頭文件/usr/include/pthread.h 。 如果這個文件中顯示的版本日期是2003年或更晚,那幾乎可以肯定你的Linux發(fā)型版使用的是NPTL實現(xiàn)。
(3)在驗證并安裝了正確的文件后,就可以進行編譯。


編譯命令

(4)運行這個程序可以看到結(jié)果(在mac上運行也是可以的(xcode))

實驗解析:
(1)定義了在創(chuàng)建線程時需要由它調(diào)用的一個函數(shù)的原型:
void *thread_function(void *arg);
(2)根據(jù)pthread_create的要求,它只有一個指向void的指針作為參數(shù),返回的也是指向void的指針。
(3)main函數(shù)中,首先定義幾個變量,然后調(diào)用pthread_create后面的代碼,而新線程開始執(zhí)行thread_function函數(shù)。
pthread_t a_thread;
void thread_result;
res = pthread_create(&athread,NULL,thread_function,(void
)message);
我們向pthread_create函數(shù)傳遞了一個pthread_t類型對象的地址,今后可以用它來引用這個新線程。我們不想改變默認(rèn)的線程屬性,所以設(shè)置第二個參數(shù)為NULL。最后兩個參數(shù)分別為將要調(diào)用的函數(shù)和一個傳遞給該函數(shù)的參數(shù)。
如果調(diào)用成功了,就會有兩個線程在運行。原先的線程(main)繼續(xù)執(zhí)行pthread_create后面的代碼,而新線程開始執(zhí)行thread_result函數(shù)。
原先的線程在查明新線程已經(jīng)啟動后,將調(diào)用pthread_join函數(shù),
res = pthread_join(a_thread,&thread_result);
PS: 這個方法 才是 執(zhí)行線程的方法,第二個參數(shù)一定鑰匙全局變量。
我們給該函數(shù)傳遞兩個參數(shù),一個是正在等待其結(jié)束的線程的標(biāo)示符,休眠一會兒,然后更新全局變量,最后退出并向主線程返回一個字符串。新線程修改了數(shù)組message,而原先的線程也可以訪問該數(shù)組。如果我們調(diào)用fork而不是pthread_create,就不會有這樣的效果了。

12.4 同時執(zhí)行##

首先明白概念: 單cpu使用的是“輪詢技術(shù)”;程序仍然利用這一事實,即除局部變量外,所有其他變量都將在一個進程中的所有線程之間共享。

修改的代碼

主線程中 // 如果run_now 的值是1,就打印1并設(shè)置為2,否則就稍做休息然后再檢查它的值。我們不斷的檢查來等待它的值變?yōu)?,這個種方式被稱為“忙等待”,雖然已經(jīng)在兩次檢查之間休息1秒鐘來減慢檢查的頻率。在本章的后面我們將看到對這個問題的一個更好的解決方法。
新線程中
在新的線程執(zhí)行的thread_function函數(shù)中,我們所做的事情和上面的大部分相同,只是把run_now 的值顛倒一下。

運行結(jié)果

實驗解析:
每個線程通過設(shè)置run_now變量的方法來通知另外一個線程開始運行,然后,它會等待另一個線程改變了這個變量的值后再次運行。這個例子顯示了兩個線程之間自動交替執(zhí)行,同時也再次闡明了一個觀點,即這兩個線程共享run_now 變量。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容