iOS開發小記-多線程篇

這兩個月App被拒審,應對蘋果那邊花了不少心思,也花了大量時間,也對蘋果審覈團隊有了全新高度的認識,後續計劃寫一下這段時間是如何與蘋果鬥智鬥勇~html

線程和進程


幾乎全部的操做系統都支持同時運行多個任務,一個任務一般就是一個程序,每一個運行程序就是一個進程。當一個程序運行時,內部可能包含了多個順序執行流,每一個順序執行流就是一個線程。ios

  • 進程(Process)

當一個程序進入內存運行後,即變成一個進程。進程是處於運行過程當中的程序,而且具備必定的獨立功能,進程是系統進行資源分配和調度的一個獨立單位。 通常而言,進程有以下特徵:git

  1. 獨立性:有本身獨立的資源,且擁有本身私有的地址空間。在沒有通過進程自己的容許下,其餘進程是不能直接訪問其進程的地址空間的。
  2. 動態性:程序只是靜態的指令集合,而進程是一個正在系統中活動的指令集合。進程有時間的概念,具備本身的生命週期和各類狀態。
  3. 併發性:多個進程能夠在單個處理器上併發執行,互相不會影響。
  • 線程(Thread)

線程也被稱做輕量級進程,線程是進程的執行單元。就像進程在系統中同樣,線程在進程中也是獨立、併發的執行流。 一個進程能夠擁有多個線程,一個線程必須有一個父進程,但再也不擁有系統資源,而是和父進程下的其餘線程一塊兒共享父進程的所有資源。多線程因爲共享父進程的資源,因此編程更加方便;可是也須要當心線程不會影響到父進程中的其餘線程。 線程是獨立運行的,它並不知道其餘線程的存在。線程的執行是搶佔式的,也就是說,當前運行的線程在任什麼時候候均可能被掛起,以便另一個線程能夠運行。程序員

  • 多線程優勢
  1. 進程間不可共享內存,但線程之間共享內存十分容易。
  2. 系統建立進程要爲其從新分配系統資源,但建立線程代價小得多,所以效率更高。

爲何要用多線程編程


爲了提升資源利用率來提高系統總體效率,實際每每是將耗時操做放在後臺執行,避免阻塞主線程,在iOS中UI繪製和用戶響應都是在主線程。github

  • 多線程線程池的大小

Java中是cpu核數*2-1,iOS沒有查到確切資料。編程

NSThread


經常使用API安全

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //打印當前線程
    NSLog(@"開始:%@ 優先級:%d", [NSThread currentThread], [NSThread currentThread].qualityOfService);
    
    //1.建立NSTread對象,必須調用start方法開始,而且只能傳一個參數object
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"test"];
    //    NSThread *thread = [[NSThread alloc] initWithBlock:^{}];
    thread.name = @"testThread";
    thread.qualityOfService = NSQualityOfServiceUserInteractive;
    [thread start];
    
    //2.直接建立並啓動線程
    //    [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"test"];
    //    [NSThread detachNewThreadWithBlock:^{}];
    
    //3.隱式直接建立
//    [NSThread performSelectorInBackground:@selector(run:) withObject:nil];
    
    //    NSLog(@"結束:%@", [NSThread currentThread]);
}

- (void)run:(NSObject *)object {
    //阻塞休眠
    //    [NSThread sleepForTimeInterval:5];
    //停止當前線程
    //    [NSThread exit];
    NSLog(@"子線程運行:%@ %@ 優先級:%d", [NSThread currentThread], object, [NSThread currentThread].qualityOfService);
}
複製代碼
  • 線程的狀態

線程被啓動後,並非直接進入執行狀態,也不是一直處於執行狀態,因爲多線程併發,線程會反覆在運行、就緒間切換。 建立一個線程後,處於新建狀態,系統爲其分配內存,初始化成員變量;調用- (void)start;方法後,該線程處於就緒狀態,系統爲其建立方法調用棧和程序計數器,此時並無運行,什麼時候運行取決於系統調度。 bash

image.png

  • 終止子線程

NSTread並無提供方法來終止子線程,只有+ (void)exit;來終止當前線程,咱們不能直接使用該方法,防止誤操做終止了主線程,正確應該是調用子線程的- (void)cancel;方法用來標記子線程狀態,而後在子線程的執行方法體內對isCancelled作判斷,而後在調用+ (void)exit;來終止當前運行的子線程。markdown

  • 線程優先級

每一個線程都有必定的優先級,優先級越高得到執行機會越多。目前經過qualityOfService屬性來設置,原來的threadPriority因爲語義不夠清晰,已經被廢棄了。多線程

