iOS開發多線程知識梳理

我只是一個搬運工,僅僅爲了加深記憶,感謝做者分享,文章大部分來源:進擊的蝸牛君git

線程和進程


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

  • 進程(Process )

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

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

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

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

爲何要用多線程編程


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

NSThread


經常使用APIbash

- (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;方法後,該線程處於就緒狀態,系統爲其建立方法調用棧和程序計數器,此時並無運行,什麼時候運行取決於系統調度。 多線程

  • 終止子線程

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

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

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

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
/** 獲取隊列 */
    //獲取指定優先級的全局併發隊列(flag填0便可,僅預留的參數,使用其餘值可能會返回null)
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //建立自定義並行隊列
    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, ^{
        
    });
    
    //異步提交代碼塊到串行隊列,線程池將在指定時間執行代碼塊(實際是5秒後加入到隊列中,實際並不必定會立馬執行,通常精度要求下是沒問題的)
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5*NSEC_PER_SEC)), queue2, ^{
        
    });
    
    //異步提交代碼到自定義串行隊列,同步函數,不管是在串行仍是並行隊列中執行,都要執行完才返回,因此要防止線程阻塞和死鎖,time表示當前是第幾回(若是提交給併發隊列,會啓動五個線程來執行)
    dispatch_apply(5, queue3, ^(size_t time) {
        
    });
    
    //實際是個long類型變量,用於判斷該代碼塊是否被執行過
    static dispatch_once_t onceToken; 
    //主線程執行一次代碼塊
    dispatch_once(&onceToken, ^{
        
    });

    //等group執行完後,才能執行下一步
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
    /** 組(用於須要等待多個任務所有執行完再進行下一步) */
    dispatch_group_t group = dispatch_group_create();
    
    //併發執行的代碼塊1
    dispatch_group_async(group, queue, ^{
        
    });
    
    //併發執行的代碼塊2
    dispatch_group_async(group, queue, ^{
        
    });
    
    //等待兩個代碼塊執行完彙總
    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是對mutex遞歸鎖的封裝

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

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

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

iOS中的鎖


1. OSSpinLock(自旋鎖)

  • 等待鎖的線程處於忙等(busy-wait)狀態,一直佔用着CPU資源;
  • 目前已經再也不安全,可能會出現優先級翻轉問題;
  • 若是等待鎖的線程優先級較高,它會一直佔用着CPU資源,優先級低的線程就沒法釋放鎖;
  • 須要導入頭文件#import <libkern/OSatomic.h>
//初始化
OSSpinLock lock = OS_SPINLOCK_INIT;
//嘗試加鎖(若是須要等待就不加鎖,直接返回false;若是不須要等待加鎖,返回true)
bool resule = OSSpinLockTry(&lock);
//加鎖
OSSpinLock(&lock);
//解鎖
OSSpinLockUnlock(&lock);
複製代碼

2. os_unfair_lock

  • 用於取代不安全的OSSpinLock,從iOS10開始支持;
  • 從底層調用看,等待os_unfair_locks鎖的線程會處於休眠狀態,並不是忙等;
  • 須要導入頭文件#import <os/lock.h>
//初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//嘗試加鎖
os_unfair_lock_trylock(&lock);
//加鎖
os_unfair_lock_lock(&lock);
//解鎖
os_unfair_lock_unlock(&lock);
複製代碼

3. pthread_mutex

互斥鎖

  • mutex叫作「互斥鎖」,等待的線程會處於休眠狀態
  • 須要導入頭文件#import <pthread.h>
//初始化鎖的屬性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_NORMAL);
//初始化
pthread_mutex_t mutex;
pthread_mutex_init (&mutex,&attr);
//嘗試加鎖
pthread_mutex_trylock (&mutex);
//加鎖
pthread_mutex_lock (&mutex);
//解鎖
pthread_mutex_unlock (&mutex);
//銷燬相關資源
pthread_mutexattr_unlock(&attr);
pthread_mutex_destroy(&mutex);
複製代碼

遞歸鎖

  • 遞歸鎖:容許同一個線程對一把鎖進行重複加鎖
// 初始化屬性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_RECURSIVE);
// 初始化鎖
pthread_mutex_t mutex;
pthread_mutex_init(mutex, &attr);
// 銷燬屬性
pthread_mutexattr_destroy(&attr);
複製代碼

條件

// 初始化鎖
pthread_mutex_t mutex;
//NULL表明使用默認屬性
pthread_mutex_init(&mutex, NULL);
// 初始化條件
pthread_cond_t cond;
pthread_cond_init(&cond, NULL);
//等待條件(進入休眠,放開mutex鎖;被喚醒後,會再次對mutex加鎖)
pthread_cond_wait(&cond, &mutex);
//激活一個等待條件的線程
pthread_cond_signal(&cond);
//銷燬資源
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
複製代碼

4. NSLock、NSRecursiveLock

  • NSLock是對mutex普通鎖的封裝
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end

@interface NSLock : NSObject <NSLocking>
{
- (BOOL)tryLock;
- (BOOl)lockBeforeDate:(NSDate *)limit;
}
@end

//初始化鎖
NSLock *lock = [[NSLock alloc] init];
複製代碼
  • NSRecursiveLock也是對mutex遞歸所得封裝,API跟NSLock基本一致。

5. NSCondition

  • NScondition 是對mutex和cond的封裝
@interface NSCondition : NSObject <NSLocking>
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;
複製代碼

6. NSConditionLock

  • NSConditionLock是對NSCondition的進一步封裝,能夠設置具體的條件值
@interface NSConditionLock : NSObject <NSLocking> {
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
}
@end
複製代碼

7. dispatch_semaphore

  • semaphore叫作信號量;
  • 信號量的初始值,能夠用來控制線程併發訪問的最大數量;
  • 信號量的初始值爲1,表明同時只容許1條線程訪問資源,保證線程同步
//信號量的初始值
int value = 1;
//初始化信號量
dispatch_semaphore semephore = dispatch_semaphore_creat(value);
//若是信號量的值<=0,當前線程就會進入休眠等待(直到信號量的值>0)
//若是信號量的值>0, 就減1,而後往下執行後面的代碼
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
//讓信號量的值加1
dispatch_semaphore_signal(semaphore);
複製代碼

8. dispatch_queue

  • 直接使用GCD的串行隊列,也是能夠實現線程同步的
dispatch_queue_t queue = dispatch_queue_creat("lock_queue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
//任務
})
複製代碼
相關文章
相關標籤/搜索