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個執行緒繼續執行任務

執行緒間通訊常用方法
  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]);  }
  1. 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和這個任務就形成了相互循環等待,就造成了死鎖。