最近結(jié)合《Objective-C 高級(jí)編程》和一些文章來深入了解下多線程相關(guān)內(nèi)容。
進(jìn)程
指的是一個(gè)正在運(yùn)行中的可執(zhí)行文件。每一個(gè)進(jìn)程都擁有獨(dú)立的虛擬內(nèi)存空間和系統(tǒng)資源,包括端口權(quán)限等,且至少包含一個(gè)主線程和任意數(shù)量的輔助線程。另外,當(dāng)一個(gè)進(jìn)程的主線程退出時(shí),這個(gè)進(jìn)程就結(jié)束了。
線程
一個(gè)CPU執(zhí)行的CPU命令列為一條無分叉路徑即為線程
我們都知道我們寫的OC/Swift源碼最后會(huì)被編譯器轉(zhuǎn)換成相應(yīng)的CPU命令列,然后程序啟動(dòng)后操作系統(tǒng)會(huì)將包含在程序中的CPU命令列配置到內(nèi)存中。然后會(huì)從應(yīng)用程序制定的地址開始一個(gè)一個(gè)的執(zhí)行命令。雖然在遇到諸如if語句、for語句等控制語句或者函數(shù)調(diào)用的情況下,執(zhí)行命令列會(huì)進(jìn)行位置遷移。但是由于一個(gè)CPU一次只能處理一個(gè)指令,因此依然可以把CPU命令列看成一條無分叉的路徑,其執(zhí)行不會(huì)出現(xiàn)分叉。
當(dāng)這種無分叉的路徑存在多條時(shí)就是“多線程”。
多線程
雖然CPU相關(guān)的技術(shù)不斷進(jìn)步,但是基本上一個(gè)CPU核一次能執(zhí)行的命令始終為1。這時(shí)候要在多條路徑中執(zhí)行CPU命令列就需要進(jìn)行“上下文切換”。
將執(zhí)行中的路徑的狀態(tài),如CPU寄存器等信息保存到對(duì)應(yīng)路徑專用的內(nèi)存塊中,然后從將要切換到的目標(biāo)路徑對(duì)應(yīng)的內(nèi)存中復(fù)原CPU寄存器等信息,繼續(xù)執(zhí)行切換路徑的CPU命令列。這就稱為“上下文切換”。
來個(gè)比喻來說,比如CPU是個(gè)學(xué)生,他需要做英語和數(shù)學(xué)兩種作業(yè)。但是我們讓CPU寫一會(huì)兒英語作業(yè)后寫一會(huì)兒數(shù)學(xué)作業(yè),然后再寫一會(huì)兒英語作業(yè)。兩種作業(yè)快速切換就給人一種CPU在同時(shí)在寫英語和數(shù)學(xué)作業(yè)的感覺。
多線程的壞處
一般我們都覺得多線程多好啊,幾個(gè)線程一起執(zhí)行任務(wù),這樣肯定會(huì)提高速度。但是從上面多線程的介紹來看并不是這樣。(你要是讓小明去一邊寫英語一邊寫數(shù)學(xué),估計(jì)早就被打死了。。)當(dāng)在不同的線程中來回切換的時(shí)候會(huì)不停地備份、替換寄存器等信息,這明顯會(huì)耗費(fèi)性能。那多線程的用處是什么呢?這就要涉及幾個(gè)新的概念。
并發(fā)、并行、串行

