iOS 多執行緒總結
- 2019 年 10 月 8 日
- 筆記
了解多執行緒,首先我們需要了解以下知識
進程
●進程是指在系統中正在運行的一個應用程式,就是一段程式的執行過程,我們可以理解為手機上的一個app。 ●每個進程之間是獨立的,每個進程均運行在其專用且受保護的記憶體空間內,擁有獨立運行所需的全部資源。
執行緒
●程式執行流的最小單元,執行緒是進程中的一個實體. ●一個進程要想執行任務,必須至少有一條執行緒.應用程式啟動的時候,系統會默認開啟一條執行緒,也就是主執行緒
任務
任務就是執行操作的意思,也就是在執行緒中執行的那段程式碼。在 GCD 中是放在 block 中的。執行任務有兩種方式:同步執行(sync)和非同步執行(async)
同步
同步添加任務到指定的隊列中,在添加的任務執行結束之前,會一直等待,直到隊列裡面的任務完成之後再繼續執行,即會阻塞執行緒。只能在當前執行緒中執行任務,不具備開啟新執行緒的能力。
dispatch_queue_t concurrentQueue = dispatch_queue_create("my.concurrent.queue", DISPATCH_QUEUE_CONCURRENT); NSLog(@"1"); dispatch_sync(concurrentQueue, ^(){ NSLog(@"2"); [NSThread sleepForTimeInterval:2]; NSLog(@"3"); }); NSLog(@"4");
非同步
執行緒會立即返回,無需等待就會繼續執行下面的任務,不阻塞當前執行緒。可以在新的執行緒中執行任務,具備開啟新執行緒的能力。如果不是添加到主隊列上,非同步會在子執行緒中執行任務
dispatch_queue_t queue = dispatch_queue_create("my.concurrent.queue", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ // dispatch_async是非同步方法。長時間處理,例如資料庫訪問 dispatch_async(dispatch_get_main_queue(), ^{ // 到主執行緒隊列中執行 // 例如介面更新 }); });
隊列
隊列(Dispatch Queue):隊列是一種特殊的線性表,採用 FIFO(先進先出)的原則,即新任務總是被插入到隊列的末尾,而讀取任務的時候總是從隊列的頭部開始讀取。每讀取一個任務,則從隊列中釋放一個任務
在 GCD 中有兩種隊列:串列隊列和並發隊列。兩者都符合 FIFO(先進先出)的原則。兩者的主要區別是:執行順序不同,以及開啟執行緒數不同。
串列隊列(Serial Dispatch Queue):
同一時間內,隊列中只能執行一個任務,只有當前的任務執行完成之後,才能執行下一個任務。(只開啟一個執行緒,一個任務執行完畢後,再執行下一個任務)。主隊列是主執行緒上的一個串列隊列,是系統自動為我們創建的
串列隊列
// 串列隊列DISPATCH_QUEUE_SERIAL // 並發隊列DISPATCH_QUEUE_CONCURRENT dispatch_queue_t serialQueue = dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_SERIAL); NSLog(@"1"); dispatch_async(serialQueue, ^{ NSLog(@"2"); }); NSLog(@"3"); dispatch_sync(serialQueue, ^{ NSLog(@"4"); }); NSLog(@"5");
列印結果13245
首先先列印1 接下來將任務2其添加至串列隊列上,由於任務2是非同步,不會阻塞執行緒,繼續向下執行,列印3然後是將任務4添加至串列隊列上,因為任務4和任務2在同一串列隊列,根據隊列先進先出原則,任務4必須等任務2執行後才能執行,又因為任務4是同步任務,會阻塞執行緒,只有執行完任務4才能繼續向下執行列印5 所以最終順序就是13245。
並發隊列(Concurrent Dispatch Queue):
同時允許多個任務並發執行。(可以開啟多個執行緒,並且同時執行任務)。並發隊列的並發功能只有在非同步(dispatch_async)函數下才有效。
// 串列隊列DISPATCH_QUEUE_SERIAL // 並發隊列DISPATCH_QUEUE_CONCURRENT dispatch_queue_t queue = dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ sleep(1); [[NSThread currentThread] setName:@"任務A"]; NSLog(@"任務A thread:%@",[NSThread currentThread]); }); dispatch_async(queue, ^{ sleep(1); [[NSThread currentThread] setName:@"任務B"]; NSLog(@"任務B thread:%@",[NSThread currentThread]); }); dispatch_async(queue, ^{ sleep(1); [[NSThread currentThread] setName:@"任務C"]; NSLog(@"任務C thread:%@",[NSThread currentThread]); }); NSLog(@"結束");
列印
2019-08-30 10:34:02.718139+0800 TestDemo[56896:6617059] 結束 2019-08-30 10:34:03.722077+0800 TestDemo[56896:6617114] 任務B thread:<NSThread: 0x6000030407c0>{number = 4, name = 任務B} 2019-08-30 10:34:03.722092+0800 TestDemo[56896:6617111] 任務A thread:<NSThread: 0x60000305c3c0>{number = 5, name = 任務A} 2019-08-30 10:34:03.722091+0800 TestDemo[56896:6617112] 任務C thread:<NSThread: 0x60000305a6c0>{number = 3, name = 任務C}
iOS中的多執行緒
主要有三種:NSThread、NSoperationQueue、GCD
1. NSThread
NSThread輕量級別的多執行緒技術 需要自己手動開闢的子執行緒,如果使用的是初始化方式就需要我們自己啟動,如果使用的是構造器方式它就會自動啟動。只要是我們手動開闢的執行緒,都需要我們自己管理該執行緒,不只是啟動,還有該執行緒使用完畢後的資源回收。
手動開啟執行緒
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(testThread:) object:@"我是參數"]; // 當使用初始化方法出來的主執行緒需要start啟動 [thread start]; // 可以為開闢的子執行緒起名字 thread.name = @"NSThread執行緒"; // 調整Thread的許可權 執行緒許可權的範圍值為0 ~ 1 。越大許可權越高,先執行的概率就會越高,由於是概率,所以並不能很準確的的實現我們想要的執行順序,默認值是0.5 thread.threadPriority = 1; // 取消當前已經啟動的執行緒 [thread cancel];
通過遍歷構造器開闢子執行緒
[NSThread detachNewThreadSelector:@selector(testThread:) toTarget:self withObject:@"構造器方式"];
2.NSoperationQueue
NSOperationQueue *queue = [[NSOperationQueue alloc]init]; NSBlockOperation *opA = [NSBlockOperation blockOperationWithBlock:^{ sleep(1); [[NSThread currentThread]setName:@"任務A"]; NSLog(@"任務A thread:%@",[NSThread currentThread]); }]; NSBlockOperation *opB = [NSBlockOperation blockOperationWithBlock:^{ sleep(10); [[NSThread currentThread]setName:@"任務B"]; NSLog(@"任務B thread:%@",[NSThread currentThread]); }]; NSBlockOperation *opC = [NSBlockOperation blockOperationWithBlock:^{ sleep(3); [[NSThread currentThread]setName:@"任務C"]; NSLog(@"任務C thread:%@",[NSThread currentThread]); }]; //添加依賴關係,保證執行順序 [opC addDependency:opB]; [opB addDependency:opA]; [queue addOperation:opA]; [queue addOperation:opB]; [queue addOperation:opC];
列印
2019-08-30 10:44:35.753808+0800 TestDemo[57063:6627379] 任務A thread:<NSThread: 0x6000022360c0>{number = 3, name = 任務A} 2019-08-30 10:44:45.759742+0800 TestDemo[57063:6627377] 任務B thread:<NSThread: 0x6000022360c0>{number = 4, name = 任務B} 2019-08-30 10:44:48.765530+0800 TestDemo[57063:6627377] 任務C thread:<NSThread: 0x6000022360c0>{number = 4, name = 任務C}
GCD
dispatch_semaphore
dispatch_semaphore是GCD用來同步的一種方式,與他相關的共有三個函數,分別是 dispatch_semaphore_create,dispatch_semaphore_signal,dispatch_semaphore_wait。
Dispatch Semaphore 在實際開發中主要用於:
保持執行緒同步,將非同步執行任務轉換為同步執行任務 保證執行緒安全,為執行緒加鎖
dispatch_semaphore_signal:
這個函數會使傳入的訊號量semaphore的值加1;
dispatch_semaphore_wait
這個函數會使傳入的訊號量semaphore的值減1;這個函數的作用是這樣的,如果semaphore訊號量的值大於0,該函數所處執行緒就繼續執行下面的語句,並且將訊號量的值減1;如果semaphore的值為0,那麼這個函數就阻塞當前執行緒等待timeout
賣火車票經典案例
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. self.ticketNumber = 100; dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); for (NSInteger i = 0; i < 10; i++) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(sellTicketsWithSemaphore) object:nil]; [thread setName:[NSString stringWithFormat:@"售票員-%zd",i]]; [thread start]; dispatch_semaphore_signal(semaphore); }); dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); } } - (void)sellTicketsWithSemaphore { while (true) { //訊號==0就阻塞當前執行緒等待timeout,>0就繼續執行下面的語句訊號量的值減1 if (self.ticketNumber > 0) { self.ticketNumber --; NSLog(@"%@賣了一張票,還剩%ld張票",[[NSThread currentThread] name],self.ticketNumber); }else{ // 退出當前執行緒 [NSThread exit]; } } }
使用GCD如何實現這個需求:A、B、C 三個任務並發,完成後執行任務 D?
需要解決這個首先就需要了解dispatch_group_enter 和 dispatch_group_leave。
dispatch_group_enter 標誌著一個任務追加到 group,執行一次,相當於 group 中未執行完畢任務數+1 dispatch_group_leave 標誌著一個任務離開了 group,執行一次,相當於 group 中未執行完畢任務數-1。 當 group 中未執行完畢任務數為0的時候,才會使dispatch_group_wait解除阻塞,以及執行追加到dispatch_group_notify中的任務。
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0); dispatch_group_t group = dispatch_group_create(); dispatch_group_enter(group); [self requestA:^{ NSLog(@"---執行A任務結束---"); dispatch_group_leave(group); }]; dispatch_group_enter(group); [self requestB:^{ NSLog(@"---執行B任務結束---"); dispatch_group_leave(group); }]; dispatch_group_enter(group); [self requestC:^{ NSLog(@"---執行C任務結束---"); dispatch_group_leave(group); }]; dispatch_group_notify(group, globalQueue, ^{ [self requestD:^{ NSLog(@"---執行D任務結束---"); }]; });
- (void)requestA:(void(^)(void))block{ NSLog(@"---執行A任務開始---"); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ block(); }); } - (void)requestB:(void(^)(void))block{ NSLog(@"---執行B任務開始---"); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ block(); }); } - (void)requestC:(void(^)(void))block{ NSLog(@"---執行C任務開始---"); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ block(); }); } - (void)requestD:(void(^)(void))block{ NSLog(@"---執行D任務開始---"); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ block(); }); }
執行緒間通訊
什麼叫做執行緒間通訊?
在1個進程中,執行緒往往不是孤立存在的,多個執行緒之間需要經常進行通訊
執行緒間通訊的體現
●1個執行緒傳遞數據給另1個執行緒 ●在1個執行緒中執行完特定任務後,轉到另1個執行緒繼續執行任務
執行緒間通訊常用方法
- NSThread :一個執行緒傳遞數據給另一個執行緒
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait; - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
//點擊螢幕開始執行下載方法 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [self performSelectorInBackground:@selector(download) withObject:nil]; } //下載圖片 - (void)download { // 1.圖片地址 NSString *urlStr = @"http://d.jpg"; NSURL *url = [NSURL URLWithString:urlStr]; // 2.根據地址下載圖片的二進位數據 NSData *data = [NSData dataWithContentsOfURL:url]; // 3.設置圖片 UIImage *image = [UIImage imageWithData:data]; // 4.回到主執行緒,刷新UI介面(為了執行緒安全) [self performSelectorOnMainThread:@selector(downloadFinished:) withObject:image waitUntilDone:NO]; // [self performSelector:@selector(downloadFinished:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES]; } - (void)downloadFinished:(UIImage *)image { self.imageView.image = image; NSLog(@"downloadFinished---%@", [NSThread currentThread]); }
- GCD :一個執行緒傳遞數據給另一個執行緒
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSLog(@"donwload---%@", [NSThread currentThread]); // 1.子執行緒下載圖片 NSURL *url = [NSURL URLWithString:@"http://d.jpg"]; NSData *data = [NSData dataWithContentsOfURL:url]; UIImage *image = [UIImage imageWithData:data]; // 2.回到主執行緒設置圖片 dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"setting---%@ %@", [NSThread currentThread], image); [self.button setImage:image forState:UIControlStateNormal]; }); });
死鎖
1、定義: 所謂死鎖,通常指有兩個執行緒T1和T2都卡住了,並等待對方完成某些操作。T1不能完成是因為它在等待T2完成。但T2也不能完成,因為它在等待T1完成。於是大家都完不成,就導致了死鎖。
2、產生死鎖的條件: 產生死鎖的四個必要條件: (1) 互斥條件:一個資源每次只能被一個進程使用。 (2) 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。 (3) 不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。 (4) 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係。 這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之 一不滿足,就不會發生死鎖。
首先來看一份導致死鎖的典型程式碼:
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. NSLog(@"執行任務1"); dispatch_sync(dispatch_get_main_queue(), ^(void){ NSLog(@"這裡死鎖了"); }); NSLog(@"執行任務3"); }
輸出:執行任務1 後死鎖了 原因:在主執行緒中運用主隊列同步,也就是把任務放到了主執行緒的隊列中。 同步對於任務是立刻執行的,那麼當把任務放進主隊列時,它就會立馬執行,只有執行完這個任務,viewDidLoad才會繼續向下執行。 而viewDidLoad和任務都是在主隊列上的,由於隊列的先進先出原則,任務又需等待viewDidLoad執行完畢後才能繼續執行,viewDidLoad和這個任務就形成了相互循環等待,就造成了死鎖。