問題? 用fork調(diào)用來創(chuàng)建新的進(jìn)程代價(jià)太高。
如果能讓一個(gè)進(jìn)程同時(shí)做兩件事情或至少看起來是這樣將會(huì)非常有用。而且,你可能有兩件更多的事情以一種非常緊密的方式同時(shí)發(fā)生。這就是需要線程發(fā)揮租用的時(shí)候了。
大綱:
》在進(jìn)程中創(chuàng)建新線程;
》在一個(gè)進(jìn)程中同步線程之間的數(shù)據(jù)訪問;
》修改線程的屬性;
》在同一個(gè)進(jìn)程中,從一個(gè)線程控制另外一個(gè)線程。
12.1什么是線程##
一個(gè)程序中的多個(gè)執(zhí)行路線就叫做 線程(thread);
定義:線程是一個(gè)進(jìn)程內(nèi)部的一個(gè)控制序列。
弄清楚fork系統(tǒng)調(diào)用和創(chuàng)建線程之間的區(qū)別很重要;
==》 當(dāng)進(jìn)程使用fork進(jìn)行調(diào)用時(shí),將創(chuàng)建該進(jìn)程的一份新的副本。這個(gè)新進(jìn)程擁有自己的變量和自己的PID,它的時(shí)間調(diào)度也是獨(dú)立的,它的執(zhí)行(通常)幾乎完全獨(dú)立于父進(jìn)程。當(dāng)在進(jìn)程中創(chuàng)建一個(gè)新進(jìn)程時(shí),新的執(zhí)行線程將擁有自己的棧(因此有自己的局部變量),但與它的創(chuàng)建者共享全局變量、文件描述符、信號(hào)處理函數(shù)和當(dāng)前目錄狀態(tài)。
PS:一般我們用fork來創(chuàng)建一個(gè)進(jìn)程的時(shí)候,這個(gè)進(jìn)程中就只有一個(gè)線程(可能在ios上就是主線程(ios上應(yīng)該是經(jīng)過處理和限制過的))。
對(duì)于單核CPU,線程的同時(shí)執(zhí)行只是一個(gè)聰明但非常有效的幻覺。
Linux的線程實(shí)現(xiàn)版本和POSIX標(biāo)準(zhǔn)之間還是存在著細(xì)微的差別,最明顯的是關(guān)于信號(hào)處理部分,這些差別中的大部分收到底層Linux內(nèi)核的限制,而不是函數(shù)庫實(shí)現(xiàn)所強(qiáng)加的。
PS: 信號(hào)量是和linux以及UNIX的內(nèi)核有關(guān)的;
優(yōu)化linux對(duì)線程的主持:增強(qiáng)linux線程的性能和刪除一些不需要的限制,其中大部分的工作都是集中在“如何將用戶級(jí)的線程映射到內(nèi)核級(jí)的線程”。
這些項(xiàng)目中有兩個(gè)重要的項(xiàng)目:
下一代POSIX線程(New Generation POSIX Thread,簡(jiǎn)寫為NGPT)
本地POSIX線程庫(Native POSIX Thread Library 簡(jiǎn)寫:NPTL).
都是通過修改linux上的內(nèi)容進(jìn)行修改支持新的函數(shù)庫;
后來重心放在NPTL,所以NPTL 這個(gè)將會(huì)成為下一代標(biāo)準(zhǔn)。
線程的優(yōu)點(diǎn)和缺點(diǎn)##
(雖然linux在創(chuàng)建進(jìn)程方面的效率也很高)
優(yōu)點(diǎn):
1)有時(shí),讓程序看起來好像是在同時(shí)運(yùn)行兩件事情是很有用的。
eg:
(1)在編輯文檔的同時(shí)對(duì)文檔中的單詞個(gè)數(shù)進(jìn)行實(shí)時(shí)統(tǒng)計(jì)。
這個(gè)是后一個(gè)線程負(fù)責(zé)處理用戶的輸入并執(zhí)行文本編輯工作,另外一個(gè)(它也可以看到相同的文檔內(nèi)容)則不斷刷新單詞計(jì)數(shù)變量……甚至還有第三個(gè)線程。
第一個(gè)線程通過這個(gè)共享的技術(shù)變量讓用戶隨時(shí)了解自己的工作進(jìn)展的情況。
(2)一個(gè)多線程的數(shù)據(jù)庫服務(wù),這是一種明顯的單進(jìn)程服務(wù)多用戶的情況。它會(huì)響應(yīng)一些請(qǐng)求的同時(shí)阻塞另外一些請(qǐng)求,使之等待磁盤操作,從而改善整體上的數(shù)據(jù)吞吐量。對(duì)數(shù)據(jù)庫來說,這個(gè)明顯的多任務(wù)工作如果用多進(jìn)程的方式倆完成將很艱難做到高效,因?yàn)楦鱾€(gè)不同的進(jìn)程必須緊密合作才能滿足加鎖和數(shù)據(jù)一致性方面,而用多線程來完成就比用更多進(jìn)程要容易多。
2)一個(gè)混雜著輸入、計(jì)算和輸出的應(yīng)用程序,可以將這幾個(gè)部分分離為3個(gè)線程來執(zhí)行,從而改善程序執(zhí)行的性能。
當(dāng)輸入或輸出線程等待連接時(shí),另外一個(gè)線程可以繼續(xù)執(zhí)行。因此,如果一個(gè)進(jìn)程在任一時(shí)刻最多只能夠一件事情的話,線程可以讓它在等待連接之列的事情的同時(shí)做一些其他有用的事情。一個(gè)需要同時(shí)處理多個(gè)網(wǎng)絡(luò)連接的服務(wù)器應(yīng)用程序也是一個(gè)天生適用于多線程的例子。
3)線程之間切換需要操作系統(tǒng)做的工作要比進(jìn)程之間的切換少得多,因此多個(gè)線程對(duì)資源的需求要遠(yuǎn)小于多個(gè)進(jìn)程。如果一個(gè)程序在邏輯上需要有多個(gè)執(zhí)行線程,那么在單處理器系統(tǒng)上把它運(yùn)行為一個(gè)多線程程序才更符合實(shí)際情況。
缺點(diǎn):
1)編寫多線程程序需要仔細(xì)設(shè)計(jì)。
(多線程程序中,因時(shí)序山的細(xì)微偏差或無意造成的變量共享而引發(fā)錯(cuò)誤的可能性很大)
2)對(duì)多線程的程序的調(diào)試要比單線程程序的調(diào)試?yán)щy得多,因?yàn)榫€程之間的交互非常難以控制。
3)將大量計(jì)算分成兩個(gè)部分,并把這兩個(gè)部分作為兩個(gè)不同的線程來運(yùn)行的程序在一臺(tái)單處理器機(jī)器上并不一定運(yùn)行的更快,除非計(jì)算確實(shí)允許它的不同部分可以被同時(shí)計(jì)算,而且運(yùn)行它的機(jī)器擁有多個(gè)處理器來支持真正的多處理。
第一個(gè)線程程序##


