一、多線程的基本概念
1、進程(Process):
- 進程定義:進程可以理解成一個運行中的應(yīng)用程序玩焰,是一個活動的實體。它是是系統(tǒng)進行資源分配和調(diào)度的基本單位芍锚,也是基本的執(zhí)行單元昔园。每一個進程都有它自己的地址空間蔓榄,一般情況下,包括文本區(qū)域(text region)默刚、數(shù)據(jù)區(qū)域(data region)和堆棧(stack region)甥郑。
- 進程狀態(tài):進程有三個狀態(tài),就緒荤西、運行和阻塞澜搅。
1、運行態(tài):進程占用CPU邪锌,并在CPU上運行勉躺。
2、就緒態(tài):進程已經(jīng)具備運行條件觅丰,但是CPU還沒有分配過來饵溅。
3、阻塞態(tài):進程因等待某件事發(fā)生而暫時不能運行舶胀。
2概说、線程(Thread):
- 線程定義:是進程的基本執(zhí)行單元,一個進程對應(yīng)多個線程嚣伐。由于線程比進程更小糖赔,基本上不擁有系統(tǒng)資源,故對它的調(diào)度所付出的開銷就會小得多轩端,能更高效的提高系統(tǒng)多個程序間并發(fā)執(zhí)行的程度放典。
- 線程狀態(tài):線基本狀態(tài):新建狀態(tài)、就緒狀態(tài)基茵、運行狀態(tài)奋构、阻塞 (等待/睡眠)狀態(tài)、死亡狀態(tài)拱层。
1弥臼、新建狀態(tài):新創(chuàng)建一個線程對象。
2根灯、就緒狀態(tài):該狀態(tài)位于“可運行的線程池”中径缅,只等待獲取CPU的使用權(quán),調(diào)用start執(zhí)行烙肺。
3纳猪、運行狀態(tài):就緒狀態(tài)的線程獲取了CPU,執(zhí)行程序代碼桃笙。
4氏堤、阻塞狀態(tài):是線程因為某種原因放棄CPU使用權(quán),暫時停止運行搏明。
5鼠锈、死亡狀態(tài):線程執(zhí)行完了或者因異常退出了執(zhí)行方法闪檬,該線程結(jié)束生命周期。
3购笆、多線程谬以、主線程、父線程和子線程:
- 多線程:在同一時刻由桌,一個CPU只能處理1條線程,但CPU可以在多條線程之間快速的切換邮丰,只要切換的足夠快行您,就造成了多線程一同執(zhí)行的假象。(應(yīng)用層就理解為多任務(wù))剪廉;
- 主線程:可以認為娃循,主線程是程序(App)的一條主線(或UI線程),一旦主線程結(jié)束斗蒋,程序程序(App)就結(jié)束了捌斧,其他所有線程都結(jié)束,所以主線程是特殊的線程泉沾;
- 父線程:一個進程中可以有很多的線程捞蚂,這個時候可以新創(chuàng)建一個線程,而在這個線程中可以在再來創(chuàng)建別的線程跷究。這樣的話姓迅,線程之間就可以層層嵌套,之前最初創(chuàng)建的線程稱之為父線程俊马;
- 子線程:父線程中創(chuàng)建的線程(嵌套的線程)丁存,稱之為子線程。(ps:還有一種通俗說法:除了主線程之外的都叫子線程柴我,要注意區(qū)別和理解普通子線程和嵌套子線程)解寝。
4、主線程艘儒、父線程和子線程關(guān)系:
重點:
1聋伦、當(dāng)父線程消亡的時候,子線程是不會消亡的彤悔,是會繼續(xù)執(zhí)行到結(jié)束嘉抓。(因為在內(nèi)核當(dāng)中,線程都是獨立的內(nèi)核對象 )
2晕窑、 當(dāng)主線程消亡的時候抑片,所有線程都得死。
2杨赤、主線程是不能看作任何線程的父線程敞斋,因為不滿足父子線程的特性 截汪,主線程具有特殊性。
二植捎、任務(wù)與隊列
1衙解、任務(wù)(Task):
就是執(zhí)行操作的意思,換句話說就是你在線程中執(zhí)行的那段代碼焰枢。在 GCD 中是放在 block 中的蚓峦。執(zhí)行任務(wù)有兩種方式:同步執(zhí)行(sync)和異步執(zhí)行(async)。兩者的主要區(qū)別是:是否等待隊列的任務(wù)執(zhí)行結(jié)束济锄,以及是否具備開啟新線程的能力暑椰。
同步執(zhí)行(sync):
- 同步添加任務(wù)到指定的隊列中,在添加的任務(wù)執(zhí)行結(jié)束之前荐绝,會一直等待一汽,直到隊列里面的任務(wù)完成之后再繼續(xù)執(zhí)行。
- 只能在當(dāng)前線程中執(zhí)行任務(wù)低滩,不具備開啟新線程的能力召夹。
異步執(zhí)行(async):
- 異步添加任務(wù)到指定的隊列中,它不會做任何等待恕沫,可以繼續(xù)執(zhí)行任務(wù)监憎。
- 可以在新的線程中執(zhí)行任務(wù),具備開啟新線程的能力昏兆。
注意:異步執(zhí)行(async)雖然具有開啟新線程的能力枫虏,但是并不一定開啟新線程。這跟任務(wù)所指定的隊列類型有關(guān)爬虱。
2隶债、隊列(Queue):
這里的隊列指執(zhí)行任務(wù)的等待隊列,即用來存放任務(wù)的隊列跑筝。隊列是一種特殊的線性表死讹,采用 FIFO(先進先出)的原則,即新任務(wù)總是被插入到隊列的末尾曲梗,而讀取任務(wù)的時候總是從隊列的頭部開始讀取赞警。每讀取一個任務(wù),則從隊列中釋放一個任務(wù)虏两。
有兩種隊列:串行隊列和并發(fā)隊列愧旦。兩者都符合 FIFO(先進先出)的原則。兩者的主要區(qū)別是:執(zhí)行順序不同定罢,以及開啟線程數(shù)不同笤虫。
串行隊列(Serial Queue):
每次只有一個任務(wù)被執(zhí)行。讓任務(wù)一個接著一個地執(zhí)行。(只開啟一個線程琼蚯,一個任務(wù)執(zhí)行完畢后酬凳,再執(zhí)行下一個任務(wù))
并發(fā)隊列(Concurrent Queue):
可以讓多個任務(wù)并發(fā)(同時)執(zhí)行。(可以開啟多個線程遭庶,并且同時執(zhí)行任務(wù))
注意:并發(fā)隊列的并發(fā)功能只有在異步(async)函數(shù)任務(wù)下才有效
3宁仔、iOS主隊列、全局隊列峦睡、自定義隊列:
主隊列:專門負責(zé)調(diào)度主線程度的任務(wù)翎苫,沒有辦法開辟新的線程。所以榨了,在主隊列下的任務(wù)不管是異步任務(wù)還是同步任務(wù)都不會開辟線程拉队,任務(wù)只會在主線程順序執(zhí)行。
主隊列異步任務(wù):現(xiàn)將任務(wù)放在主隊列中阻逮,但是不是馬上執(zhí)行,等到主隊列中的其它所有除我們使用代碼添加到主隊列的任務(wù)的任務(wù)都執(zhí)行完畢之后才會執(zhí)行我們使用代碼添加的任務(wù)秩彤。
主隊列同步任務(wù):容易阻塞主線程叔扼,所以不要這樣寫。原因:我們自己代碼任務(wù)需要馬上執(zhí)行漫雷,但是主線程正在執(zhí)行代碼任務(wù)的方法體瓜富,因此代碼任務(wù)就必須等待,而主線程又在等待代碼任務(wù)的完成好去完成下面的任務(wù)降盹,因此就形成了相互等待与柑。整個主線程就被阻塞了。
全局隊列:本質(zhì)是一個并發(fā)隊列蓄坏,由系統(tǒng)提供价捧,方便編程,可以不用創(chuàng)建就直接使用涡戳。
自定義隊列:除了主隊列和系統(tǒng)提供的全局隊列之外结蟋,用戶自定義的串行或者并行隊列統(tǒng)稱為自定義隊列(普通隊列)。
注意:全局并發(fā)隊列渔彰、主隊列是兩種特殊隊列嵌屎,由iOS系統(tǒng)(GDC)提供。全局并發(fā)隊列可以作為普通并發(fā)隊列來使用恍涂。
4宝惰、隊列與任務(wù)組合:
我們有兩種隊列(串行隊列/并發(fā)隊列),兩種任務(wù)執(zhí)行方式(同步執(zhí)行/異步執(zhí)行)再沧,那么我們就有了四種不同的組合方式尼夺。這四種不同的組合方式是:
1、 同步執(zhí)行 + 并發(fā)隊列
2、 異步執(zhí)行 + 并發(fā)隊列
3汞斧、 同步執(zhí)行 + 串行隊列
4夜郁、 異步執(zhí)行 + 串行隊列
實際上,剛才還說了兩種特殊隊列:全局并發(fā)隊列粘勒、主隊列竞端。全局并發(fā)隊列可以作為普通并發(fā)隊列來使用。但是主隊列因為有點特殊庙睡,所以我們就又多了兩種組合方式事富。這樣就有六種不同的組合方式了。
5乘陪、 同步執(zhí)行 + 主隊列(容易阻塞卡死)
6统台、 異步執(zhí)行 + 主隊列
三、iOS多線程方案
4種實現(xiàn)方案:
pthread:一套C的通用的多線程API啡邑,線程生命周期需程序員管理贱勃,很少使用。
NSThread:使用更加面向?qū)ο蟀疲梢灾苯硬僮骶€程對象贵扰,線程生命周期需要程序員管理。
GCD(Grand Central Dispatch):基于C語言的API流部,是蘋果公司為多核的并行運算提出的解決方案戚绕,自動管理線程的生命周期(創(chuàng)建線程、調(diào)度任務(wù)枝冀、銷毀線程)舞丛。
NSOperation:是基于GCD的封裝,使用更加面向?qū)ο蠊€程生命周期自動管理球切。
1、pthread
POSIX threads 的簡稱绒障,POSIX 線程是一個 POSIX 標(biāo)準(zhǔn)線程欧聘,是一套純C語言的通用API,線程的生命周期需要程序員自己管理端盆,使用難度較大怀骤,所以在實際開發(fā)中通常不使用,我們可以在開源框架YY_Kit中(YYMemoryCache)看到大量使用pthread_mutex鎖焕妙。
#include <sys/ipc.h> // 進程間通信蒋伦。
#include <sys/msg.h> //消息隊列。
#include <pthread.h> //包含thread 庫焚鹊。
運用大量的API操作痕届,靈活多變韧献,難用。
pthread_create(&thread, NULL, run, NULL);中各項參數(shù)含義:
- 第一個參數(shù)&thread是線程對象研叫,指向線程標(biāo)識符的指針
- 第二個是線程屬性锤窑,可賦值NULL
- 第三個run表示指向函數(shù)的指針(run對應(yīng)函數(shù)里是需要在新線程中執(zhí)行的任務(wù))
- 第四個是運行函數(shù)的參數(shù),可賦值NULL
2嚷炉、NSThread
第一種方式實例化:
NSThread *newThread = [[NSThread alloc]initWithTarget:self selector:@selector(threadRun) object:nil];
//也可以使用另外兩種初始化函數(shù)
NSThread *newThread=[[NSThread alloc]init]; NSThread *newThread= [[NSThread alloc]initWithBlock:^{ NSLog(@"initWithBlock"); }];
第二種方式類方法:
//這種方式創(chuàng)建線程后自動啟動線程
[NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:@"lcj"];
第三種方式隱式創(chuàng)建:
//用于線程之間通信渊啰,比如:指定任務(wù)在當(dāng)前線程執(zhí)行
//不傳遞參數(shù)指定函數(shù)在當(dāng)前線程執(zhí)行
[self performSelector:@selector(doSomething)];
//傳遞參數(shù)指定函數(shù)在當(dāng)前線程執(zhí)行
[self performSelector: @selector(doSomething:) withObject:tempStr];
//傳遞參數(shù)指定函數(shù)2秒后在當(dāng)前線程執(zhí)行
[self performSelector:@selector(doSomething:) withObject:tempStr afterDelay:2.0];
3、GCD(Grand Central Dispatch)
是基于C語言的API申屹,充分利用設(shè)備的多核绘证,旨在替換NSThread等線程技術(shù)。線程的生命周期由系統(tǒng)自動管理哗讥,在實際開發(fā)中經(jīng)常使用嚷那。
GCD的優(yōu)勢:
GCD是蘋果公司為多核的并行運算提出的解決方案;
GCD會自動利用更多的CPU內(nèi)核(比如雙核、四核);
GCD會自動管理線程的生命周期(創(chuàng)建線程杆煞、調(diào)度任務(wù)魏宽、銷毀線程);
程序員只需要告訴GCD想要執(zhí)行什么任務(wù),不需要編寫任何線程管理代碼
另外其他的方法:
1决乎、 GCD柵欄(dispatch_barrier)
當(dāng)任務(wù)需要異步進行湖员,但是這些任務(wù)需要分成兩組來執(zhí)行,第一組完成之后才能進行第二組的操作瑞驱。這時候就用了到GCD的柵欄方法dispatch_barrier_async。
2 窄坦、GCD隊列組( dispatch_group )
異步執(zhí)行幾個耗時操作唤反,當(dāng)這幾個操作都完成之后再回到主線程進行操作,就可以用到隊列組了鸭津。
所有的任務(wù)會并發(fā)的執(zhí)行(不按序)彤侍。
所有的異步函數(shù)都添加到隊列中,然后再納入隊列組的監(jiān)聽范圍逆趋。
使用dispatch_group_notify函數(shù)盏阶,來監(jiān)聽上面的任務(wù)是否完成,如果完成, 就會調(diào)用這個方法闻书。
4名斟、NSOperation
NSOperation是基于GCD之上的更高一層封裝,NSOperation需要配合NSOperationQueue來實現(xiàn)多線程魄眉。NSOperation實現(xiàn)多線程的步驟如下:
1砰盐、創(chuàng)建任務(wù):先將需要執(zhí)行的操作封裝到NSOperation對象中。
2坑律、創(chuàng)建隊列:創(chuàng)建NSOperationQueue岩梳。
3、將任務(wù)加入到隊列中:將NSOperation對象添加到NSOperationQueue中。
需要注意的是冀值,NSOperation是個抽象類也物,實際運用時中需要使用它的子類,有三種方式:
1列疗、使用子類NSInvocationOperation
2滑蚯、使用子類NSBlockOperation定義繼承自NSOperation的子類,
3作彤、通過實現(xiàn)內(nèi)部相應(yīng)的方法來封裝任務(wù)膘魄。
四、線程安全與線程鎖
1竭讳、線程安全:
- 線程管理安全:使用pthread 创葡、NSTread等線程手動管理的API需要注意。
- 多線程訪問數(shù)據(jù)安全:多線程中同時對一個數(shù)據(jù)或內(nèi)存塊進行操作的行為(讀绢慢、寫灿渴、增、刪胰舆、遍歷等等)骚露。
- 線程鎖使用安全:濫用互斥鎖導(dǎo)致死鎖。
2缚窿、線程鎖的原理分類:
- 互斥鎖(Mutex Lock):互斥量是阻塞鎖棘幸。如果共享數(shù)據(jù)已經(jīng)有其他線程加鎖了,線程會進入休眠狀態(tài)等待鎖倦零,不會占用CPU資源误续。一旦被訪問的資源被解鎖,則等待資源的線程會被喚醒扫茅。
iOS中互斥鎖:
@synchronized,NSLock, pthread_mutex, NSConditionLock, NSCondition, NSRecursiveLock
- 自旋鎖( Spin Lock) :自旋鎖是一種非阻塞鎖蹋嵌。如果共享數(shù)據(jù)已經(jīng)有其他線程加鎖了,線程會以死循環(huán)的方式等待鎖葫隙,一直占用CPU資源栽烂,一旦被訪問的資源被解鎖,則等待資源的線程會立即執(zhí)行恋脚。用在以下情況:鎖持有的時間短腺办,而且線程并不希望在重新調(diào)度上花太多的成本。"原地打轉(zhuǎn)"糟描。
iOS中自旋鎖:
atomic菇晃、OSSpinLock、dispatch_semaphore_t
3蚓挤、兩種鎖適用于不同場景:
- 如果是多核處理器磺送,如果預(yù)計線程等待鎖的時間很短驻子,短到比線程兩次上下文切換時間要少的情況下,使用自旋鎖是劃算的估灿。
- 如果是多核處理器崇呵,如果預(yù)計線程等待鎖的時間較長,至少比兩次線程上下文切換的時間要長馅袁,建議使用互斥量域慷。
- 如果是單核處理器,一般建議不要使用自旋鎖汗销。因為犹褒,在同一時間只有一個線程是處在運行狀態(tài),那如果運行線程發(fā)現(xiàn)無法獲取鎖弛针,只能等待解鎖叠骑,但因為自身不掛起,所以那個獲取到鎖的線程沒有辦法進入運行狀態(tài)削茁,只能等到運行線程把操作系統(tǒng)分給它的時間片用完宙枷,才能有機會被調(diào)度。這種情況下使用自旋鎖的代價很高茧跋。
- 如果加鎖的代碼經(jīng)常被調(diào)用慰丛,但競爭情況很少發(fā)生時,應(yīng)該優(yōu)先考慮使用自旋鎖瘾杭,自旋鎖的開銷比較小诅病,互斥量的開銷較大≈嗨福互斥鎖線程進入休眠狀態(tài)既不占用CPU資源贤笆,但是為什么,互斥鎖比自旋鎖的效率低呢,是因為休眠页徐、以及喚醒休眠比忙等更加消耗CPU資源。
4银萍、線程鎖的功能分類:
- 讀寫鎖 ( rwlock ):是計算機程序的并發(fā)控制的一種同步機制变勇,也稱“共享-互斥鎖”、多讀者-單寫者鎖) 用于解決多線程對公共資源讀寫問題贴唇。讀操作可并發(fā)重入搀绣,寫操作是互斥的。 讀寫鎖通常用互斥鎖戳气、條件變量链患、信號量實現(xiàn)。
- 條件鎖:就是條件變量瓶您,當(dāng)進程的某些資源要求不滿足時就進入休眠麻捻,也就是鎖住了纲仍。當(dāng)資源被分配到了,條件鎖打開贸毕,進程繼續(xù)運行郑叠。
- 信號量(semaphore):是一種更高級的同步機制,互斥鎖可以說是semaphore在僅取值0/1時的特例明棍。信號量可以有更多的取值空間乡革,用來實現(xiàn)更加復(fù)雜的同步,而不單單是線程間互斥摊腋。
- 遞歸鎖 (recursivelock):嚴(yán)格上講遞歸鎖只是互斥鎖的一個特例沸版,同樣只能有一個線程訪問該對象,但允許同一個線程在未釋放其擁有的鎖時反復(fù)對該鎖進行加鎖操作兴蒸。如果是加鎖的可以繼續(xù)加鎖视粮,繼續(xù)往下走,不同線程來訪問這段代碼時类咧,發(fā)現(xiàn)有鎖要等待所有鎖解開之后才可以繼續(xù)往下走馒铃。