NSQualityOfServiceUserInteractive:最高優先級,主要用於提供交互UI的操做,好比處理點擊事件,繪製圖像到屏幕上
NSQualityOfServiceUserInitiated:次高優先級,主要用於執行須要當即返回的任務
NSQualityOfServiceDefault:默認優先級,當沒有設置優先級的時候,線程默認優先級
NSQualityOfServiceUtility:普通優先級,主要用於不須要當即返回的任務
NSQualityOfServiceBackground:後臺優先級,用於徹底不緊急的任務
複製代碼
  • 缺點

使用NSThread進行多線程編程較複雜,須要本身控制多線程的同步、併發,還須要本身控制線程的終止銷燬,稍不留神就容易出現錯誤,對開發者要求較高,通常較少使用。

NSOperation


iOS還提供了NSOperation與NSOperationQueue來實現多線程,是基於 GCD 更高一層的封裝,徹底面向對象。可是比 GCD 更簡單易用、代碼可讀性也更高。

NSOperationQueue:負責管理系統提交的多個NSOperation ,底層維護了一個線程池。不一樣於 GCD 中的調度隊列 FIFO(先進先出)的原則。NSOperationQueue 對於添加到隊列中的操做,首先進入準備就緒的狀態(就緒狀態取決於操做之間的依賴關係),而後進入就緒狀態的操做的開始執行順序(非結束執行順序)由操做之間相對的優先級決定(優先級是操做對象自身的屬性)。

NSOperation:表明一個多線程任務。

  • 爲何要使用 NSOperation、NSOperationQueue?
  1. 可添加完成的代碼塊,在操做完成後執行。
  2. 添加操做之間的依賴關係,方便的控制執行順序。
  3. 設定操做執行的優先級。
  4. 能夠很方便的取消一個操做的執行。
  5. 使用 KVO 觀察對操做執行狀態的更改:isExecuteing、isFinished、isCancelled。
  • 經常使用API
NSOperationQueue *queue;
    //獲取執行當前NSOperation的NSOperationQueue隊列
    //    queue = [NSOperationQueue currentQueue];
    //獲取主線程的NSOperationQueue隊列
    //    queue = [NSOperationQueue mainQueue];
    //自定義隊列
    queue = [[NSOperationQueue alloc] init];
    //隊列名
    queue.name = @"testOperationQueue";
    //最大併發操做數(系統有限制,即便設置很大,也會自動調整)
    queue.maxConcurrentOperationCount = 10;
    //設置優先級
    queue.qualityOfService = NSQualityOfServiceDefault;
    
    //自定義NSOperation,若是SEL和Block爲空,系統不會加入到指定隊列
    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"blockOperation");
    }];
    //添加依賴關係,invocationOperation執行完後才執行blockOperation
    [blockOperation addDependency:invocationOperation];
    //添加到隊列中
    //    [queue addOperation:invocationOperation];
    [queue addOperations:@[invocationOperation, blockOperation] waitUntilFinished:NO];
    //直接添加代碼塊任務
    [queue addOperationWithBlock:^{
        
    }];
    
    //打印全部的NSOperation
    for(int i=0; i<queue.operationCount; i++) {
        NSLog(@"隊列%@的第%d個NSOperation:%@", queue.name, i, queue.operations[i]);
    }
    
    //終止全部NSOperation
    //    [queue cancelAllOperations];
    //執行完全部NSOperation才能解除阻塞當前線程
    //    [queue waitUntilAllOperationsAreFinished];
複製代碼

GCD(Grand Central Dispatch)

  • 基本概念
  1. 隊列:隊列負責開發者提交的任務,不過不一樣任務的執行時間不同,先處理的任務不必定先完成。隊列便可是串行的,也但是並行的,隊列底層會維持一個線程池來處理任務,串行隊列只須要維護一個線程便可,並行隊列則須要維護多個線程。
  2. 任務:用戶提交給隊列的工做單元,這些任務將會提交給隊列底層維護的線程池。
  3. 異步:能夠在新的線程中執行任務,但不必定會開闢新的線程。dispatch函數會當即返回, 而後Block在後臺異步執行。
  4. 同步:在當前線程執行任務,不會開闢新的線程。必須等到Block函數執行完畢後,dispatch函數纔會返回。

注:隊列的串行和並行決定了任務以何種方式執行,執行的異步和同步決定是否須要開闢新線程處理任務。

  • 特色
  1. GCD 可用於多核的並行運算
  2. GCD 會自動利用更多的 CPU 內核(好比雙核、四核)
  3. GCD 會自動管理線程的生命週期(建立線程、調度任務、銷燬線程)
  4. 程序員只須要告訴 GCD 想要執行什麼任務,不須要編寫任何線程管理代碼。
  • 經常使用API
