最近結(jié)合《Objective-C 高級編程》和一些文章來深入了解下多線程相關(guān)內(nèi)容筋遭。
進程
指的是一個正在運行中的可執(zhí)行文件簇秒。每一個進程都擁有獨立的虛擬內(nèi)存空間和系統(tǒng)資源肩杈,包括端口權(quán)限等径玖,且至少包含一個主線程和任意數(shù)量的輔助線程瘪贱。另外扬卷,當一個進程的主線程退出時牙言,這個進程就結(jié)束了。
線程
一個CPU執(zhí)行的CPU命令列為一條無分叉路徑即為線程
我們都知道我們寫的OC/Swift源碼最后會被編譯器轉(zhuǎn)換成相應(yīng)的CPU命令列怪得,然后程序啟動后操作系統(tǒng)會將包含在程序中的CPU命令列配置到內(nèi)存中咱枉。然后會從應(yīng)用程序制定的地址開始一個一個的執(zhí)行命令。雖然在遇到諸如if語句徒恋、for語句等控制語句或者函數(shù)調(diào)用的情況下蚕断,執(zhí)行命令列會進行位置遷移。但是由于一個CPU一次只能處理一個指令入挣,因此依然可以把CPU命令列看成一條無分叉的路徑亿乳,其執(zhí)行不會出現(xiàn)分叉。
當這種無分叉的路徑存在多條時就是“多線程”径筏。
多線程
雖然CPU相關(guān)的技術(shù)不斷進步葛假,但是基本上一個CPU核一次能執(zhí)行的命令始終為1。這時候要在多條路徑中執(zhí)行CPU命令列就需要進行“上下文切換”滋恬。
將執(zhí)行中的路徑的狀態(tài)聊训,如CPU寄存器等信息保存到對應(yīng)路徑專用的內(nèi)存塊中,然后從將要切換到的目標路徑對應(yīng)的內(nèi)存中復(fù)原CPU寄存器等信息恢氯,繼續(xù)執(zhí)行切換路徑的CPU命令列带斑。這就稱為“上下文切換”。
來個比喻來說勋拟,比如CPU是個學(xué)生遏暴,他需要做英語和數(shù)學(xué)兩種作業(yè)。但是我們讓CPU寫一會兒英語作業(yè)后寫一會兒數(shù)學(xué)作業(yè)指黎,然后再寫一會兒英語作業(yè)朋凉。兩種作業(yè)快速切換就給人一種CPU在同時在寫英語和數(shù)學(xué)作業(yè)的感覺。
多線程的壞處
一般我們都覺得多線程多好啊醋安,幾個線程一起執(zhí)行任務(wù)杂彭,這樣肯定會提高速度墓毒。但是從上面多線程的介紹來看并不是這樣。(你要是讓小明去一邊寫英語一邊寫數(shù)學(xué)亲怠,估計早就被打死了所计。。)當在不同的線程中來回切換的時候會不停地備份团秽、替換寄存器等信息主胧,這明顯會耗費性能。那多線程的用處是什么呢习勤?這就要涉及幾個新的概念踪栋。
并發(fā)、并行图毕、串行
Erlang 之父 Joe Armstrong 用一張圖解釋了并發(fā)與并行的區(qū)別夷都。并發(fā)是兩隊交替使用一臺咖啡機,并行則是兩個隊列同時使用兩臺咖啡機予颤。串行則是一個隊列使用一臺咖啡機囤官。(更詳細的說明可以看看知乎這些回答)
多線程就是采用了并行的技術(shù)來同時處理不同的指令。而在我們的程序中通過主線程來繪制界面蛤虐,響應(yīng)用戶交互事件党饮。如果在這個線程上進行長時間的處理,就會阻塞主線程的執(zhí)行驳庭,妨礙主線程中Runloop的住循環(huán)刑顺,這樣就不能及時更新界面,響應(yīng)用戶的交互嚷掠,這就給用戶卡頓的感覺。而使用多線程就能解決這個問題荞驴,給用戶“快”的感覺不皆。所以多線程所謂能提高速度指的就是這個意思。
同步熊楼、異步
同步就是我們平常寫的那些代碼霹娄。它會一行接一行的執(zhí)行,每一行都可以看成是一個任務(wù)鲫骗,一個任務(wù)沒執(zhí)行完就不會執(zhí)行下一個任務(wù)犬耻。異步就是允許執(zhí)行一行的時候函數(shù)直接返回,真正要執(zhí)行的任務(wù)稍后完成执泰。
對于同步執(zhí)行的任務(wù)來說系統(tǒng)傾向于在同一個線程中執(zhí)行枕磁。這是因為這個時候就算開了其他線程系統(tǒng)也要等他們在各自線程中全執(zhí)行完成,這樣以來又增加了線程切換時的性能术吝,得不償失计济。
對于異步執(zhí)行的任務(wù)來說系統(tǒng)傾向于在多個線程中執(zhí)行茸苇,這樣就可以更好的利用CPU性能,縮短完成任務(wù)的時間沦寂,提高效率学密。
隊列、線程
這兩個概念經(jīng)常被混淆传藏,其實這兩個是不同層級的概念腻暮。隊列是為了方便使用和理解的抽象結(jié)構(gòu),線程則是系統(tǒng)級進行運算調(diào)度的單位毯侦。系統(tǒng)利用隊列來進行任務(wù)調(diào)度哭靖,它會根據(jù)調(diào)度任務(wù)的需要和系統(tǒng)的負載等情況動態(tài)的創(chuàng)建和銷毀線程。并行隊列可能對應(yīng)多個線程叫惊。串行隊列則每次對應(yīng)一個線程款青,這個線程可能不變,可能會被更換霍狰。
蘋果官方對GCD的說明
Grand Central Dispatch是異步執(zhí)行任務(wù)的技術(shù)之一抡草。開發(fā)者要做的只是定義想要執(zhí)行的任務(wù)并追加到適當?shù)腝ueue中,GCD就能生成必要的線程并計劃計劃任務(wù)蔗坯。
可以看出我們需要注意的兩點就是Queue和添加任務(wù)到Queue康震,在GCD中對應(yīng)的就是Dispatch_Queue(隊列)和dispatch_async、dispatch_sync( 執(zhí)行方式)宾濒。
隊列
串行隊列(Serial Dispatch Queue)遵循先進先出規(guī)則腿短,每一個任務(wù)都會等待它上個任務(wù)處理完成后執(zhí)行,因此每次只執(zhí)行一個任務(wù)绘梦。主隊列是一種特殊的串行隊列橘忱。是在主線程中執(zhí)行的隊列。
并行隊列(Concurrent Dispatch Queue)依然遵循先進先出卸奉,不過每個任務(wù)不會等其他任務(wù)處理結(jié)束后再執(zhí)行钝诚,而是在其他任務(wù)開始執(zhí)行后就開始執(zhí)行,這樣就實現(xiàn)的多個任務(wù)并行榄棵。
舉個例子:現(xiàn)在有三個任務(wù)blk1凝颇,blk2,blk3疹鳄。
在串行隊列里會先執(zhí)行blk1拧略,等blk1執(zhí)行完之后執(zhí)行blk2,然后等blk2結(jié)束后再執(zhí)行blk3瘪弓。
在并行隊列里會先執(zhí)行blk1垫蛆,不用等blk1的處理結(jié)束就開始執(zhí)行blk2,這時候也不用等待blk1,blk2的執(zhí)行結(jié)束直接執(zhí)行blk3月褥。
// 串行隊列的創(chuàng)建
dispatch_queue_t serial = dispatch_queue_create("com.zhouke.serial", NULL);
// 獲取主隊列(這是個串行隊列)
dispatch_queue_t mainSerial = dispatch_get_main_queue();
// 創(chuàng)建并行隊列
dispatch_queue_t concurrentCreated = dispatch_queue_create("com.zhouke.concurrent", DISPATCH_QUEUE_CONCURRENT);
// 獲取默認優(yōu)先級的并行隊列
dispatch_queue_t concurrentGet = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
執(zhí)行方式
同步執(zhí)行(dispatch_sync)這個函數(shù)就是把指定的block同步添加到指定的隊列中弛随,在這個block執(zhí)行結(jié)束之前,函數(shù)會一直等待宁赤。(了解這個死鎖就很容易理解了)
異步執(zhí)行(dispatch_sync)這個函數(shù)會將指定的block非同步的添加到指定隊列中舀透,函數(shù)不做等待。
一般來說同步方法會在當前線程執(zhí)行决左,異步方法會開啟新的線程愕够。但是對于主隊列來說就有點特殊了。在主隊列執(zhí)行同步方法會產(chǎn)生死鎖佛猛,執(zhí)行異步方法不會開啟新的線程惑芭,依然在主線程執(zhí)行。
線程的開辟
- (串行/并行)隊列決定任務(wù)是否在當前線程(注意不是隊列)執(zhí)行继找。
- (同步/異步)任務(wù)決定任務(wù)立即執(zhí)行(阻塞線程)還是添加到隊列末尾(不阻塞線程)遂跟。
由上可知會開辟新線程的兩種情況:
- 并行隊列+異步任務(wù) = 多條新線程
- 串行隊列+異步任務(wù) = 一條新線程
其余的情況下都不會開辟線程。
死鎖
如果向當前串行隊列同步派發(fā)任務(wù)就會產(chǎn)生死鎖
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1.%@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"2.%@", [NSThread currentThread]);
});
}
這段代碼就會會造成死鎖婴渡。我們可以把dispatch_sync這個函數(shù)當做一個任務(wù)A幻锁,block里包裝的是另一個任務(wù)B。然后我們可以看到A處于主隊列中边臼,這時同步添加任務(wù)B到主隊列中哄尔。任務(wù)A會等待B任務(wù)完成,但是由于當前主隊列是串行隊列柠并,這個新增的B任務(wù)要等到A任務(wù)執(zhí)行完才能執(zhí)行岭接,這樣就造成了兩個任務(wù)互相等待,導(dǎo)致死鎖臼予。
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1.%@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("com.xxx.xxx", NULL);
dispatch_sync(queue, ^{
NSLog(@"2.%@", [NSThread currentThread]);
});
}
上面這段代碼就不會照成死鎖鸣戴,這是因為dispatch_sync這個函數(shù)處于主隊列中,但是block包裝的任務(wù)處于queue這個串行隊列中,兩者在不同的串行隊列粘拾,因此不會死鎖窄锅。
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"1.%@", [NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("com.xxx.xxx", NULL);
dispatch_async(queue, ^{
NSLog(@"2.%@", [NSThread currentThread]);
dispatch_sync(queue, ^{
NSLog(@"3.%@", [NSThread currentThread]);
});
});
}
這段代碼依然會死鎖,原因跟第一段代碼一樣半哟。dispatch_async函數(shù)可以看成是把block里的任務(wù)放到queue中執(zhí)行酬滤,這時dispatch_sync處于queue這個隊列中签餐,它的block包裝的任務(wù)依然處于queue隊列中寓涨,因此會死鎖。
dispatch_group
在串行隊列中如果想在全部任務(wù)結(jié)束后再做些操作是很好處理的氯檐,但是對于并行隊列就不一樣了戒良,這時候我們就需要使用Dispatch Group.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
NSLog(@"任務(wù)1執(zhí)行");
});
dispatch_group_async(group, queue, ^{
NSLog(@"任務(wù)2執(zhí)行");
});
dispatch_group_async(group, queue, ^{
NSLog(@"任務(wù)3執(zhí)行");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"全部執(zhí)行完成");
});
這段代碼中全部執(zhí)行完成這個任務(wù)就會在任務(wù)1、2冠摄、3全部執(zhí)行后調(diào)用糯崎。
dispatch_barrier_async
當多個線程同時更新資源的時候會造成數(shù)據(jù)競爭几缭,這時候我們需要使用dispatch_barrier_async。
比如我們經(jīng)常會碰到的一個問題沃呢,atomic修飾的屬性一定是安全的嗎年栓?
答案是否定的,atomic只保證了針對這個屬性的成員變量的讀寫的原子性薄霜,而在同一線程中多次獲取屬性時某抓,每次獲取的結(jié)果卻未必相同,因為兩次獲取操作之間其他線程可能會寫入新值惰瓜。使用串行隊列可以解決這個問題,將所有的讀取寫入操作都放到串行隊列中否副,這樣就能保證線程安全了。更好的更高性能的解決辦法就是利用 dispatch_barrier_async
_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- (NSString *)name
{
__block NSString *localString;
dispatch_sync(_queue, ^{
localString = _name;
});
return localString;
}
- (void)setName:(NSString *)name
{
dispatch_barrier_async(_queue, ^{
_name = name;
});
}
當上面的執(zhí)行時屬性的讀取操作并發(fā)執(zhí)行崎坊,而寫入操作必須單獨執(zhí)行备禀。
需要注意的是如果我們調(diào)用dispatch_barrier_async時提交到一個global queue,barrier blocks執(zhí)行效果與dispatch_async()一致奈揍;只有將Barrier blocks提交到使用DISPATCH_QUEUE_CONCURRENT屬性創(chuàng)建的并行queue時它才會表現(xiàn)的如同預(yù)期曲尸。
dispatch_semaphore
dispatch_barrier_async能在任務(wù)這種粒度上來防止數(shù)據(jù)競爭,當我們需要更細粒度控制的時候就需要使用dispatch_semaphore打月。
首先介紹一下信號量(semaphore)的概念队腐。信號量是持有計數(shù)的信號,不過這么解釋等于沒解釋奏篙。我們舉個生活中的例子來看看柴淘。
假設(shè)有一個房子,它對應(yīng)進程的概念秘通,房子里的人就對應(yīng)著線程为严。一個進程可以包括多個線程。這個房子(進程)有很多資源肺稀,比如花園第股、客廳等,是所有人(線程)共享的话原。
但是有些地方夕吻,比如臥室,最多只有兩個人能進去睡覺繁仁。怎么辦呢涉馅,在臥室門口掛上兩把鑰匙。進去的人(線程)拿著鑰匙進去黄虱,沒有鑰匙就不能進去稚矿,出來的時候把鑰匙放回門口。
這時候,門口的鑰匙數(shù)量就稱為信號量(Semaphore)晤揣。很明顯桥爽,信號量為0時需要等待,信號量不為零時昧识,減去1而且不等待钠四。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/*
* 生成dispatch_semaphore,其初始值設(shè)置為1
* 保證訪問array的線程在同一時間只有一個
*/
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i < 1000; ++i) {
dispatch_async(queue, ^{
/*
某個線程執(zhí)行到這里跪楞,如果信號量值為1形导,那么wait方法返回1,開始執(zhí)行接下來的操作习霹。
與此同時朵耕,因為信號量變?yōu)?,其它執(zhí)行到這里的線程都必須等待
*/
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
/*
執(zhí)行了wait方法后淋叶,信號量的值變成了0阎曹。可以進行接下來的操作煞檩。
這時候其它線程都得等待wait方法返回处嫌。
可以對array修改的線程在任意時刻都只有一個,可以安全的修改array
*/
[array addObject:[NSNumber numberWithInt:i]];
/*
排他操作執(zhí)行結(jié)束,記得要調(diào)用signal方法,把信號量的值加1伦腐。
這樣塞赂,如果有別的線程在等待wait函數(shù)返回玩般,就由最先等待的線程執(zhí)行。
*/
dispatch_semaphore_signal(semaphore);
});
}
具體使用的例子
1、控制并發(fā)數(shù)
// 創(chuàng)建隊列組
dispatch_group_t group = dispatch_group_create();
// 創(chuàng)建信號量,并且設(shè)置值為10
dispatch_semaphore_t semaphore = dispatch_semaphore_create(10);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i < 100; i++)
{ // 由于是異步執(zhí)行的捆昏,所以每次循環(huán)Block里面的dispatch_semaphore_signal根本還沒有執(zhí)行就會執(zhí)行dispatch_semaphore_wait,從而semaphore-1.當循環(huán)10此后毙沾,semaphore等于0骗卜,則會阻塞線程,直到執(zhí)行了Block的dispatch_semaphore_signal 才會繼續(xù)執(zhí)行
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_group_async(group, queue, ^{
NSLog(@"%i",i);
sleep(2);
// 每次發(fā)送信號則semaphore會+1左胞,
dispatch_semaphore_signal(semaphore);
});
}
2寇仓、限制請求頻次
每次請求發(fā)出后由于信號量0則其他線程必須等待,只有等請求返回成功或者失敗后信號量設(shè)為1烤宙,這時候才能繼續(xù)其他的網(wǎng)絡(luò)請求遍烦。
- (void)request1{
//創(chuàng)建信號量并設(shè)置計數(shù)默認為0
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFJSONResponseSerializer serializer];
NSString *url = [NSString stringWithFormat:@"%s","http://v3.wufazhuce.com:8000/api/channel/movie/more/0?platform=ios&version=v4.0.1"];
[manager GET:url parameters:nil progress:^(NSProgress * _Nonnull uploadProgress) {
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSArray *data = responseObject[@"data"];
for (NSDictionary *dic in data) {
NSLog(@"請求1---%@",dic[@"id"]);
}
//計數(shù)加1
dispatch_semaphore_signal(semaphore);
//11380-- data.lastObject[@"id"];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"shibai...");
//計數(shù)加1
dispatch_semaphore_signal(semaphore);
}];
//若計數(shù)為0則一直等待
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
參考資料
1、《Objective-C高級編程 iOS與OS X多線程和內(nèi)存管理》
2门烂、《Effective Objective-C 2.0 編寫高質(zhì)量iOS與OS X代碼的52個有效方法》
2乳愉、 iOS多線程編程總結(jié)
3、http://www.reibang.com/p/96b93aa05bcd