線程函數(shù)在頭文件<pthread.h>中,一般都是以pthread_開頭。
并且在編譯程序的時(shí)候需要用到選項(xiàng)-lpthread來鏈接線程庫(可以下面 的編譯指令);
在最初設(shè)計(jì)UNIX /POSIX庫歷程時(shí),我們假設(shè)的是每個(gè)進(jìn)程只有一個(gè)可執(zhí)行線程。一個(gè)明顯的例子就是errno,該變量就是用來用于獲取某個(gè)變量失敗之后的錯(cuò)誤信息。在一個(gè)多線程里面,默認(rèn)只有一個(gè)errno的變量供所有的變量共享。造成了一個(gè)線程在獲取剛才的錯(cuò)誤信息的時(shí)候,很容就被其他先線程所修改。類似的還有fputs之類的函數(shù),這個(gè)函數(shù)通常是用一個(gè)全局性區(qū)域來緩存輸出數(shù)據(jù)。
PS:一個(gè)進(jìn)程只有一個(gè)變量共享給該進(jìn)程的多個(gè)線程使用,就造成該變量值的改變。
這個(gè)問題應(yīng)該怎么樣修改?可以在不同的線程創(chuàng)建不同的errno。這個(gè)進(jìn)程沒有什么區(qū)別?? 局部變量的相對(duì)于線程而不是進(jìn)程。
解決方案:需要使用被稱為可重入的例程??芍厝氪a可以被多次調(diào)用而仍然正常工作,這些調(diào)用可以來自不同的線程以及也可以來自不同的嵌套調(diào)用。所以,代碼中可重入的部分通常只使用局部變量,這使得該代碼的調(diào)用都會(huì)獲得唯一一份數(shù)據(jù)副本。
【這個(gè)應(yīng)該就是前面所說的使用局部變量來處理每一個(gè)線程都有一份副本】
&& 宏定義的內(nèi)容是
編寫多線程時(shí),需要定義_REENTRANT來告訴編譯器我們需要進(jìn)行可重入功能,這個(gè)宏的定義必須位于程序中的任何#include 語句之前。 它將為我們做3件事情,并且做得非常優(yōu)雅,以至于我們一般不需要知道它到底做了哪些事。
1)它會(huì)對(duì)部分函數(shù)重新定義它們的可安全重入的版本,這些函數(shù)的名字一般不會(huì)發(fā)生改變,只是會(huì)在函數(shù)名后面臺(tái)添加_r字符串。例如:函數(shù)名字gethostbyname將變?yōu)間ethostbyname_r。
2)stdio.h 中原來宏的形式實(shí)現(xiàn)的一些函數(shù)將變成安全重入的函數(shù);
3)在errno.h 中定義的變量errno現(xiàn)在將成為一個(gè)函數(shù)調(diào)用,它能夠以一種多線程安全的方式來獲取真正的errno值。
在程序中包含頭文件pthread.h 還將向我們提供一些其他的將在代碼中使用到的定義和函數(shù)原型,就如同頭文件stdio.h 為標(biāo)準(zhǔn)輸入和標(biāo)準(zhǔn)輸出例程所提供的定義一樣。
最后,需要確保在程序中包含了正確的線程頭文件,并且在編譯器程序時(shí)鏈接了實(shí)現(xiàn)pthread函數(shù)的正確的線程庫。
PS: 基本上明白了多線程函數(shù)的使用以及調(diào)用的
上面函數(shù)的解析
創(chuàng)建一個(gè)線程(類似于進(jìn)程中的fork() 創(chuàng)建進(jìn)程)
int pthread_create(pthread_t _Nullable * _Nonnull __restrict,
const pthread_attr_t * _Nullable __restrict,
void * _Nullable (* _Nonnull)(void * _Nullable),
void * _Nullable __restrict);
// 第一個(gè)參數(shù): 指向pthread_t 類型數(shù)據(jù)的指針,線程創(chuàng)建的時(shí)候,這個(gè)指針指向的變量中將被寫入一個(gè)標(biāo)示符,應(yīng)用這個(gè)標(biāo)示符來引用新的線程
// 第二個(gè)參數(shù): 用于設(shè)置線程屬性 (一般不需要特殊的屬性,所以這里設(shè)置為NULL)
// 第三個(gè)參數(shù):告訴線程將要啟動(dòng)執(zhí)行的函數(shù) (調(diào)用了thread_function)
// 第四個(gè)參數(shù):傳遞給第三個(gè)參數(shù)函數(shù)的的參數(shù)(就是傳給thread_function這個(gè)函數(shù)的參數(shù))
void * (*)(void *)
第三個(gè)參數(shù)的內(nèi)容要求:我們必須傳遞一個(gè)函數(shù)地址,該函數(shù)以一個(gè)指向void的指針為參數(shù),返回的也是一個(gè)指向void的指針為參數(shù),返回的也是一個(gè)指向void的指針。
【用fork調(diào)用后,父子進(jìn)程將在同意而位置繼續(xù)執(zhí)行下去,只是fork調(diào)用返回值是不同的,
&&&&&&
但是對(duì)于新線程來說,我們必須明確提供給他一個(gè)函數(shù)指針,新線程將在這個(gè)新位置開始執(zhí)行。】
創(chuàng)建線程的函數(shù)若是成功就會(huì)返回0,若是失敗就會(huì)返回錯(cuò)誤代碼。
pthread_create和大多數(shù)pthread_系列函數(shù)一樣,在失敗的時(shí)并沒有遵循UNIX函數(shù)的慣例返回-1,這種情況在UNIX函數(shù)中屬于一少部分。
void pthread_exit(void *retval)
線程通過調(diào)用pthread_exit函數(shù)終止執(zhí)行,就如同進(jìn)程在結(jié)束調(diào)用exit函數(shù)一樣。這個(gè)函數(shù)的作用是,終止調(diào)用它的線程并返回一個(gè)指向某個(gè)對(duì)象的指針。注意,絕不能夠用它來返回一個(gè)指向局部變量的指針,因?yàn)榫€程調(diào)用該函數(shù)后,這個(gè)局部變量就不在存在了,這將引起嚴(yán)重的程序漏洞。
int pthread_join(pthread_t th, void **thread_return);
這個(gè)參數(shù)指定了將要等待的線程,線程通過pthread_create返回的標(biāo)識(shí)符來指定。第二個(gè)參數(shù)是一個(gè)指針,它指向另外一個(gè)指針,而后者指向線程的返回值。與pthread_create類似,這個(gè)函數(shù)在成功時(shí)返回0,失敗的時(shí)候返回錯(cuò)誤碼。
基本步驟:
(1)編譯這個(gè)程序的時(shí)候,我們首先定義宏定義_REENTRANT.在少數(shù)系統(tǒng)上,可能還需要定義宏:_POSIX_C_SOURCE ,但一般不需要定義它。
(2)接下來必須鏈接真確的線程庫。(如實(shí)使用老的Linux版本,默認(rèn)的線程庫不是NPTL,需要升級(jí)linux)簡(jiǎn)單的檢查方法是查看頭文件/usr/include/pthread.h 。 如果這個(gè)文件中顯示的版本日期是2003年或更晚,那幾乎可以肯定你的Linux發(fā)型版使用的是NPTL實(shí)現(xiàn)。
(3)在驗(yàn)證并安裝了正確的文件后,就可以進(jìn)行編譯。