/** 獲取隊列 */
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);//獲取指定優先級的全局併發隊列(flag填0便可,僅預留的參數,使用其餘值可能會返回null)
    dispatch_queue_t queue1 = dispatch_queue_create("testQueue1", DISPATCH_QUEUE_CONCURRENT);//建立自定義並行隊列
    dispatch_queue_t queue2 = dispatch_get_main_queue();//獲取系統主線程關聯的串行隊列
    dispatch_queue_t queue3 = dispatch_queue_create("testQueue3", DISPATCH_QUEUE_SERIAL);//建立自定義串行隊列
    
    /** 提交任務 */
    dispatch_async(queue, ^{
        
    });//異步提交代碼塊到併發隊列
    
    dispatch_sync(queue1, ^{
        
    });//同步提交代碼塊到自定義併發隊列
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5*NSEC_PER_SEC)), queue2, ^{
        
    });//異步提交代碼塊到串行隊列,線程池將在指定時間執行代碼塊(實際是5秒後加入到隊列中,實際並不必定會立馬執行,通常精度要求下是沒問題的)
    
    dispatch_apply(5, queue3, ^(size_t time) {
        
    });//異步提交代碼到自定義串行隊列,同步函數,不管是在串行仍是並行隊列中執行,都要執行完才返回,因此要防止線程阻塞和死鎖,time表示當前是第幾回(若是提交給併發隊列,會啓動五個線程來執行)
    
    static dispatch_once_t onceToken; //實際是個long類型變量,用於判斷該代碼塊是否被執行過
    dispatch_once(&onceToken, ^{
        
    });//主線程執行一次代碼塊

    //等group執行完後,才能執行下一步
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
    /** 組(用於須要等待多個任務所有執行完再進行下一步) */
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, queue, ^{
        
    });//併發執行的代碼塊1
    dispatch_group_async(group, queue, ^{
        
    });//併發執行的代碼塊2
    dispatch_group_notify(group, queue, ^{
        
    });//等待兩個代碼塊執行完彙總
    
    /** 柵欄(用於須要依次執行完多個線程組) */
    //併發隊列異步執行代碼塊1,2
    dispatch_async(queue, ^{
        //代碼塊1
    });
    dispatch_async(queue, ^{
        //代碼塊2
    });
    //1,2執行完後纔會執行3,4
    dispatch_barrier_async(queue, ^{
        
    });
    //併發隊列異步執行代碼塊3,4
    dispatch_async(queue, ^{
        //代碼塊3
    });
    dispatch_async(queue, ^{
        //代碼塊4
    });

    /** 信號量(用於控制線程的等待和執行) */
    //建立信號量,value表示初始信號總量,支持多少個操做來執行
    dispatch_semaphore_t t = dispatch_semaphore_create(1);
    //發送一個信號,讓信號總量+1
    dispatch_semaphore_signal(t);
    //使信號總量-1,若是總量爲0,則會一直等待(阻塞所在線程),直到總量大於0則繼續執行
    dispatch_semaphore_wait(t, DISPATCH_TIME_FOREVER);
    
    /*1.能夠將異步執行變爲同步執行,如須要等待下載完後再直接返回數據(咱們也能夠經過block回調)*/
    //總信號量設置爲0
    dispatch_semaphore_t t1 = dispatch_semaphore_create(0);
    //執行耗時代碼
    void (^downloadTask)(void) = ^ {
        //下載圖片
        ...
        ...
        //完成後發送信號量
        dispatch_semaphore_signal(t1);
    };
    downloadTask();
    //一直等到信號量計數爲1才執行下一步,也就是等到圖片下載完後
    dispatch_semaphore_wait(t1, DISPATCH_TIME_FOREVER);
    
    /*2.保證線程安全*/
    //設置信號量初始計數爲1,保證只能有一個操做能進來
    dispatch_semaphore_t t2 = dispatch_semaphore_create(1);
    //至關於加鎖,消耗使用計數,若是已經被一個線程使用,後續只能掛起等待信號量回復
    dispatch_semaphore_wait(t2, DISPATCH_TIME_FOREVER);
    //執行業務代碼
    ...
    ...
    //解鎖
    dispatch_semaphore_signal(t2);
    
    /*3.模擬NSOperationQueue的最大併發操做數*/
    //最大併發操做支持10
    dispatch_semaphore_t t3 = dispatch_semaphore_create(10);
    //剩餘操做同上,其實就是相似於將NSOperationQueue的maxConcurrentOperationCount設置爲10
複製代碼
  • 後臺運行

在App程序進入後臺時,咱們應該儘可能釋放內存和保存用戶數據或者狀態信息。在默認狀況下,應用僅有5秒鐘處理這些工做,咱們能夠經過調用UIApplicationbeginBackgroundTaskWithExpirationHandler方法來申請延長處理時間,最多有十分鐘。