Erlang 之父 Joe Armstrong 用一張圖解釋了并發(fā)與并行的區(qū)別。并發(fā)是兩隊(duì)交替使用一臺(tái)咖啡機(jī),并行則是兩個(gè)隊(duì)列同時(shí)使用兩臺(tái)咖啡機(jī)。串行則是一個(gè)隊(duì)列使用一臺(tái)咖啡機(jī)。(更詳細(xì)的說明可以看看知乎這些回答)
多線程就是采用了并行的技術(shù)來同時(shí)處理不同的指令。而在我們的程序中通過主線程來繪制界面,響應(yīng)用戶交互事件。如果在這個(gè)線程上進(jìn)行長(zhǎng)時(shí)間的處理,就會(huì)阻塞主線程的執(zhí)行,妨礙主線程中Runloop的住循環(huán),這樣就不能及時(shí)更新界面,響應(yīng)用戶的交互,這就給用戶卡頓的感覺。而使用多線程就能解決這個(gè)問題,給用戶“快”的感覺。所以多線程所謂能提高速度指的就是這個(gè)意思。
同步、異步
同步就是我們平常寫的那些代碼。它會(huì)一行接一行的執(zhí)行,每一行都可以看成是一個(gè)任務(wù),一個(gè)任務(wù)沒執(zhí)行完就不會(huì)執(zhí)行下一個(gè)任務(wù)。異步就是允許執(zhí)行一行的時(shí)候函數(shù)直接返回,真正要執(zhí)行的任務(wù)稍后完成。
對(duì)于同步執(zhí)行的任務(wù)來說系統(tǒng)傾向于在同一個(gè)線程中執(zhí)行。這是因?yàn)檫@個(gè)時(shí)候就算開了其他線程系統(tǒng)也要等他們?cè)诟髯跃€程中全執(zhí)行完成,這樣以來又增加了線程切換時(shí)的性能,得不償失。
對(duì)于異步執(zhí)行的任務(wù)來說系統(tǒng)傾向于在多個(gè)線程中執(zhí)行,這樣就可以更好的利用CPU性能,縮短完成任務(wù)的時(shí)間,提高效率。
隊(duì)列、線程
這兩個(gè)概念經(jīng)常被混淆,其實(shí)這兩個(gè)是不同層級(jí)的概念。隊(duì)列是為了方便使用和理解的抽象結(jié)構(gòu),線程則是系統(tǒng)級(jí)進(jìn)行運(yùn)算調(diào)度的單位。系統(tǒng)利用隊(duì)列來進(jìn)行任務(wù)調(diào)度,它會(huì)根據(jù)調(diào)度任務(wù)的需要和系統(tǒng)的負(fù)載等情況動(dòng)態(tài)的創(chuàng)建和銷毀線程。并行隊(duì)列可能對(duì)應(yīng)多個(gè)線程。串行隊(duì)列則每次對(duì)應(yīng)一個(gè)線程,這個(gè)線程可能不變,可能會(huì)被更換。
蘋果官方對(duì)GCD的說明
Grand Central Dispatch是異步執(zhí)行任務(wù)的技術(shù)之一。開發(fā)者要做的只是定義想要執(zhí)行的任務(wù)并追加到適當(dāng)?shù)腝ueue中,GCD就能生成必要的線程并計(jì)劃計(jì)劃任務(wù)。
可以看出我們需要注意的兩點(diǎn)就是Queue和添加任務(wù)到Queue,在GCD中對(duì)應(yīng)的就是Dispatch_Queue(隊(duì)列)和dispatch_async、dispatch_sync( 執(zhí)行方式)。
隊(duì)列
串行隊(duì)列(Serial Dispatch Queue)遵循先進(jìn)先出規(guī)則,每一個(gè)任務(wù)都會(huì)等待它上個(gè)任務(wù)處理完成后執(zhí)行,因此每次只執(zhí)行一個(gè)任務(wù)。主隊(duì)列是一種特殊的串行隊(duì)列。是在主線程中執(zhí)行的隊(duì)列。
并行隊(duì)列(Concurrent Dispatch Queue)依然遵循先進(jìn)先出,不過每個(gè)任務(wù)不會(huì)等其他任務(wù)處理結(jié)束后再執(zhí)行,而是在其他任務(wù)開始執(zhí)行后就開始執(zhí)行,這樣就實(shí)現(xiàn)的多個(gè)任務(wù)并行。
舉個(gè)例子:現(xiàn)在有三個(gè)任務(wù)blk1,blk2,blk3。
在串行隊(duì)列里會(huì)先執(zhí)行blk1,等blk1執(zhí)行完之后執(zhí)行blk2,然后等blk2結(jié)束后再執(zhí)行blk3。
在并行隊(duì)列里會(huì)先執(zhí)行blk1,不用等blk1的處理結(jié)束就開始執(zhí)行blk2,這時(shí)候也不用等待blk1,blk2的執(zhí)行結(jié)束直接執(zhí)行blk3。
// 串行隊(duì)列的創(chuàng)建
dispatch_queue_t serial = dispatch_queue_create("com.zhouke.serial", NULL);
// 獲取主隊(duì)列(這是個(gè)串行隊(duì)列)
dispatch_queue_t mainSerial = dispatch_get_main_queue();
// 創(chuàng)建并行隊(duì)列
dispatch_queue_t concurrentCreated = dispatch_queue_create("com.zhouke.concurrent", DISPATCH_QUEUE_CONCURRENT);
// 獲取默認(rèn)優(yōu)先級(jí)的并行隊(duì)列
dispatch_queue_t concurrentGet = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
執(zhí)行方式
同步執(zhí)行(dispatch_sync)這個(gè)函數(shù)就是把指定的block同步添加到指定的隊(duì)列中,在這個(gè)block執(zhí)行結(jié)束之前,函數(shù)會(huì)一直等待。(了解這個(gè)死鎖就很容易理解了)
異步執(zhí)行(dispatch_sync)這個(gè)函數(shù)會(huì)將指定的block非同步的添加到指定隊(duì)列中,函數(shù)不做等待。
一般來說同步方法會(huì)在當(dāng)前線程執(zhí)行,異步方法會(huì)開啟新的線程。但是對(duì)于主隊(duì)列來說就有點(diǎn)特殊了。在主隊(duì)列執(zhí)行同步方法會(huì)產(chǎn)生死鎖,執(zhí)行異步方法不會(huì)開啟新的線程,依然在主線程執(zhí)行。
線程的開辟
- (串行/并行)隊(duì)列決定任務(wù)是否在當(dāng)前線程(注意不是隊(duì)列)執(zhí)行。
- (同步/異步)任務(wù)決定任務(wù)立即執(zhí)行(阻塞線程)還是添加到隊(duì)列末尾(不阻塞線程)。
由上可知會(huì)開辟新線程的兩種情況:
- 并行隊(duì)列+異步任務(wù) = 多條新線程
- 串行隊(duì)列+異步任務(wù) = 一條新線程
其余的情況下都不會(huì)開辟線程。
死鎖
如果向當(dāng)前串行隊(duì)列同步派發(fā)任務(wù)就會(huì)產(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]);
});
}
這段代碼就會(huì)會(huì)造成死鎖。我們可以把dispatch_sync這個(gè)函數(shù)當(dāng)做一個(gè)任務(wù)A,block里包裝的是另一個(gè)任務(wù)B。然后我們可以看到A處于主隊(duì)列中,這時(shí)同步添加任務(wù)B到主隊(duì)列中。任務(wù)A會(huì)等待B任務(wù)完成,但是由于當(dāng)前主隊(duì)列是串行隊(duì)列,這個(gè)新增的B任務(wù)要等到A任務(wù)執(zhí)行完才能執(zhí)行,這樣就造成了兩個(gè)任務(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]);
});
}
上面這段代碼就不會(huì)照成死鎖,這是因?yàn)閐ispatch_sync這個(gè)函數(shù)處于主隊(duì)列中,但是block包裝的任務(wù)處于queue這個(gè)串行隊(duì)列中,兩者在不同的串行隊(duì)列,因此不會(huì)死鎖。
- (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]);
});
});
}
這段代碼依然會(huì)死鎖,原因跟第一段代碼一樣。dispatch_async函數(shù)可以看成是把block里的任務(wù)放到queue中執(zhí)行,這時(shí)dispatch_sync處于queue這個(gè)隊(duì)列中,它的block包裝的任務(wù)依然處于queue隊(duì)列中,因此會(huì)死鎖。
dispatch_group
在串行隊(duì)列中如果想在全部任務(wù)結(jié)束后再做些操作是很好處理的,但是對(duì)于并行隊(duì)列就不一樣了,這時(shí)候我們就需要使用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í)行完成這個(gè)任務(wù)就會(huì)在任務(wù)1、2、3全部執(zhí)行后調(diào)用。
dispatch_barrier_async
當(dāng)多個(gè)線程同時(shí)更新資源的時(shí)候會(huì)造成數(shù)據(jù)競(jìng)爭(zhēng),這時(shí)候我們需要使用dispatch_barrier_async。
比如我們經(jīng)常會(huì)碰到的一個(gè)問題,atomic修飾的屬性一定是安全的嗎?
答案是否定的,atomic只保證了針對(duì)這個(gè)屬性的成員變量的讀寫的原子性,而在同一線程中多次獲取屬性時(shí),每次獲取的結(jié)果卻未必相同,因?yàn)閮纱潍@取操作之間其他線程可能會(huì)寫入新值。使用串行隊(duì)列可以解決這個(gè)問題,將所有的讀取寫入操作都放到串行隊(duì)列中,這樣就能保證線程安全了。更好的更高性能的解決辦法就是利用 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;
});
}
當(dāng)上面的執(zhí)行時(shí)屬性的讀取操作并發(fā)執(zhí)行,而寫入操作必須單獨(dú)執(zhí)行。

