在學習iOS多線程應用以前,咱們先來學習一下什麼是線程?php
以上線程調度說的是單核設備,多核設備能夠經過並行來同時執行多個線程ios
在iOS中有四種多線程方案,對好比下算法
方案 | 簡介 | 語言 | 線程生命週期 | 使用頻率 |
---|---|---|---|---|
pthread | 一套通用的多線程API 適用於Unix\Linux\Windows等系統 跨平臺\可移植 使用難度大 |
C | 開發者手動管理 | 幾乎不用 |
NSThread | 底層是pthread 使用更加面向對象 使用方便,能夠執行操做線程對象 |
OC | 開發者手動管理 | 偶爾使用 |
GCD | 替代NSThread 充分利用設備的多核 |
C | 自動管理 | 經常使用 |
NSOperation | 對GCD的封裝 使用更加面向對象 增長了一些使用功能 |
OC | 自動管理 | 經常使用 |
pthread是基於c語言的一套多線程API,正是由於底層是C語言,因此pthread可以在不一樣的操做系統上使用,移植性很強。可是pthread使用起來特別麻煩,並且須要手動管理線程的聲明週期,所以基本不多使用,此處也不作過多介紹。swift
NSThread是蘋果官方提供的一套操做線程的API,它是面向對象的,而且是輕量級的,使用靈活。可是和pthread同樣,NSThread也須要開發者手動管理線程的生命週期。所以也不多使用,可是NSThread提供了一些很是實用的方法數組
#pragma mark - 線程建立
//獲取當前線程
+(NSThread *)currentThread;
//建立線程後自動啓動線程
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;
//線程休眠,可設置休眠結束時間
+ (void)sleepUntilDate:(NSDate *)date;
//線程休眠多久
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
//取消線程
- (void)cancel;
//啓動線程
- (void)start;
//退出線程
+ (void)exit;
// 得到主線程
+ (NSThread *)mainThread;
//初始化方法
- (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument NS_AVAILABLE(10_5, 2_0);
//是否正在執行
- (BOOL)isExecuting NS_AVAILABLE(10_5, 2_0);
//是否執行完成
- (BOOL)isFinished NS_AVAILABLE(10_5, 2_0);
//是否取消線程
- (BOOL)isCancelled NS_AVAILABLE(10_5, 2_0);
- (void)cancel NS_AVAILABLE(10_5, 2_0);
//線程啓動
- (void)start NS_AVAILABLE(10_5, 2_0);
#pragma mark - 線程通訊
//與主線程通訊
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
// equivalent to the first method with kCFRunLoopCommonModes
//與其餘子線程通訊
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
// equivalent to the first method with kCFRunLoopCommonModes
//隱式建立並啓動線程
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg NS_AVAILABLE(10_5, 2_0);
複製代碼
NSThread的用法也很是簡單,這裏不作介紹,有興趣的同窗能夠根據系統提供的API去進行嘗試。安全
NSThread在日常開發中也有使用,例如咱們常用[NSThread currentThread]來獲取當前線程,使用[NSThread mainThread]來獲取主線程。線程保活也是基於NSThread和RunLoop來實現的。bash
GCD是蘋果爲解決多核設備並行運算而提出的解決方案,它會合理的利用CPU多核的特性。而且GCD可以自動管理線程的生命週期(好比建立線程、任務調度、銷燬線程等等),咱們只須要告訴GCD具體要執行的任務,不須要編寫任何關於線程的代碼。同時GCD結合block使用更加簡潔,所以在多線程開發中,GCD是首選。網絡
在學習GCD以前,首先來學習兩個比較重要的概念:任務和隊列多線程
任務其實就是咱們須要執行的操做,在GCD中,咱們一般將須要執行的操做放在block中。執行任務有兩種方式:同步和異步。併發
所以,同步和異步最大的區別就是:是否具備開闢新線程的能力。
在GCD中,隊列主要分爲兩種:串行隊列和併發隊列
串行隊列和併發隊列任務的插入方式都遵循FIFO(先進先出)原則,也就是新的任務總會插入到隊列的末尾,可是串行隊列中先進入隊列的任務會先執行,而且等到任務執行完以後纔會執行後面的任務。而併發隊列則會同時執行隊列中的多個任務,而且任務之間不會相互等待,任務的執行順序和執行過程也不可預測。
GCD的使用步驟其實很簡單,主要分爲兩個步驟
GCD中的隊列有兩種,串行隊列和併發隊列,除此以外,GCD還提供了兩種特殊的隊列,一種是主隊列(其實就是一個串行隊列),一種是全局隊列(併發隊列)。
建立隊列是經過dispatch_queue_create函數,它有兩個參數:
建立隊列的代碼以下:
//建立串行隊列
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
//建立併發隊列
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
//獲取全局併發隊列(參數1:隊列優先級 參數二:保留字段,通常傳0)
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//獲取主隊列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
複製代碼
這裏須要注意的是:主隊列其實就是一個普通的串行隊列,任何添加到主隊列的任務都會在主線程中執行
GCD中,添加任務的方式也有兩種,使用dispatch_sync建立同步任務和使用dispatch_async建立異步任務。無論是建立同步任務仍是異步任務,都須要指定隊列dispatch_queue_t
//建立串行隊列
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"任務1");
dispatch_async(serialQueue, ^{
sleep(3);
NSLog(@"任務2--%@",[NSThread currentThread]);
});
NSLog(@"任務3");
dispatch_sync(serialQueue, ^{
sleep(1);
NSLog(@"任務4--%@",[NSThread currentThread]);
});
NSLog(@"任務5");
複製代碼
最終輸出的結果以下:
任務1和任務3先打印,以後纔會打印任務2。執行完任務2以後,纔會執行任務4,而且執行完任務4,最後纔會執行任務5。由此就能夠驗證上文中的結論:
//建立併發隊列
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"任務1");
dispatch_async(concurrentQueue, ^{
NSLog(@"開始任務2");
sleep(3);
NSLog(@"任務2--%@",[NSThread currentThread]);
});
NSLog(@"任務3");
dispatch_sync(concurrentQueue, ^{
NSLog(@"開始任務4");
sleep(3);
NSLog(@"任務4--%@",[NSThread currentThread]);
});
NSLog(@"任務5");
複製代碼
執行結果以下:
隊列存在兩種:串行隊列和併發隊列,加上系統提供的主隊列總共三種隊列(此處因爲主隊列中添加的任務都會在主線程中執行,所以將主隊列單獨做爲一種特殊的隊列)。
任務又分爲兩種:同步任務和異步任務,所以隊列加任務共有6種組合,所產生的效果及對好比下:
串行隊列(手動建立) | 主隊列 | 併發隊列 | |
---|---|---|---|
同步任務(sync) | ●不會開闢新線程 ●串行執行任務 |
產生死鎖 | ●不會開闢新線程 ●串行執行任務 |
異步任務(async) | ●開闢新線程 ●串行執行任務 |
●不會開闢新線程 ●串行執行任務 |
●開闢新線程 ●併發執行任務 |
還要注意一點:當使用sync向主隊列中添加同步任務時,會產生死鎖。此處暫時不考慮任務嵌套。
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"任務1");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"任務2");
});
NSLog(@"任務3");
}
複製代碼
執行圖以下
首先,在執行viewDidLoad方法時,實際上是將viewDidLoad添加到主隊列中,由於viewDidLoad如今是在隊首,因此先執行viewDidLoad方法。
viewDidLoad中有3個任務,都是在主線程中執行,當執行完任務1後,經過dispatch_sync方法又向主隊列中添加了任務2(實際上是整個block,這裏暫且稱爲任務2),可是因爲同步任務的特性是必現執行完且返回才能執行後面的任務,所以必需要執行完任務2才能執行後面的任務3。
此時在主隊列中存在兩個任務,viewDidLoad和任務2,任務2想要執行,就必須等待viewDidLoad執行完,而viewDidLoad想要執行完,必需要執行完任務2以及任務3,可是任務3想要執行,就必須執行完任務2,所以任務2在等待viewDidLoad執行完,viewDidLoad又在等待任務2執行完,從而形成死鎖。
//建立串行隊列
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{ //此處稱爲block1
NSLog(@"任務1");
dispatch_sync(serialQueue, ^{ //此處稱爲block2
NSLog(@"任務2");
});
NSLog(@"任務3");
});
複製代碼
執行圖以下:
首先,經過dispatch_async添加異步任務時會開啓新的線程,因此此時block1中的任務是在子線程中執行,同時由於是在串行隊列中增長的異步任務,因此block1會被添加到串行隊列中去,而且在隊首。
在子線程中執行block1中的方法,先執行任務1,而後執行dispatch_sync方法,此時會向串行隊列中增長同步任務block2,而且須要等到block2執行完成以後纔會執行任務3。
此時在串行隊列中存在兩個任務,block1和block2,block2想要執行,就必須等待block1執行完,而block1想要執行完,必需要執行完block2以及任務3,可是任務3想要執行,又必須執行完block2,所以block1在等待block2執行完,block2又在等待block1執行完,從而形成死鎖。
//建立串行隊列
dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
NSLog(@"任務1");
dispatch_sync(queue, ^{
NSLog(@"任務2");
});
NSLog(@"任務3");
});
複製代碼
其實這種死鎖的方式和第一種相似,同步任務仍是在主線程執行,只不過被添加到了自定義的串行隊列中,所以形成死鎖的緣由和第一種基本相同,這裏不作介紹。
柵欄方法主要是在多組操做之間增長柵欄,從而分割多組操做,使得各組操做之間順序執行。例如:有兩組操做,須要執行完第一組操做以後再執行第二組操做,此時就須要用到dispatch_barrier_async,代碼以下:
//建立併發隊列
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
//任務組一
for (int i = 0; i < 5; i++) {
dispatch_async(concurrentQueue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"執行組一任務%d",i);
});
}
//柵欄方法
dispatch_barrier_sync(concurrentQueue, ^{
NSLog(@"柵欄方法");
});
//任務組二
for (int i = 0; i < 5; i++) {
dispatch_async(concurrentQueue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"執行組二任務%d",i);
});
}
複製代碼
前提是全部的任務都須要添加到同一個隊列中
執行結果以下:
能夠看出任務組一中的5個任務併發執行,執行完成以後會先執行柵欄函數,最後纔會執行任務組二中的全部操做,具體以下圖:
還有一點須要注意的是,這個函數傳入的併發隊列必須是經過dispatch_queue_create手動建立的,若是傳入的是一個串行或者一個全局的併發隊列,那麼這個函數的效果等同於dispatch_async函數
隊列組是一個很是實用的功能,它能夠在一組異步任務都執行完成以後,再執行下一步操做。例如:有多個接口,須要等到全部的接口返回結果以後再到主線程更新UI。
隊列組有三種使用方法:
- (void)testGroup1{
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, concurrentQueue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"網絡任務1:%@", [NSThread currentThread]);
});
dispatch_group_async(group, concurrentQueue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"網絡任務2:%@", [NSThread currentThread]);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"主線程更新UI:%@", [NSThread currentThread]);
});
}
複製代碼
- (void)testGroup2{
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
dispatch_async(concurrentQueue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"網絡任務1:%@", [NSThread currentThread]);
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(concurrentQueue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"網絡任務2:%@", [NSThread currentThread]);
dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"主線程更新UI:%@", [NSThread currentThread]);
});
}
複製代碼
- (void)testGroup3{
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, concurrentQueue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"網絡任務1:%@", [NSThread currentThread]);
});
dispatch_group_async(group, concurrentQueue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"網絡任務2:%@", [NSThread currentThread]);
});
//等待上面的任務所有完成後,會往下繼續執行(會阻塞當前線程)
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"主線程更新UI:%@", [NSThread currentThread]);
});
}
複製代碼
以上三種方式的執行結果相同,以下:
信號量就是一種用來控制訪問資源的數量的標識,當咱們設置了一個信號量,在線程訪問以前加上信號量的處理,就能夠告知系統按照咱們設定的信號量數量來執行多個線程。信號量實際上是用計數來實現的,若是信號量計數小於0,則會一直等待,阻塞線程。若是信號量計數爲0或者大於0,則不等待且計數-1。
GCD提供了三個方法來幫助咱們使用信號量
函數 | 做用 |
---|---|
dispatch_semaphore_create | 建立信號量,初始值能夠爲0 |
dispatch_semaphore_signal | 發送信號,信號量計數+1 |
dispatch_semaphore_wait | 若是信號量>0,則使信號量-1,執行後續操做 若是信號量<=0,則會阻塞當前線程,直到信號量>0 |
示例代碼以下:
- (void)testSemaphore{
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
//信號量初始爲0
dispatch_semaphore_t seq = dispatch_semaphore_create(0);
NSLog(@"任務1");
dispatch_async(concurrentQueue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"任務2");
//信號量+1
dispatch_semaphore_signal(seq);
});
//此時信號量小於0,因此一直等待,當信號量>=0時執行後續代碼
dispatch_semaphore_wait(seq, DISPATCH_TIME_FOREVER);
dispatch_async(concurrentQueue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"任務3");
//信號量+1
dispatch_semaphore_signal(seq);
});
//信號量-1
dispatch_semaphore_wait(seq, DISPATCH_TIME_FOREVER);
NSLog(@"任務4");
}
複製代碼
執行結果以下:
首先會執行任務1,而後往併發隊列中添加異步任務,以後執行dispatch_semaphore_wait時,信號量-1,此時信號量小於0(初始爲0),所以線程被阻塞,一直在此處等待。當任務2執行完成後,會調用dispatch_semaphore_signal,此時信號量+1,程序繼續往下執行。
所以,信號量也能夠用來實現多個異步任務順序執行,以及多個異步任務所有執行結束以後統一執行某些操做的需求。
NSOperation實際上是對GCD更高一層的封裝,徹底面向對象,使用起來比GCD更加簡單易用,代碼的可讀性也更高。而且NSOperation也提供了一些GCD沒有提供的更加實用的功能。好比:
NSOperation是一個抽象類,不能直接使用。想要使用他的功能,就要使用它的子類NSInvocationOperation和NSBlockOperation。也能夠自定義NSOperation的子類。
NSBlockOperation是將任務存放到block中,在合適的時機進行調用。
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任務1:%@", [NSThread currentThread]);
}];
[operation1 start];
複製代碼
而且NSBlockOperation還能夠經過addExecutionBlock:方法添加額外操做,而且經過addExecutionBlock:添加的任務和經過blockOperationWithBlock:添加的任務能夠在不一樣的線程中併發執行。
- (void)testBlock{
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"主任務:%@",[NSThread currentThread]);
}];
[op addExecutionBlock:^{
NSLog(@"附加任務1:%@",[NSThread currentThread]);
}];
[op addExecutionBlock:^{
NSLog(@"附加任務2:%@",[NSThread currentThread]);
}];
[op start];
}
複製代碼
執行結果以下:
經過blockOperationWithBlock:建立的任務默認會在當前線程中同步執行,可是當blockOperationWithBlock:和addExecutionBlock:同時使用,而且addExecutionBlock:添加的任務足夠多時,blockOperationWithBlock:建立的任務也會在子線程中執行。
經過addExecutionBlock:添加任務必定會開闢新的線程,在新線程中執行附加任務。
NSInvocationOperation能夠指定target和selector
- (void)testOp{
NSInvocationOperation *invocationOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(opeartion) object:nil];
[invocationOp start];
}
- (void)opeartion{
NSLog(@"任務%@", [NSThread currentThread]);
}
複製代碼
默認狀況下,NSInvocationOperation在調用start方法的時候不會開啓線程,會在當前線程同步執行,只有當operation被添加到NSOperationQueue中才會開啓新線程異步執行操做。
NSOperation能夠設置任務之間的依賴,使任務按照預約的依賴順序執行
- (void)testOp{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任務一:%@",[NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任務二:%@",[NSThread currentThread]);
}];
NSInvocationOperation *op3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(testOp) object:nil];
//任務二依賴任務一
[op2 addDependency:op1];
//任務三依賴任務二
[op3 addDependency:op2];
[queue addOperations:@[op1, op2, op3] waitUntilFinished:NO];
}
- (void)methond3{
NSLog(@"任務三:%@",[NSThread currentThread]);
}
複製代碼
本來三個任務是併發執行,可是添加完依賴以後就變成了順序執行,以下:
此時由於3個任務順序執行,因此只需開闢一條線程便可。
NSOperation中也有隊列的概念,就是NSOperationQueue,一般NSOperationQueue和NSOperation會結合使用,一旦NSOperation被添加到NSOperationQueue時,會自動開闢新的線程異步執行
- (void)testOperation{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任務1:%@", [NSThread currentThread]);
}];
[operation1 start];
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任務2, %@", [NSThread currentThread]);
}];
[queue addOperation:operation2];
}
複製代碼
執行結果以下:
能夠看到,任務1沒有添加到NSOperationQueue中,在主線程中執行,任務2添加到NSOperationQueue中,在子線程中執行。
注意:NSOperation添加到NSOperationQueue後會自動執行start方法,無需手動調用。
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//設置最大併發數
queue.maxConcurrentOperationCount = 1;
for (int i = 0; i < 5; i++) {
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任務%d,%@",i, [NSThread currentThread]);
}];
[queue addOperation:op];
}
複製代碼
代碼中將最大併發數設置爲1,任務就會順序執行,結果以下:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//掛起任務
queue.suspended = YES;
//恢復任務
queue.suspended = NO;
//取消隊列中全部任務(已經開始的沒法取消)
[queue cancelAllOperations];
複製代碼
//獲取當前隊列
[NSOperationQueue currentQueue];
//獲取主隊列
[NSOperationQueue mainQueue];
複製代碼
//設置操做優先級
@property NSOperationQueuePriority queuePriority;
複製代碼
//操做是否正在執行
@property (readonly, getter=isExecuting) BOOL executing;
//操做是否完成
@property (readonly, getter=isFinished) BOOL finished;
//操做是不是併發執行
@property (readonly, getter=isConcurrent) BOOL concurrent;
//操做是不是異步執行
@property (readonly, getter=isAsynchronous) BOOL asynchronous;
//操做是否準備就緒
@property (readonly, getter=isReady) BOOL ready;
複製代碼
//操做是否被取消
@property (readonly, getter=isCancelled) BOOL cancelled;
//取消操做
- (void)cancel;
複製代碼
//添加任務依賴
- (void)addDependency:(NSOperation *)op;
//移除任務依賴
- (void)removeDependency:(NSOperation *)op;
//獲取當前任務的全部依賴
@property (readonly, copy) NSArray<NSOperation *> *dependencies;
//阻塞任務執行線程,直到該任務執行完成
- (void)waitUntilFinished;
//在當前任務執行完成以後調用completionBlock
@property (nullable, copy) void (^completionBlock)(void);
複製代碼
//添加單挑任務
- (void)addOperation:(NSOperation *)op;
//添加多個任務
- (void)addOperations:(NSArray<NSOperation *> *)ops;
//直接向隊列中添加一個NSBlockOperation類型的操做
- (void)addOperationWithBlock:(void (^)(void))block;
//在隊列中的全部任務都執行完成以後會執行barrier block,相似柵欄
- (void)addBarrierBlock:(void (^)(void))barrier;
複製代碼
//設置最大併發數
@property NSInteger maxConcurrentOperationCount;
複製代碼
//掛起\恢復隊列操做 YES:掛起 NO:恢復
@property (getter=isSuspended) BOOL suspended;
//取消隊列中全部操做
- (void)cancelAllOperations;
//阻塞當前線程,直到隊列中的操做所有執行完
- (void)waitUntilAllOperationsAreFinished;
複製代碼
//獲取當前隊列
@property (class, readonly, strong, nullable) NSOperationQueue *currentQueue;
//獲取主隊列
@property (class, readonly, strong) NSOperationQueue *mainQueue;
複製代碼
在單線程條件下,任務都是串行執行,因此不存在安全問題,多線程可以極大的提升程序運行效率,可是多線程也存在隱患。當多個線程訪問同一塊資源時,很是容易引起數據錯亂和數據安全問題。例如:如今有兩條線程同時訪問和修改同一個變量,以下:
線程A和線程B同時讀取Integer的值,都爲17,而後又同時對Integer的值+1,以後在修改Integer的值時因爲線程A和線程B併發執行,所以兩個線程會同時將Integer的值改成18,從而致使數據錯亂。解決辦法就是使用線程同步技術,就是讓線程按預約的前後順序依次執行。常見的線程同步技術是:加鎖。以修改Integer的值爲例,使用線程同步技術後結果以下:
線程A在訪問Integer前先進行加鎖操做,此時線程B沒法訪問Integer,而後線程A讀取Integer的值,改成18,而後進行解鎖,此時線程B就可以訪問Integer,先進行加鎖,讀取Integer值爲18,而後修改成19,最後再解鎖。所以,使用加鎖技術,就可以解決多線程的安全問題。
iOS中常見的線程同步技術有如下幾種,咱們以一個簡單的Demo來對比一下這幾種線程同步技術。
示例:假設如今銀行帳戶上有5000元,使用多線程,分屢次在銀行帳戶上存錢取錢,保證最後銀行存款正確。
若是咱們使用多線程可是不使用線程同步技術的話,代碼以下:
- (void)moneyTest{
__block int totalMoney = 5000;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
[NSThread sleepForTimeInterval:2];
totalMoney+=100;
NSLog(@"存100,帳戶餘額:%d",totalMoney);
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
[NSThread sleepForTimeInterval:2];
totalMoney-=200;
NSLog(@"取200,帳戶餘額:%d",totalMoney);
}
});
}
複製代碼
若是按正常的流程,通過5次存錢和5次取錢,帳戶餘額應該最終變爲4500元,可是最終執行的結果缺大不相同,以下:
整個過程當中帳戶餘額的計算都有問題,同時,通過10次存取以後,帳戶餘額還剩4700元,這就是多線程使用帶來的弊端。
如今,咱們就用如下技術來解決存錢取錢的問題
OSSpinLock叫作「自旋鎖」,顧名思義,線程在等待解鎖的過程當中會處於忙等狀態,而且一直會佔用CPU資源。
OSSpinLock如今已經再也不安全,由於它會出現優先級反轉的問題,即優先級低的線程首先得到鎖,進行加鎖操做,CPU會給它分配資源來執行後續任務,若是此時有高優先級的線程進入,那麼CPU會優先給高優先級的線程分配資源,此時低優先級線程得不到資源沒法釋放鎖,而高優先級線程因爲在等待低優先級線程解鎖,並且是處於忙等狀態,一直佔用着CPU資源。所以就致使優先級反轉的問題。
OSSpinLock具體Api以下:
#import <libkern/OSAtomic.h>
//初始化鎖
OSSpinLock lock = OS_SPINLOCK_INIT;
//嘗試加鎖(若是須要等待,就不加鎖,直接返回false,若是不須要等待就加鎖,而且返回true)
bool result = OSSpinLockTry(&lock);
//加鎖
OSSpinLockLock(&lock);
//解鎖
OSSpinLockUnlock(&lock)
複製代碼
回到上述Demo,對存錢取錢操做進行加鎖,以下:
- (void)moneyTest{
//初始化鎖
__block OSSpinLock lock = OS_SPINLOCK_INIT;
__block int totalMoney = 5000;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
OSSpinLockLock(&lock); //加鎖
[NSThread sleepForTimeInterval:.1];
totalMoney+=100;
OSSpinLockUnlock(&lock);//解鎖
NSLog(@"存100,帳戶餘額:%d",totalMoney);
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
OSSpinLockLock(&lock); //加鎖
[NSThread sleepForTimeInterval:.1];
totalMoney-=200;
OSSpinLockUnlock(&lock);//解鎖
NSLog(@"取200,帳戶餘額:%d",totalMoney);
}
});
}
複製代碼
運行結果以下:
能夠看到,整個過程按照順序依次執行,先進行存錢,後進行取錢,最終帳戶餘額爲4500元,解決了數據錯亂的問題。
os_unfair_lock被用來取代OSSpinLock,而且從iOS 10開始支持os_unfair_lock。等待鎖的線程會處於休眠狀態(不一樣於OSSpinLock的忙等狀態),不會佔用CPU資源。所以,使用os_unfair_lock不會致使優先級反轉的問題。
os_unfair_lockApi以下:
#import <os/lock.h>
//初始化鎖
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//嘗試加鎖
bool result = os_unfair_lock_trylock(&lock);
//加鎖
os_unfair_lock_lock(&lock);
//解鎖
os_unfair_lock_unlock(&lock);
複製代碼
使用方式同OSSpinLock。
pthread_mutex稱爲互斥鎖,即當一個線程得到某一共享資源的使用權以後,會將該資源進行加鎖,若是此時有其它線程想要獲取該資源的鎖,那麼它將會被阻塞進入睡眠狀態,直到該資源被解鎖後纔會喚醒。若是有多個線程嘗試獲取該資源的鎖,那麼它們都會進入睡眠狀態,一旦該資源被解鎖,這些線程就都會被喚醒,可是真正能得到資源使用權的是第一個被喚醒的線程。
使用互斥鎖的線程在等待鎖的過程當中會處於休眠狀態,不會佔用CPU資源
pthread_mutex的Api以下:
#import <pthread.h>
/*
* Mutex type attributes
*/
#define PTHREAD_MUTEX_NORMAL 0 //默認類型,普通鎖
#define PTHREAD_MUTEX_ERRORCHECK 1 //檢錯鎖
#define PTHREAD_MUTEX_RECURSIVE 2 //遞歸鎖
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL
//初始化鎖屬性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
//初始化鎖,第二個參數能夠傳NULL,就是使用默認的屬性
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);
//嘗試加鎖
pthread_mutex_trylock(&mutex);
//加鎖
pthread_mutex_lock(&mutex);
//解鎖
pthread_mutex_unlock(&mutex);
//銷燬
pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&mutex);
複製代碼
使用pthread_mutex對存錢取錢進行加鎖,以下:
- (void)moneyTest{
//初始化鎖
__block pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
__block int totalMoney = 5000;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
pthread_mutex_lock(&mutex); //加鎖
[NSThread sleepForTimeInterval:.1];
totalMoney+=100;
pthread_mutex_unlock(&mutex);//解鎖
NSLog(@"存100,帳戶餘額:%d",totalMoney);
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
pthread_mutex_lock(&mutex); //加鎖
[NSThread sleepForTimeInterval:.1];
totalMoney-=200;
pthread_mutex_unlock(&mutex);//解鎖
NSLog(@"取200,帳戶餘額:%d",totalMoney);
}
});
//在不使用鎖時須要調用此方法對鎖進行銷燬
//pthread_mutexattr_destroy(&mutex);
}
複製代碼
在初始化鎖時,咱們能夠指定鎖的類型爲PTHREAD_MUTEX_RECURSIVE,此時咱們就建立了一個遞歸鎖。遞歸鎖是指同一個線程能夠屢次得到某一個共享資源的鎖(屢次進行加鎖操做),別的線程想要獲取該資源鎖,就必須等待該線程釋放全部次數的鎖。下面咱們就建立一個遞歸函數的Demo來了解一下遞歸鎖的使用:
#import "XLMutexRecursiveTest.h"
#import <pthread.h>
@interface XLMutexRecursiveTest ()
@property(nonatomic, assign)pthread_mutex_t mutex;
@end
@implementation XLMutexRecursiveTest
- (instancetype)init
{
self = [super init];
if (self) {
//遞歸鎖:容許同一個線程對同一把鎖進行重複加鎖
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
//pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
//初始化鎖
pthread_mutex_init(&_mutex, &attr);
//銷燬屬性
pthread_mutexattr_destroy(&attr);
}
return self;
}
- (void)recursiveTask{
pthread_mutex_lock(&_mutex);
NSLog(@"recursiveTask");
static int count = 0;
if (count < 5) {
count++;
[self recursiveTask];
}
pthread_mutex_unlock(&_mutex);
}
- (void)dealloc{
pthread_mutex_destroy(&_mutex);
}
@end
複製代碼
首先建立普通鎖PTHREAD_MUTEX_NORMAL,而後實例化XLMutexRecursiveTest實例進行調用
XLMutexRecursiveTest *recursiveTest = [[XLMutexRecursiveTest alloc] init];
[recursiveTest recursiveTask];
複製代碼
執行以後發現程序會一直卡死在第一次打印NSLog的地方。這是由於當首次執行recursiveTask方法時會對_mutex進行加鎖,而後執行NSLog,當count < 5時,會再次執行recursiveTask方法,此時會發現_mutex已經被加鎖了,所以第二次執行的recursiveTask方法會一直在等待解鎖,而第一次執行的recursiveTask方法想要解鎖,就必需要等第二次的任務執行完成,所以就形成了死鎖。
下面咱們將鎖改爲遞歸鎖,從新執行,會發現全部的任務都正常打印了,以下
注意:在不使用pthread_mutex時要調用pthread_mutexattr_destroy和pthread_mutex_destroy對鎖及其屬性進行銷燬。
條件變量是在多線程中用來實現「等待->喚醒」邏輯的經常使用方式,相似於GCD中的信號量。條件變量是利用一個全局共享變量來進行線程同步。它主要分爲三步:
而且爲了防止資源競爭,一般將條件變量和互斥鎖結合使用。由於條件變量一般是多個線程或者進程的共享變量,因此就極有可能產生資源競爭,因此在使用條件變量以前須要對其加上互斥鎖。pthread_mutex關於條件變量使用的Api以下:
//初始化鎖
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
//初始化條件
pthread_cond_t condt;
pthread_cond_init(&condt, NULL);
//等待條件(此時會進入休眠狀態,同時對mutex進行解鎖,被再次喚醒後,會對mutex再次加鎖)
pthread_cond_wait(&condt, &mutex);
//激活一個等待該條件的線程
pthread_cond_signal(&condt);
//激活全部等待該條件的線程
pthread_cond_broadcast(&condt);
//銷燬
pthread_cond_destroy(&condt);
pthread_mutex_destroy(&mutex);
複製代碼
條件變量比較典型的應用即是生產者-消費者模式,下面就模擬生產者-消費者來建立一個簡單的Demo瞭解一下條件變量加互斥鎖的使用,代碼以下:
#import "XLMutexConditionLockTest.h"
#import <pthread.h>
@interface XLMutexConditionLockTest ()
//杯子餘量
@property(nonatomic, strong)NSMutableArray *cupsRemain;
@property(nonatomic, assign)pthread_mutex_t mutex;
@property(nonatomic, assign)pthread_cond_t condt;
@end
@implementation XLMutexConditionLockTest
- (instancetype)init
{
self = [super init];
if (self) {
//初始化鎖
pthread_mutex_init(&_mutex, NULL);
//初始化條件
pthread_cond_init(&_condt, NULL);
}
return self;
}
- (void)testSaleAndProduce{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self _saleCup];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self _produceCup];
});
}
//出首杯子
- (void)_saleCup{
pthread_mutex_lock(&_mutex);
if (self.cupsRemain.count == 0) {
//若是杯子餘量爲0,則等待生產杯子
NSLog(@"當前無可用杯子庫存");
pthread_cond_wait(&_condt, &_mutex);
}
//此時有可出售的杯子
[self.cupsRemain removeLastObject];
NSLog(@"售出一個杯子");
pthread_mutex_unlock(&_mutex);
}
//生產杯子
- (void)_produceCup{
pthread_mutex_lock(&_mutex);
//睡眠兩秒,模擬生產過程
sleep(2);
[self.cupsRemain addObject:@"yellow cup"];
NSLog(@"生產了一個黃色杯子");
//通知條件變量成立
pthread_cond_signal(&_condt);
pthread_mutex_unlock(&_mutex);
}
- (void)dealloc{
//銷燬
pthread_cond_destroy(&_condt);
pthread_mutex_destroy(&_mutex);
}
@end
複製代碼
執行結果以下:
能夠發現,雖然_produceCup方法睡眠2s執行,可是_saleCup方法仍是等待_produceCup執行完成以後再執行。由此能夠總結出整個條件變量的流程以下:
dispatch_semaphore叫作信號量,前面講GCD的時候也介紹過。dispatch_semaphore是經過設置一個全局的信號量,來控制線程併發訪問的最大數量。假設信號量初始值爲1,那麼表明同時只容許1條線程訪問資源,以此來保證線程同步。使用方式以下:
- (void)testDispatch{
//設置信號初始值
int semaphoreValue = 1;
//初始化信號量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(semaphoreValue);
__block int totalMoney = 5000;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
//若是此時信號量<=0,那麼dispatch_semaphore_wait會讓線程處於休眠等待狀態,直到信號量>0
//若是信號量>0,則執行dispatch_semaphore_wait會使信號量-1
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
[NSThread sleepForTimeInterval:.1];
totalMoney+=100;
//會對信號量進行+1操做
dispatch_semaphore_signal(semaphore);
NSLog(@"存100,帳戶餘額:%d",totalMoney);
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);//信號量-1
[NSThread sleepForTimeInterval:.1];
totalMoney-=200;
dispatch_semaphore_signal(semaphore);
NSLog(@"取200,帳戶餘額:%d",totalMoney);//信號量+1
}
});
}
複製代碼
初始信號量的值爲1,此時,調用dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)方法,會判斷信號量是否>0,若是信號量>0則會執行後續的操做,而且將信號量的值-1。若是信號量<=0,那麼此方法會使當前線程處於休眠等待狀態,直到信號量的值>0。
調用dispatch_semaphore_signal(semaphore)會使信號量+1,兩種方法搭配使用就能實現線程同步的效果。
dispatch_queue(DISPATCH_QUEUE_SERIAL)其實就是一個串行隊列,上文也說過,無論往串行隊列中添加同步任務仍是異步任務,在執行時都是串行執行任務。使用方式以下
- (void)testDispatchQueue{
//建立串行隊列
dispatch_queue_t queue = dispatch_queue_create("lock_queue", DISPATCH_QUEUE_SERIAL);
__block int totalMoney = 5000;
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[NSThread sleepForTimeInterval:.2];
totalMoney+=100;
NSLog(@"存100,帳戶餘額:%d",totalMoney);
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[NSThread sleepForTimeInterval:.2];
totalMoney-=200;
NSLog(@"取200,帳戶餘額:%d",totalMoney);//信號量+1
}
});
}
複製代碼
NSLock、NSRecursiveLock、NSCondition和NSConditionLock實際上是對pthread_mutex中普通鎖、遞歸鎖和條件變量的封裝,使其面向對象,使用起來更加簡單。使用方式其實和pthread_mutex差很少,這裏不作單獨介紹了,只列出經常使用Api
@protocol NSLocking
//加鎖
- (void)lock;
//解鎖
- (void)unlock;
@end
@interface NSLock : NSObject <NSLocking>
//嘗試加鎖
- (BOOL)tryLock;
//給鎖設置到期時間
- (BOOL)lockBeforeDate:(NSDate *)limit;
@end
複製代碼
@interface NSRecursiveLock : NSObject <NSLocking>
//嘗試加鎖
- (BOOL)tryLock;
//給鎖設置到期時間
- (BOOL)lockBeforeDate:(NSDate *)limit;
@end
複製代碼
NSCondition 實際上是封裝了一個互斥鎖和條件變量, 它把前者的 lock 方法和後者的 wait/signal 統一在 NSCondition 對象中,暴露給使用者。它的加鎖和解鎖過程同NSLock一致
@interface NSCondition : NSObject <NSLocking>
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;
@end
複製代碼
NSConditionLock是對NSCondition的再一次封裝,與NSCondition不一樣的是NSConditionLock能夠設置具體的條件值
@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
複製代碼
@synchronized內部其實封裝了一個mutex遞歸鎖。傳入一個obj參數,內部會自動生成obj對應的遞歸鎖,而且存放在哈希表中。經過obj的內存地址到哈希表中能拿到obj對應的遞歸鎖。想要了解@synchronized內部實現,能夠下載objc源碼,查看objc_sync.mm文件中的objc_sync_enter和objc_sync_exit函數。
@synchronized的使用很簡單,以下:
@synchronized (obj) {
//須要加鎖的代碼
}
複製代碼
將@synchronized應用到存錢取錢的案例中,以下:
- (void)testSynchronized{
__block int totalMoney = 5000;
NSObject *obj = [NSObject new];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (obj) {
for (int i = 0; i < 5; i++) {
[NSThread sleepForTimeInterval:.2];
totalMoney+=100;
NSLog(@"存100,帳戶餘額:%d",totalMoney);
}
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (obj) {
for (int i = 0; i < 5; i++) {
[NSThread sleepForTimeInterval:.2];
totalMoney-=200;
NSLog(@"取200,帳戶餘額:%d",totalMoney);
}
}
});
}
複製代碼
傳入的obj必須有值,若是obj傳nil,則@synchronized(nil)不起任何做用。同時要實現多線程同步的話,就必須傳入相同的obj
借用大神ibireme的再也不安全的 OSSpinLock一文中關於各類鎖的性能對比圖,以下:
分辨自旋鎖和互斥鎖的方式,能夠根據等待鎖的過程當中,線程是休眠仍是忙等狀態來區分。若是線程是休眠狀態。就是互斥鎖,若是是忙等狀態,就是自旋鎖。在OC中能夠跟蹤彙編代碼來判斷一個鎖是自旋鎖仍是互斥鎖。以OSSpinLock和os_unfair_lock爲例來進行彙編代碼跟蹤:
#import "XLLockTest.h"
#import <libkern/OSAtomic.h>
#import <os/lock.h>
@interface XLLockTest ()
@property(nonatomic, assign)OSSpinLock lock;
@end
@implementation XLLockTest
- (instancetype)init{
self = [super init];
if (self) {
_lock = OS_SPINLOCK_INIT;
}
return self;
}
- (void)test{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self thread2];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self thread1];
});
}
- (void)thread1{
OSSpinLockLock(&_lock);
NSLog(@"thread1");
OSSpinLockUnlock(&_lock);
}
- (void)thread2{
OSSpinLockLock(&_lock);
sleep(60);
NSLog(@"thread2");
OSSpinLockUnlock(&_lock);
}
複製代碼
斷點在thread1方法,調用test方法,使用LLDB指令si一步一步執行彙編代碼。首先進入OSSpinLockLock方法
在OSSpinLockLock方法內部調用了_OSSpinLockLockSlow函數
進入_OSSpinLockLockSlow函數,執行si指令,會發現,程序一直在循環執行一段彙編指令,以下:
熟悉彙編的同窗能夠看出其實這一段彙編代碼就是一個while循環,由此就能夠看出OSSpinLock屬於自旋鎖。
將Demo中的鎖換成os_unfair_lock,而後用相同的方式跟蹤彙編代碼。首先是進入os_unfair_lock_lock方法,方法內部會調用_os_unfair_lock_lock_slow函數
_os_unfair_lock_lock_slow函數內部會調用__ulock_wait函數
在__ulock_wait函數內部會調用syscall,syscall其實就是系統級別的函數,執行完syscall函數以後,當前線程就會進入休眠狀態。
由此就能夠看出os_unfair_lock屬於互斥鎖。
自旋鎖其實就是指當一個線程獲取到資源鎖以後,其它線程在獲取資源鎖時,會一直處於忙等狀態(busy-waiting)。處於忙等狀態的線程會一直處於活躍狀態,可是內部並無執行任何有效的任務,只是一直在循環查看資源鎖擁有者是否已經釋放了鎖。
如下狀況下適合使用自旋鎖
互斥鎖則是指當一個線程獲取到資源鎖以後,其它線程在獲取資源鎖時會被阻塞,進入睡眠狀態(sleep-waiting)。線程休眠以後不會佔用CPU資源,直到資源鎖被釋放以後纔會喚醒線程。
如下狀況下適合使用互斥鎖
在OC中可使用atomic或者nonatomic來修飾屬性,表明原子性和非原子性。其實通俗一點來講,使用atomic修飾的屬性是線程安全的,而使用nonatomic修飾的屬性不是線程安全的。爲何說atomic修飾的屬性是線程安全的呢?查看objc源碼中的objc-accessors.mm文件能夠看到atomic的底層實現,經過閱讀源碼能夠發現,atomic修飾屬性其實就是給屬性的setter和getter方法內部增長了自旋鎖,源碼以下:
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
......
if (!atomic) return *slot;
//從全局的哈希表中獲取到自旋鎖
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
return objc_autoreleaseReturnValue(value);
}
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) {
......
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
//從全局的哈希表中獲取到自旋鎖
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
......
}
複製代碼
可是atomic只是保證了getter和setter存取方法的線程安全,並不能保證整個對象是線程安全的。假設咱們使用atomic修飾NSArray類型的屬性
@property(atomic, strong)NSArray *sourceArray;
複製代碼
若是多個線程對sourceArray進行添加數據操做,確定會產生內存問題,由於atomic只是針對sourceArray自己的getter和setter方法,若是使用[_sourceArray objectAtIndex:index]時,就不是線程安全的,由於它和sourceArray的setter和getter方法沒有關係。想要保證[_sourceArray objectAtIndex:index]的線程安全,就須要對_sourceArray的使用進行加鎖操做。
在開發過程當中個,有一種比較特殊的狀況,就是在臨界區中有I/O操做時,若是咱們使用以上任何一種鎖來對臨界區進行加鎖,那麼在同一時間內只能執行一次讀或者寫的操做。可是多條線程同時執行讀的操做是不會有任何數據問題的,只有在多條線程同時執行讀寫操做時纔會形成數據問題。總結來講,就是要知足如下的幾種場景:
以上的場景就是典型的「多讀單寫」的操做,常用在文件等數據的讀寫操做。在iOS中想要實現這種效果,經常使用的方案有如下兩種:
關於dispatch_barrier_async的使用,上文GCD的部分有詳細介紹,此處主要介紹pthread_rwlock的使用。
pthread_rwlock主要Api以下:
- (void)testRwLock{
//初始化鎖
pthread_rwlock_t rwLock;
pthread_rwlock_init(&rwLock, NULL);
//讀操做-加鎖
pthread_rwlock_rdlock(&rwLock);
//讀操做-嘗試加鎖
pthread_rwlock_tryrdlock(&rwLock);
//寫操做-加鎖
pthread_rwlock_wrlock(&rwLock);
//讀操做-嘗試加鎖
pthread_rwlock_trywrlock(&rwLock);
//解鎖
pthread_rwlock_unlock(&rwLock);
//銷燬
pthread_rwlock_destroy(&rwLock);
}
複製代碼
模擬讀寫操做,代碼以下:
#import "XLLockTest.h"
#import <pthread.h>
@interface XLLockTest ()
@property(nonatomic, assign)pthread_rwlock_t rwlock;
@property (strong, nonatomic) dispatch_queue_t queue;
@end
@implementation XLLockTest
- (instancetype)init{
self = [super init];
if (self) {
pthread_rwlock_init(&_rwlock, NULL);
self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
}
return self;
}
- (void)test{
for (int i = 0; i < 10; i++) {
dispatch_async(self.queue, ^{
[self readThread];
});
dispatch_async(self.queue, ^{
[self writeThread];
});
}
}
- (void)readThread{
pthread_rwlock_rdlock(&_rwlock);
sleep(1);
NSLog(@"讀操做");
pthread_rwlock_unlock(&_rwlock);
}
- (void)writeThread{
pthread_rwlock_wrlock(&_rwlock);
sleep(1);
NSLog(@"寫操做");
pthread_rwlock_unlock(&_rwlock);
}
- (void)dealloc{
pthread_rwlock_destroy(&_rwlock);
}
複製代碼
調用XLLockTest的test方法,打印以下:
能夠看出,在同一時間內,可能會執行兩次讀操做,可是隻會執行一次寫操做。
以上內容純屬我的理解,若是有什麼不對的地方歡迎留言指正。
一塊兒學習,一塊兒進步~~~