進(jìn)程與線程
在了解多線程之前,需要弄清進(jìn)程和線程的概念和他們之間的區(qū)別咙边。
進(jìn)程:
系統(tǒng)中正在運(yùn)行的一個程序,進(jìn)程之間是相互獨(dú)立的次员,每個進(jìn)程都有屬于自己的內(nèi)存空間败许。比如手機(jī)中的微信應(yīng)用和印象筆記應(yīng)用,他們都是iOS系統(tǒng)中獨(dú)立的進(jìn)程翠肘,有著自己的內(nèi)存空間檐束。
線程:
進(jìn)程內(nèi)部執(zhí)行任務(wù)所需要的執(zhí)行路徑。進(jìn)程若想執(zhí)行任務(wù)束倍,則必須得在線程下執(zhí)行被丧。也就是說進(jìn)程至少有一個線程才能執(zhí)行任務(wù)。但是绪妹,我們使用軟件的時候甥桂,很少有只讓它做一件事的時候:
舉個印象筆記的?? : 當(dāng)你正在編輯一則筆記的時候點(diǎn)擊了同步按鈕,那么編輯任務(wù)(線程)和同步任務(wù)(線程)一定是不能按照順序執(zhí)行的邮旷。因?yàn)橥饺蝿?wù)的完成時間是不可控的黄选,如果在同步的過程中無法進(jìn)行別的任務(wù)(線程)那就太糟糕了!
因此婶肩,我們需要讓一些任務(wù)可以同時進(jìn)行办陷。既然任務(wù)是在線程上執(zhí)行的,那么多任務(wù)的執(zhí)行就意味著需要多線程的開啟和使用律歼。
來一張圖直觀地展示一下內(nèi)存民镜,進(jìn)程和線程的關(guān)系:
多線程概述
多線程的實(shí)現(xiàn)原理:雖然在同一時刻,CPU只能處理1條線程险毁,但是CPU可以快速地在多條線程之間調(diào)度(切換)制圈,造成了多線程并發(fā)執(zhí)行的假象。
能適當(dāng)提高程序的執(zhí)行效率畔况。
能適當(dāng)提高資源利用率(CPU鲸鹦、內(nèi)存利用率)。
創(chuàng)建線程是需要成本的:iOS下主要成本包括:在楑喂颍空間的子線程512KB馋嗜、主線程1MB,創(chuàng)建線程大約需要90毫秒的創(chuàng)建時間域庇。
線程越多嵌戈,CPU在調(diào)度線程上的開銷就越大覆积。
線程越多听皿,程序設(shè)計(jì)就越復(fù)雜:因?yàn)橐紤]到線程之間的通信熟呛,多線程的數(shù)據(jù)共享。
多線程在iOS中應(yīng)用
1. iOS的主線程
一個iOS程序運(yùn)行后尉姨,默認(rèn)會開啟1條線程庵朝,稱為“主線程”或“UI線程”
1>顯示\刷新UI界面;
2>處理UI事件(比如點(diǎn)擊事件、滾動事件又厉、拖拽事件等)
主線程的使用注意事項(xiàng):
不能把比較耗時的操作放到主線程中九府,嚴(yán)重影響UI的流暢度,給用戶一種程序“卡頓”的體驗(yàn)覆致。因此侄旬,要將耗時的操作放在子線程中異步執(zhí)行。這樣一來煌妈,及時開始執(zhí)行了耗時的操作儡羔,也不會影響主線程中UI交互的體驗(yàn)。
2. iOS子線程
子線程是異步執(zhí)行的璧诵,不影響主線程汰蜘。在iOS開發(fā)中,我們需要將耗時的任務(wù)(網(wǎng)絡(luò)請求之宿,復(fù)雜的運(yùn)算)放在子線程進(jìn)行族操,不讓其影響UI的交互體驗(yàn)。
3.多線程安全
當(dāng)多個線程訪問同一塊資源時比被,很容易引發(fā)數(shù)據(jù)錯亂和數(shù)據(jù)安全問題色难。就好比好幾個人在同時修改同一個表格,造成數(shù)據(jù)的錯亂等缀。
我們需要給數(shù)據(jù)添加互斥鎖枷莉。也就是說,當(dāng)某線程訪問一個數(shù)據(jù)之前就要給數(shù)據(jù)加鎖项滑,讓其不被其他的線程所修改依沮。就好比一個人修改表格的時候給表格設(shè)置了密碼,那么其他人就無法訪問文件了枪狂。當(dāng)他修改文件之后危喉,再講密碼撤銷,第二個人就可以訪問該文件了州疾。
注意:這里的線程都為子線程辜限,如果給數(shù)據(jù)加了鎖,就等于將這些異步的子線程變成同步的了严蓖,這也叫做線程同步技術(shù)薄嫡。
@synchronized(鎖對象) {// 需要鎖定的代碼? };
優(yōu)點(diǎn):能有效防止因多線程搶奪資源造成的數(shù)據(jù)安全問題
缺點(diǎn):需要消耗大量的CPU資源
互斥鎖的使用前提:多條線程搶奪同一塊資源的時候使用氧急。
OC在定義屬性時有nonatomic和atomic兩種選擇
atomic:原子屬性,為setter方法加鎖(默認(rèn)就是atomic)
nonatomic:非原子屬性毫深,不會為setter方法加鎖
atomic:線程安全吩坝,需要消耗大量的資源
nonatomic:非線程安全,適合內(nèi)存小的移動設(shè)備
建議:?所有屬性都聲明為nonatomic哑蔫,盡量避免多線程搶奪同一塊資源钉寝,將加鎖、資源搶奪的業(yè)務(wù)邏輯交給服務(wù)器端處理闸迷,減小移動客戶端的壓力嵌纲。
多線程在iOS 中應(yīng)用-GCD
GCD,全稱為 Grand Central Dispatch 腥沽,是iOS用來管理線程的技術(shù)逮走。 純C語言,提供了非常多強(qiáng)大的函數(shù)今阳。
GCD會自動利用更多的CPU內(nèi)核(比如雙核师溅、四核)。
GCD會自動管理線程的生命周期(創(chuàng)建線程酣栈、調(diào)度任務(wù)险胰、銷毀線程)。
程序員只需要告訴GCD想要執(zhí)行什么任務(wù)矿筝,不需要編寫任何線程管理代碼起便。
為了要提高軟件性能窖维,應(yīng)該異步執(zhí)行耗時任務(wù)(加載圖片)榆综,以防止影響主線程任務(wù)的執(zhí)行(UI相應(yīng))。舉個?? :
從網(wǎng)絡(luò)加載一張圖片铸史,如果將此任務(wù)放到主線程鼻疮,那么在下載完成的時間里,軟件是無法相應(yīng)用戶的任何操作的琳轿。特別地判沟,如果當(dāng)前是在可以滾動的頁面,就會造成無法滾動這種體驗(yàn)非常糟的情況崭篡。
所以:應(yīng)該將網(wǎng)絡(luò)加載放在異步執(zhí)行挪哄,執(zhí)行成功后,再回到主線程顯示加載后的圖片(詳細(xì)做法馬上就會講到)琉闪。
由開發(fā)者定制將要執(zhí)行的任務(wù)迹炼。
將任務(wù)添加到隊(duì)列中,GCD會自動將隊(duì)列中的任務(wù)取出,放到對應(yīng)的線程中執(zhí)行斯入。
注意:任務(wù)的取出遵循隊(duì)列的FIFO原則:先進(jìn)先出砂碉,后進(jìn)后出。
隊(duì)列是用來存放任務(wù)的增蹭,由GCD將這些任務(wù)從隊(duì)列中取出并放到相應(yīng)的線程中執(zhí)行。
1>.并發(fā)隊(duì)列(Concurrent Dispatch Queue)
可以讓多個任務(wù)并發(fā)(同時)執(zhí)行(自動開啟多個線程同時執(zhí)行任務(wù))闹伪,并發(fā)功能只有在異步(dispatch_async)函數(shù)下才有效
2>. 串行隊(duì)列(Serial Dispatch Queue)
讓任務(wù)一個接著一個地執(zhí)行(一個任務(wù)執(zhí)行完畢后沪铭,再執(zhí)行下一個任務(wù))壮池。
那么隊(duì)列和線程又有什么區(qū)別偏瓤?
簡單來說,隊(duì)列就是用來存放任務(wù)的“暫存區(qū)”椰憋,而線程是執(zhí)行任務(wù)的路徑厅克,GCD將這些存在于隊(duì)列的任務(wù)取出來放到相應(yīng)的線程上去執(zhí)行,而隊(duì)列的性質(zhì)決定了在其中的任務(wù)在哪種線程上執(zhí)行橙依。
下面由一張圖來直觀地展示任務(wù)证舟,隊(duì)列和線程的關(guān)系:
在這里,我們可以看到窗骑,放入串行隊(duì)列的任務(wù)會一個一個地執(zhí)行女责。而放入并行隊(duì)列的任務(wù),會在多個線程并發(fā)地執(zhí)行创译。
GCD中獲得串行有2種途徑:
1>.使用dispatch_queue_create函數(shù)創(chuàng)建串行隊(duì)列
// 創(chuàng)建串行隊(duì)列(隊(duì)列類型傳遞NULL或者DISPATCH_QUEUE_SERIAL)
dispatch_queue_tqueue = dispatch_queue_create("serial_queue",NULL);
2>.使用主隊(duì)列(跟主線程相關(guān)聯(lián)的隊(duì)列)
主隊(duì)列是GCD自帶的一種特殊的串行隊(duì)列:放在主隊(duì)列中的任務(wù)抵知,都會放到主線程中執(zhí)行。
可以使用dispatch_get_main_queue()獲得系統(tǒng)提供的主隊(duì)列:
dispatch_queue_tqueue = dispatch_get_main_queue();
1>.使用dispatch_queue_create函數(shù)創(chuàng)建并發(fā)隊(duì)列软族。
dispatch_queue_tqueue = dispatch_queue_create("concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
2>.使用dispatch_get_global_queue獲得全局并發(fā)隊(duì)列刷喜。
GCD默認(rèn)已經(jīng)提供了全局的并發(fā)隊(duì)列,供整個應(yīng)用使用立砸,可以無需手動創(chuàng)建掖疮。
dispatch_queue_tqueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
需求點(diǎn):我們有時需要在子線程處理一個耗時比較長的任務(wù),而且此任務(wù)完成后颗祝,要在主線程執(zhí)行另一個任務(wù)浊闪。
例子:從網(wǎng)絡(luò)加載圖片(在子線程),加載完成就更新UIView(在主線程)螺戳。
為了實(shí)現(xiàn)這個需求搁宾,我們需要首先拿到全局并發(fā)隊(duì)列(或自己開啟一個子線程)來執(zhí)行耗時的操作,然后在其完成block中拿到全局串行隊(duì)列來執(zhí)行UI刷新的任務(wù)温峭。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
????????????//加載圖片
????????NSData*dataFromURL = [NSDatadataWithContentsOfURL:imageURL];
????????UIImage*imageFromData = [UIImageimageWithData:dataFromURL];
????????dispatch_async(dispatch_get_main_queue(), ^{
????????????//加載完成更新view
????????????UIImageView*imageView = [[UIImageViewalloc] initWithImage:imageFromData];
? ????? });? ? ?
});
以筆者的拙見猛铅,除了復(fù)雜的算法,網(wǎng)絡(luò)請求以外凤藏,大多數(shù)dataWithContentsOf奸忽。堕伪。。函數(shù)可能也會比較耗時栗菜,所以以后遇到與NSData交互的操作時欠雌,盡量將其放在子線程執(zhí)行。
需求點(diǎn):用于在程序啟動到終止疙筹,只執(zhí)行一次的代碼富俄。此代碼被執(zhí)行后,相當(dāng)于自身全部被加上了注釋而咆,不會再執(zhí)行了霍比。
為了實(shí)現(xiàn)這個需求,我們需要使用dispatch_once讓代碼在運(yùn)行一次后即刻被“雪藏”暴备。
//使用dispatch_once函數(shù)能保證某段代碼在程序運(yùn)行過程中只被執(zhí)行1次
staticdispatch_once_tonceToken;
dispatch_once(&onceToken, ^{
// 只執(zhí)行1次的代碼悠瞬,這里默認(rèn)是線程安全的:不會有其他線程可以訪問到這里
});
需求點(diǎn):執(zhí)行多個耗時的異步任務(wù),但是只能等到這些任務(wù)都執(zhí)行完畢后涯捻,才能在主線程執(zhí)行某個任務(wù)浅妆。
為了實(shí)現(xiàn)這個需求,我們需要讓將這些異步執(zhí)行的操作放在dispatch_group_async函數(shù)中執(zhí)行障癌,最后再調(diào)用dispatch_group_notify來執(zhí)行最后執(zhí)行的任務(wù)凌外。
dispatch_group_t group =? dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
// 執(zhí)行1個耗時的異步操作
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
// 執(zhí)行1個耗時的異步操作
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 等前面的異步操作都執(zhí)行完畢后,回到主線程...
});
讓我們看一下示例代碼和運(yùn)行結(jié)果:
示例代碼:
為了使對比明顯涛浙,筆者多開了幾條線程康辑,這樣更容易看清問題。
dispatch_group_t group =? dispatch_group_create(); ??
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
// 執(zhí)行1個耗時的異步操作
for(NSIntegerindex =0; index <10000; index ++) {
? ? }
NSLog(@"完成了任務(wù)1");
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
// 執(zhí)行1個耗時的異步操作
for(NSIntegerindex =0; index <20000; index ++) {
? ? }
NSLog(@"完成了任務(wù)2");
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
// 執(zhí)行1個耗時的異步操作
for(NSIntegerindex =0; index <200000; index ++) {
? ? }
NSLog(@"完成了任務(wù)3");
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
// 執(zhí)行1個耗時的異步操作
for(NSIntegerindex =0; index <400000; index ++) {
? ? }
NSLog(@"完成了任務(wù)4");
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
// 執(zhí)行1個耗時的異步操作
for(NSIntegerindex =0; index <1000000; index ++) {
? ? }
NSLog(@"完成了任務(wù)5");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 等前面的異步操作都執(zhí)行完畢后蝗拿,回到主線程...
NSLog(@"都完成了");
});
運(yùn)行結(jié)果:
從三次運(yùn)行的結(jié)果來看:
異步執(zhí)行的任務(wù)1-5的最終完成時間是與其自身完成任務(wù)所需要的時間并無絕對關(guān)聯(lián)晾捏。因?yàn)槿蝿?wù)5是最耗時的,它在第一次運(yùn)行結(jié)果里并不是最后才完成的哀托。任務(wù)1是最不耗時的惦辛,但是它在第二次運(yùn)行結(jié)果里也不是最先完成的。
異步執(zhí)行的任務(wù)1-5無論完成順序如何仓手,只有當(dāng)他們都完成后才會調(diào)用主線程的打印“都完成了”胖齐。
需求點(diǎn):雖然我們有時要執(zhí)行幾個不同的異步任務(wù),但是我們還是要將其分成兩組:當(dāng)?shù)谝唤M異步任務(wù)都執(zhí)行完成后才執(zhí)行第二組的異步任務(wù)嗽冒。這里的組可以包含一個任務(wù)呀伙,也可以包含多個任務(wù)。
為了實(shí)現(xiàn)這個需求添坊,我們需要使用dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);在兩組任務(wù)之間形成“柵欄”剿另,使其“下方”的異步任務(wù)在其“上方”的異步任務(wù)都完成之前是無法執(zhí)行的。
dispatch_queue_tqueue = dispatch_queue_create("12312312", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"----任務(wù) 1-----");
});
dispatch_async(queue, ^{
NSLog(@"----任務(wù) 2-----");
});? ?
dispatch_barrier_async(queue, ^{
NSLog(@"----barrier-----");
});
dispatch_async(queue, ^{
NSLog(@"----任務(wù) 3-----");
});
dispatch_async(queue, ^{
NSLog(@"----任務(wù) 4-----");
});