- (void)applicationDidEnterBackground:(UIApplication *)application {
    //聲明關閉後臺任務代碼塊
    void (^endBackgroundTask)(UIBackgroundTaskIdentifier backgroudTask) = ^(UIBackgroundTaskIdentifier backgroudTask) {
        [[UIApplication sharedApplication] endBackgroundTask:backgroudTask];
        backgroudTask = UIBackgroundTaskInvalid;
    };
    
    //開啓後臺任務
    __block UIBackgroundTaskIdentifier backgroudTask;
    backgroudTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
        //十分鐘內仍然沒有完成,系統處理終止句柄
        endBackgroundTask(backgroudTask);
    }];
    
    //執行相關代碼
    
    //結束後臺任務
    endBackgroundTask(backgroudTask);
}
複製代碼
  • 線程死鎖
- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"%@", [NSThread currentThread]);
    });
}
複製代碼

在主隊列中增長同步代碼塊,就會形成死鎖,因爲同步是須要當即順序執行的,上述代碼中,Block中的方法須要在viewDidLoad結束後才能完成,可是viewDidLoad想要結束又必須得先結束Block中的方法,因此相互永久等待,形成了死鎖。

  • GCD會形成循環引用嗎?

直接使用GCD的相關API通常是不會的,block結束後沒有循環引用的條件,YYKit的Issues下有個有趣的討論:dispatch_async的block裏面須要__weak self 嗎?

  • 注意
  1. 同步執行會在當前線程執行任務,不具有開闢線程的能力或者說沒有必要開闢新的線程。而且,同步執行必須等到Block函數執行完畢,dispatch函數纔會返回,從而阻塞同一串行隊列中外部方法的執行。
  2. 異步執行dispatch函數會直接返回,只有異步執行纔有開闢新線程的必要,可是異步執行不必定會開闢新線程。
  3. 想要開闢新線程必須讓任務在異步執行,想要開闢多個線程,只有讓任務在並行隊列中異步執行才能夠。執行方式和隊列類型多層組合在必定程度上可以實現對於代碼執行順序的調度。
  4. 同步+串行:未開闢新線程,串行執行任務;同步+並行:未開闢新線程,串行執行任務;異步+串行:新開闢一條線程,串行執行任務;異步+並行:開闢多條新線程,並行執行任務;在主線程中同步使用主隊列執行任務,會形成死鎖。

線程安全

線程安全主要是因爲系統的線程調度具備必定的隨機性形成的,因爲是多併發,多個線程同時對一份數據進行讀寫,就可能在讀取執行一半的時候另一個線程去寫入,致使數據異常。

  • 線程安全的類的特徵
  1. 該類的對象能夠被多個線程安全訪問。
  2. 每一個線程調用對象的任意方法都會獲得正確的結果。
  3. 每一個線程調用對象的任意方法以後,該對象仍保持合理狀態。
  • @synchronized(同步鎖、互斥鎖)

爲了解決這個問題,Objective-C的多線程支持引入同步,使用@synchronized修飾代碼塊,被修飾的代碼塊可簡稱爲同步代碼塊。語法格式以下:

@synchronized (obj) {
    //同步代碼塊
}
複製代碼

其中obj就是同步監視器,當一個線程執行同步前,必須先得到同步監視器的鎖定,任什麼時候刻只能有一個線程得到鎖定,執行完成後,纔會釋放,若是此時有新線程訪問,那麼新線程會進入休眠狀態。一般推薦使用可能被併發訪問的的共享資源做爲同步監視器。

  • NSLock

除了上面的同步代碼塊,還支持顯示的同步鎖NSLock,在須要加鎖的代碼塊首尾使用- (void)lock;- (void)unlock;來加鎖和釋放鎖。

  • 自旋鎖

當新線程訪問代碼時,若是發現有其餘的線程正在訪問,新線程會用死循環的方式去等待當前線程的訪問結束,比較耗性能,例如atomic就是採用的自旋鎖。 可使用OSSpinLock,可是有安全問題,iOS10之後可使用os_unfair_lock

  • 讀寫鎖
//加讀鎖
pthread_rwlock_rdlock(&rwlock);
//解鎖
pthread_rwlock_unlock(&rwlock);
//加寫鎖
pthread_rwlock_wrlock(&rwlock);
//解鎖
pthread_rwlock_unlock(&rwlock);
複製代碼
  • 遞歸鎖
  • 條件鎖
  • 爲了減小線程安全的負面影響,應使用何種策略?
  1. 只對有可能會形成資源競爭的方法進行同步。
  2. 提供非線程安全和線程安全兩個版本,單線程中使用非線程安全版本。

相關資料

iOS 多線程:『GCD』詳盡總結

併發編程:API 及挑戰

iOS開發中的11種鎖以及性能對比

相關文章
相關標籤/搜索