(4)運(yùn)行這個(gè)程序可以看到結(jié)果(在mac上運(yùn)行也是可以的(xcode))
實(shí)驗(yàn)解析:
(1)定義了在創(chuàng)建線程時(shí)需要由它調(diào)用的一個(gè)函數(shù)的原型:
void *thread_function(void *arg);
(2)根據(jù)pthread_create的要求,它只有一個(gè)指向void的指針作為參數(shù),返回的也是指向void的指針。
(3)main函數(shù)中,首先定義幾個(gè)變量,然后調(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ù)傳遞了一個(gè)pthread_t類型對(duì)象的地址,今后可以用它來引用這個(gè)新線程。我們不想改變默認(rèn)的線程屬性,所以設(shè)置第二個(gè)參數(shù)為NULL。最后兩個(gè)參數(shù)分別為將要調(diào)用的函數(shù)和一個(gè)傳遞給該函數(shù)的參數(shù)。
如果調(diào)用成功了,就會(huì)有兩個(gè)線程在運(yùn)行。原先的線程(main)繼續(xù)執(zhí)行pthread_create后面的代碼,而新線程開始執(zhí)行thread_result函數(shù)。
原先的線程在查明新線程已經(jīng)啟動(dòng)后,將調(diào)用pthread_join函數(shù),
res = pthread_join(a_thread,&thread_result);
PS: 這個(gè)方法 才是 執(zhí)行線程的方法,第二個(gè)參數(shù)一定鑰匙全局變量。
我們給該函數(shù)傳遞兩個(gè)參數(shù),一個(gè)是正在等待其結(jié)束的線程的標(biāo)示符,休眠一會(huì)兒,然后更新全局變量,最后退出并向主線程返回一個(gè)字符串。新線程修改了數(shù)組message,而原先的線程也可以訪問該數(shù)組。如果我們調(diào)用fork而不是pthread_create,就不會(huì)有這樣的效果了。
12.4 同時(shí)執(zhí)行##
首先明白概念: 單cpu使用的是“輪詢技術(shù)”;程序仍然利用這一事實(shí),即除局部變量外,所有其他變量都將在一個(gè)進(jìn)程中的所有線程之間共享。

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

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