需要注意的是如果我們調(diào)用dispatch_barrier_async時(shí)提交到一個(gè)global queue,barrier blocks執(zhí)行效果與dispatch_async()一致;只有將Barrier blocks提交到使用DISPATCH_QUEUE_CONCURRENT屬性創(chuàng)建的并行queue時(shí)它才會(huì)表現(xiàn)的如同預(yù)期。
dispatch_semaphore
dispatch_barrier_async能在任務(wù)這種粒度上來防止數(shù)據(jù)競(jìng)爭(zhēng),當(dāng)我們需要更細(xì)粒度控制的時(shí)候就需要使用dispatch_semaphore。
首先介紹一下信號(hào)量(semaphore)的概念。信號(hào)量是持有計(jì)數(shù)的信號(hào),不過這么解釋等于沒解釋。我們舉個(gè)生活中的例子來看看。
假設(shè)有一個(gè)房子,它對(duì)應(yīng)進(jìn)程的概念,房子里的人就對(duì)應(yīng)著線程。一個(gè)進(jìn)程可以包括多個(gè)線程。這個(gè)房子(進(jìn)程)有很多資源,比如花園、客廳等,是所有人(線程)共享的。
但是有些地方,比如臥室,最多只有兩個(gè)人能進(jìn)去睡覺。怎么辦呢,在臥室門口掛上兩把鑰匙。進(jìn)去的人(線程)拿著鑰匙進(jìn)去,沒有鑰匙就不能進(jìn)去,出來的時(shí)候把鑰匙放回門口。
這時(shí)候,門口的鑰匙數(shù)量就稱為信號(hào)量(Semaphore)。很明顯,信號(hào)量為0時(shí)需要等待,信號(hào)量不為零時(shí),減去1而且不等待。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
/*
* 生成dispatch_semaphore,其初始值設(shè)置為1
* 保證訪問array的線程在同一時(shí)間只有一個(gè)
*/
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i < 1000; ++i) {
dispatch_async(queue, ^{
/*
某個(gè)線程執(zhí)行到這里,如果信號(hào)量值為1,那么wait方法返回1,開始執(zhí)行接下來的操作。
與此同時(shí),因?yàn)樾盘?hào)量變?yōu)?,其它執(zhí)行到這里的線程都必須等待
*/
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
/*
執(zhí)行了wait方法后,信號(hào)量的值變成了0??梢赃M(jìn)行接下來的操作。
這時(shí)候其它線程都得等待wait方法返回。
可以對(duì)array修改的線程在任意時(shí)刻都只有一個(gè),可以安全的修改array
*/
[array addObject:[NSNumber numberWithInt:i]];
/*
排他操作執(zhí)行結(jié)束,記得要調(diào)用signal方法,把信號(hào)量的值加1。
這樣,如果有別的線程在等待wait函數(shù)返回,就由最先等待的線程執(zhí)行。
*/
dispatch_semaphore_signal(semaphore);
});
}
具體使用的例子
1、控制并發(fā)數(shù)
// 創(chuàng)建隊(duì)列組
dispatch_group_t group = dispatch_group_create();
// 創(chuàng)建信號(hào)量,并且設(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í)行就會(huì)執(zhí)行dispatch_semaphore_wait,從而semaphore-1.當(dāng)循環(huán)10此后,semaphore等于0,則會(huì)阻塞線程,直到執(zhí)行了Block的dispatch_semaphore_signal 才會(huì)繼續(xù)執(zhí)行
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_group_async(group, queue, ^{
NSLog(@"%i",i);
sleep(2);
// 每次發(fā)送信號(hào)則semaphore會(huì)+1,
dispatch_semaphore_signal(semaphore);
});
}
2、限制請(qǐng)求頻次
每次請(qǐng)求發(fā)出后由于信號(hào)量0則其他線程必須等待,只有等請(qǐng)求返回成功或者失敗后信號(hào)量設(shè)為1,這時(shí)候才能繼續(xù)其他的網(wǎng)絡(luò)請(qǐng)求。
- (void)request1{
//創(chuàng)建信號(hào)量并設(shè)置計(jì)數(shù)默認(rèn)為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(@"請(qǐng)求1---%@",dic[@"id"]);
}
//計(jì)數(shù)加1
dispatch_semaphore_signal(semaphore);
//11380-- data.lastObject[@"id"];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"shibai...");
//計(jì)數(shù)加1
dispatch_semaphore_signal(semaphore);
}];
//若計(jì)數(shù)為0則一直等待
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
參考資料
1、《Objective-C高級(jí)編程 iOS與OS X多線程和內(nèi)存管理》
2、《Effective Objective-C 2.0 編寫高質(zhì)量iOS與OS X代碼的52個(gè)有效方法》
2、 iOS多線程編程總結(jié)
3、http://www.itdecent.cn/p/96b93aa05bcd