一 進程和線程
進程
- 進程是一個具有一定獨立功能的程序關(guān)于某次數(shù)據(jù)集合的一次運行活動,它是操作系統(tǒng)分配資源的基本單元.
- 進程是指在系統(tǒng)中正在運行的一個應用程序,就是一段程序的執(zhí)行過程,可以理解為手機上的一個app.
- 每個進程之間是獨立的,每個進程均運行在其專用且受保護的內(nèi)存空間內(nèi),擁有獨立運行的全部資源.
線程
- 程序執(zhí)行流的最小單元,線程是進程中的一個實體.
- 一個進程要想執(zhí)行任務,必須有一條線程.應用程序啟動時,系統(tǒng)會默認開啟一條線程,也就是主線程.
線程和進程的關(guān)系
- 線程是進程的執(zhí)行單元,進程所有的任務都在線程中執(zhí)行的.
- 線程是CPU分配資源和調(diào)度的最小單位.
- 一個程序可以對應多個進程,一個進程中可以有多個線程,但至少要有一個主線程.
- 同一個進程內(nèi)的線程共享資源.
二 多進程和多線程
多進程
打開MAC的活動監(jiān)視器,可以看到很多歌進程同事運行.
- 進程是程序在計算機上的一次執(zhí)行活動.當你運行一個程序,你就啟動了一個進程.顯然,程序 是死的(靜態(tài)的),進程是活的(動態(tài)的).
- 進程可以分為系統(tǒng)進程和用戶進程.凡是用于完成操作系統(tǒng)的各種功能的進程就是系統(tǒng)進程,他們就是處于運行狀態(tài)下的操作系統(tǒng)本身;所有由用戶啟動的進程都是用戶進程.進程是應用系統(tǒng)進行資源分配的單位.
- 在同一個時間內(nèi),同一個計算系統(tǒng)中如果允許兩個或兩個以上的進程處于運行狀態(tài),這便是多進程
多線程
同一時間,CPU只能處理一條線程.多線程并發(fā)執(zhí)行,其實是CPU快速的在多個線程之間調(diào)度.如果CPU調(diào)度線程的時間足夠快,就造成了多線程并發(fā)執(zhí)行的假象.如果線程非常多,CPU會在N個多線程之間調(diào)度,消耗大量的CPU資源,每條線程被執(zhí)行的頻次會被降低,造成線程的執(zhí)行率下降.
多線程的優(yōu)點
- 能適當?shù)奶岣叱绦虻膱?zhí)行效率;
- 能適當?shù)奶岣哔Y源利用率(CPU,內(nèi)存利用率);
多線程的缺點
- 開啟線程需要占用一定的內(nèi)存空間(默認情況下,主線程占用1M,子線程占用512KB),如果大量開啟線程,會占用大量的內(nèi)存空間,降低程序性能;
- 線程越多,CPU在調(diào)度線程上的開銷越大;
- 程序設計更加復雜:比如線程之間的通信,多線程的數(shù)據(jù)共享;
三 任務/隊列
任務
所執(zhí)行的操作,也就是在線程中執(zhí)行的那段代碼.在GCD中是放在block中的.執(zhí)行任務有兩種方式:同步執(zhí)行(sync)和異步執(zhí)行(async)
同步(sync):同步添加任務到指定的隊列中,在添加的任務執(zhí)行結(jié)束執(zhí)行之前會一直等待,知道隊列里的任務完成之后再繼續(xù)執(zhí)行,即會柱塞線程.只能在當前線程中執(zhí)行任務(不一定是主線程),不具備開辟新線程的能力.
異步(async):線程會立即返回,無需等待就會繼續(xù)執(zhí)行下面的任務,不阻塞當前線程.可以在新的線程中執(zhí)行任務,具備開啟新線程的能力.
隊列
隊列(Dispatch Queue):這里的隊列指執(zhí)行任務的等待隊列,即用來存放任務的隊列.隊列是一種特殊的線性表,采用FIFO(先進先出)的原則,即新任務總是被插入到隊列的末位,而讀取任務總是從隊列的頭部讀取.每讀取一個任務,則從隊列釋放一個任務.
在GCD中有兩種隊列:串行隊列和并發(fā)隊列.兩者都符合FIFO(先進先出)的原則.兩者主要區(qū)別是:執(zhí)行順序不同,以及開啟線程數(shù)不同.
串行隊列(Serial Dispatch Queue):同一時間內(nèi),隊列中只能執(zhí)行一個任務,只有當前的任務執(zhí)行完成之后,才能執(zhí)行下一個任務.(只 開啟一個線程,一個任務執(zhí)行完畢后,再執(zhí)行下一個任務).主隊列是主線程上的一個串行隊列,是 系統(tǒng)自動為我們創(chuàng)建的
并發(fā)隊列(Concurrent Dispatch Queue):同時允許多個任務并發(fā)執(zhí)行.(可以開啟多個線程,并且同時執(zhí)行任務).并發(fā)隊列的并發(fā)功能只有在異步(dispatch_async)函數(shù)下才有效.
四 iOS中的多線程
iOS中的多線程主要有三種:NSThread,NSOperationQueue,GCD
NSThread:輕量級別的多線程
是我們手動開辟的子線程,如果使用的是初始化方式需要我們自己啟動,如果使用的構(gòu)造器方式它會自動啟動.只要我們手動開辟線程,都需要我們自己管理該線程,不只是啟動,還有該線程使用完畢后的資源回收.
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(testThread:) object:@"我是參數(shù)"];
//使用初始化方法時需要start來啟動
[thread start];
//開辟子線程的名字
thread.name = @"NSThread線程";
//線程的權(quán)限,范圍值0-1,權(quán)限越高,先執(zhí)行的概率越高,由于是概率,所以并不能很準確的實現(xiàn)我們想要的執(zhí)行
thread.threadPriority = 1;
//取消當前已啟動的線程
[thread cancel];
performSelector...只要是 NSObject 的子類或者對象都可以通過調(diào)用方法進入子線程和主線程,其實這些 方法所開辟的子線程也是 NSThread 的另一種體現(xiàn)方式,是在NSObject的分類中對NSThread的封裝.在編譯階段并不會去檢查方法是否有效存在,如果不存在只會給出警告.
//在當前線程延遲1s執(zhí)行
[self performSelector:@selector(performSelectorTest:) withObject:@"performSelectorTestafterDelay1s" afterDelay:1.f];
//回到主線程執(zhí)行.waitUntilDone是否將該回調(diào)方法執(zhí)行完在執(zhí)行后面的代碼
[self performSelectorOnMainThread:@selector(performSelectorTest:) withObject:@"performSelectorOnMainThread" waitUntilDone:YES];
//開辟子線程
[self performSelectorInBackground:@selector(performSelectorTest:) withObject:@"performSelectorInBackground"];
//在指定線程上執(zhí)行
[self performSelector:@selector(performSelectorTest:) onThread:[NSThread currentThread] withObject:@"performSelectorOnThread" waitUntilDone:YES];
需要注意的是:如果是帶 afterDelay 的延時函數(shù),會在內(nèi)部創(chuàng)建一個 NSTimer,然后添加到當前線程的 Runloop 中.也就是如果當前線程沒有開啟 runloop,該方法會失效.在子線程中,需要啟動 runloop(注意調(diào)用順序)
NSOperationQueue
operation 是蘋果基于GCD封裝的,是面向?qū)ο蟆R话?operation 和 operation queue配合使用,這樣可以很方便的在異步執(zhí)行任務,但是這樣也不是必須的。operation 內(nèi)部有一個 start 方法可調(diào)用,但是無法保證在異步執(zhí)行。我們通常使用的NSInvocationOperation和NSBlockOperation都是NSOperation的子類。當然,我們也可以自己創(chuàng)建NSOperation的子類。
NSInvocationOperation
NSOperationQueue *queue = [NSOperationQueue new];
NSInvocationOperation *invOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(operationTest:) object:@"參數(shù)"];
[queue addOperation:invOp];
- (void)operationTest:(NSString *)parameter
{
NSLog(@"%@ - %@", [NSThread currentThread], parameter);
}
打印結(jié)果可以看到我們在異步線程執(zhí)行了這段方法。如果我們直接使用 operation 的 start 方法而不是加入 queue ,那打印結(jié)果就只會在當前線程打印。
NSBlockOperation
NSOperationQueue *queue = [NSOperationQueue new];
NSBlockOperation *blockOp = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
}];
[queue addOperation:blockOp];
除了上述使用 NSOperationQueue 添加 NSBlockOperation 實例外, NSBlockOperation 實例還有 addExecutionBlock: 方法,可以方便的添加多個 block
NSOperationQueue *queue = [NSOperationQueue new];
NSBlockOperation *blockOp = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1- %@", [NSThread currentThread]);
}];
[blockOp addExecutionBlock:^{
NSLog(@"2- %@", [NSThread currentThread]);
}];
[blockOp addExecutionBlock:^{
NSLog(@"3 - %@", [NSThread currentThread]);
}];
// [blockOp start];
[queue addOperation:blockOp];
自定義NSOperation
@synthesize finished = _finished;
@synthesize executing = _executing;
- (BOOL)isAsynchronous
{
return YES;
}
- (void)start
{
if (self.isCancelled) {
[self willChangeValueForKey:@"isFinished"];
_finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}
[self willChangeValueForKey:@"isExecuting"];
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
_executing = YES;
[self didChangeValueForKey:@"isExecuting"];
}
- (void)main
{
for (NSInteger i = 0; i < 9999; i++) {
if (self.isCancelled) {
break;
}
NSLog(@"thread - %@, i is %@", [NSThread currentThread], @(i));
}
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
_executing = NO;
_finished = YES;
[self didChangeValueForKey:@"isFinished"];
[self didChangeValueForKey:@"isExecuting"];
}
首先,NSOperation 類中含有executing、finished屬性,并且是只讀的。如果想修改這兩個屬性的值,我們可以使用 @synthesize 關(guān)鍵字手動合成兩個實例變量 _executing 和 _finished 。并且通過 willChangeValueForKey: 和 didChangeValueForKey: 通過 KVO 通知它們的值修改了。
那怎么實現(xiàn)并發(fā)了:
1.重寫 - (BOOL)isAsynchronous 方法來告訴別人方法是否是并發(fā)。
2.一般我們會重寫 start 方法來進行一些初始化操作。比如,如果判斷線程被取消或者添加一些條件判斷。
3.重寫 main 方法做復雜邏輯操作。
GCD
GCD---隊列
GCD共有三種隊列類型:
main queue:通過dispatch_get_main_queue()獲得,這是一個主線程相關(guān)的串行隊列.
global queue:全局隊列是并發(fā)隊列,由整個進程共享.存在高中低三種優(yōu)先級的全局隊列.調(diào)用dispatch_get_global_queue()并傳入優(yōu)先級來訪問.
自定義隊列:通過dispatch_queue_create()創(chuàng)建隊列.
GCD任務執(zhí)行順序
//創(chuàng)建一個串行隊列
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
//異步任務插入串行隊列
dispatch_async(serialQueue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"2");
});
NSLog(@"3");
//同步任務插入串行隊列
dispatch_sync(serialQueue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"4");
});
NSLog(@"5");
//創(chuàng)建一個并發(fā)隊列
dispatch_queue_t concurrentQueue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
//異步任務插入并發(fā)隊列
dispatch_async(concurrentQueue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"6");
});
NSLog(@"7");
//同步任務插入并發(fā)隊列
dispatch_sync(concurrentQueue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"8");
});
NSLog(@"9");
//獲取主隊列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
//異步任務插入主隊列
dispatch_async(mainQueue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"10");
});
NSLog(@"11");
//同步任務插入主隊列
dispatch_sync(mainQueue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"12");
});
NSLog(@"13");
打印順序是1,3,2,4,5,7,8,9,11 (6隨機出現(xiàn)在7之后或者沒有)
原因是:首先打印 1,接下來將任務2添加至串行隊列上,由于任務2是異步,不會阻塞線程,繼續(xù)向下執(zhí)行,打印3,然后是任務4,將任務4添加至串行隊列上,因為任務4和任務2在同一個串行隊列上,任務4必須等任務2執(zhí)行后才能執(zhí)行,又因為任務4是同步任務,會阻塞線程,只有執(zhí)行完4后才能繼續(xù)向下執(zhí)行打印5.之后創(chuàng)建一個并發(fā)隊列并將異步任務6添加到并發(fā)隊列,因為6是異步任務不會阻塞線程,繼續(xù)向下執(zhí)行,打印7.接下來將同步任務8添加到并發(fā)隊列,因為是并發(fā)隊列8不需要等6完成,又因為8是同步任務,會阻塞線程,所以任務8執(zhí)行完后才能執(zhí)行任務9.向下繼續(xù)執(zhí)行,獲取主隊列,前面提到過主隊列是一個主線程相關(guān)的串行隊列,所以把異步任務10添加到主隊列不會阻塞主線程,接下來會打印11.當把同步任務12添加到主線程時,會發(fā)生死鎖(關(guān)于死鎖,下面篇幅有講解),所以12,13不會被執(zhí)行,異步任務10也因為死鎖的原因斌沒有執(zhí)行完.任務6會因為計算時間不穩(wěn)定,隨機出現(xiàn)在7之后,或者沒有被打印.
| 總結(jié) | 串行隊列 | 并發(fā)隊列 | 主隊列 |
|---|---|---|---|
| 同步任務(sync) | 不開辟新線程,阻塞當前線程 | 不開辟新線程,阻塞當前線程 | 死鎖 |
| 異步任務(async) | 開辟新線程,不阻塞當前線程,異步任務串行執(zhí)行 | 開辟新線程,不阻塞當前線程,異步任務并發(fā)執(zhí)行 | 主線程運行,不阻塞當前線程,異步任務串行執(zhí)行 |
CGD中的死鎖
-(void)mainQueueLock{
//獲取主隊列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
//異步任務插入主隊列
dispatch_async(mainQueue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"10");
});
NSLog(@"11");
//同步任務插入主隊列
dispatch_sync(mainQueue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"12");
});
NSLog(@"13");
}
上面例子中,會在同步任務12添加到主隊列時發(fā)生死鎖.我們知道主隊列是一個主線程相關(guān)的串行隊列,當程序執(zhí)行到mainQueueLock方法時,已經(jīng)在主線程中有一個同步任務mainQueueLock,而在執(zhí)行mainQueueLock時又向主隊列中添加了同步任務12.mainQueueLock是同步任務,會阻塞線程,只有mainQueueLock執(zhí)行完成后才能執(zhí)行任務12,而mainQueueLock要執(zhí)行完又必須得執(zhí)行完任務12,這樣兩個任務就會因為相互等待而導致死鎖.
同樣,下面的代碼也會造成死鎖.
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
dispatch_sync(serialQueue, ^{
NSLog(@"0");
});
});
外面的函數(shù)無論是同步還是異步都會造成死鎖
這是因為里面的任務和外面的任務都太同一個串行隊列里,又是同步,這和上面的主隊列同步是一樣的.
解決方案,試講里面的同步改為異步或者將serialQueue換成其他串行隊列或者并行隊列.
GCD中的柵欄函數(shù)
當任務需要異步進行,但這些任務需要分成兩組,第一組完成才能進行第二組操作.這時候就要用到GCD的柵欄方法dispatch_barrier_async和dispatch_barrier_sync.
//創(chuàng)建串行隊列
dispatch_queue_t quene = dispatch_queue_create("yc", DISPATCH_QUEUE_SERIAL);
NSLog(@"當前線程%@",[NSThread currentThread]);
//串行隊列添加異步操作
dispatch_async(quene, ^{
NSLog(@"1號我在哪里啊-----%@",[NSThread currentThread]);
});
//設置同步柵欄
dispatch_barrier_async(quene, ^{
NSLog(@"異步柵欄操作線程------%@",[NSThread currentThread]);
sleep(5);
NSLog(@"異步柵欄休息5秒");
});
//串行隊列添加同步操作
dispatch_sync(quene, ^{
NSLog(@"2號我在哪里啊-----%@",[NSThread currentThread]);
});
//設置異步柵欄
dispatch_barrier_sync(quene, ^{
NSLog(@"同步柵欄操作線程------%@",[NSThread currentThread]);
sleep(5);
NSLog(@"同步柵欄休息五秒");
});
NSLog(@"結(jié)束");
打印結(jié)果是
14:16:25 當前線程<NSThread: 0x600002ec4b40>{number = 1, name = main}
14:16:25 1號我在哪里啊-----<NSThread: 0x600002e94bc0>{number = 4, name = (null)}
14:16:25 異步柵欄操作線程------<NSThread: 0x600002e8ffc0>{number = 3, name = (null)}
14:16:30 異步柵欄休息5秒
14:16:30 2號我在哪里啊-----<NSThread: 0x600002ec4b40>{number = 1, name = main}
14:16:30 同步柵欄操作線程------<NSThread: 0x600002ec4b40>{number = 1, name = main}
14:16:35 同步柵欄休息五秒
14:16:35 結(jié)束
我們再試下并發(fā)隊列
//創(chuàng)建并發(fā)隊列
dispatch_queue_t quene = dispatch_queue_create("yc", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"當前線程%@",[NSThread currentThread]);
//串行隊列添加異步操作
dispatch_async(quene, ^{
NSLog(@"1號我在哪里啊-----%@",[NSThread currentThread]);
});
//設置同步柵欄
dispatch_barrier_async(quene, ^{
NSLog(@"異步柵欄操作線程------%@",[NSThread currentThread]);
sleep(5);
NSLog(@"異步柵欄休息5秒");
});
//串行隊列添加同步操作
dispatch_sync(quene, ^{
NSLog(@"2號我在哪里啊-----%@",[NSThread currentThread]);
});
//設置異步柵欄
dispatch_barrier_sync(quene, ^{
NSLog(@"同步柵欄操作線程------%@",[NSThread currentThread]);
sleep(5);
NSLog(@"同步柵欄休息五秒");
});
NSLog(@"結(jié)束");
結(jié)果
14:28:33 當前線程<NSThread: 0x600002fed440>{number = 1, name = main}
14:28:33 1號我在哪里啊-----<NSThread: 0x600002fa26c0>{number = 6, name = (null)}
14:28:33 異步柵欄操作線程------<NSThread: 0x600002fa26c0>{number = 6, name = (null)}
14:28:38 異步柵欄休息5秒
14:28:38 2號我在哪里啊-----<NSThread: 0x600002fed440>{number = 1, name = main}
14:28:38 同步柵欄操作線程------<NSThread: 0x600002fed440>{number = 1, name = main}
14:28:43 同步柵欄休息五秒
14:28:43 結(jié)束
我們對比兩次的數(shù)據(jù)可以看到.
1.異步柵欄會開辟新的線程;
2.同步柵欄在當前線程上;
3.無論同步柵欄還是異步柵欄,都是需要前面一組完成后,在執(zhí)行柵欄方法,之后再執(zhí)行下一組操作.
從上面看,兩種柵欄除了線程不一樣其他基本相同.事實上,在基本需求上兩者確實差不多.但柵欄方法內(nèi)部執(zhí)行異步方法時就有所區(qū)別.
dispatch_queue_t quene = dispatch_queue_create("yc", DISPATCH_QUEUE_SERIAL);
NSLog(@"當前線程%@",[NSThread currentThread]);
dispatch_async(quene, ^{
NSLog(@"1號我在哪里啊-----%@",[NSThread currentThread]);
});
dispatch_barrier_sync(quene, ^{
NSLog(@"同步柵欄操作線程1------%@",[NSThread currentThread]);
dispatch_async(quene, ^{
NSLog(@"同步柵欄操作線程2------%@",[NSThread currentThread]);
sleep(5);
NSLog(@"同步柵欄休息5秒");
});
});
NSLog(@"同步柵欄-異步操作--結(jié)束");
dispatch_barrier_async(quene, ^{
NSLog(@"異步柵欄操作線程1------%@",[NSThread currentThread]);
dispatch_async(quene, ^{
NSLog(@"異步柵欄操作線程2------%@",[NSThread currentThread]);
sleep(5);
NSLog(@"異步柵欄休息5秒");
});
});
NSLog(@"異步柵欄-異步操作--結(jié)束");