細數iOS中的線程同步方案(一)
細數iOS中的線程同步方案(二)
NSLock
這個其實就是對pthread_mutex普通互斥鎖的封裝;面向對象,使用起來更方便;
- (void)lock;
- (void)unlock;
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
NSRecursiveLock 遞歸鎖
對pthread_mutex遞歸鎖的封裝,方法和上面的一樣;
NSCondition
對pthread_cond條件鎖的封裝,使用pthread_cond需要配合pthread_mutex互斥鎖使用,NSCondition封裝好了,一把鎖就能實現:
NSCondition *lock = [[NSCondition alloc] init];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
[lock lock];
while (self.condition_value <= 0) { // 條件成立則暫時解鎖並等待
[lock wait];
}
NSLog(@"%@===read===start",[NSThread currentThread]);
sleep(2);
NSLog(@"%@===read===end",[NSThread currentThread]);
[lock unlock];
}];
[queue addOperationWithBlock:^{
[lock lock];
NSLog(@"%@===write===start",[NSThread currentThread]);
sleep(3);
self.condition_value = 1; // 一定要更改條件 否則上面read線程條件成立又會wait
NSLog(@"%@===write===end",[NSThread currentThread]);
[lock signal]; // 傳遞信號給等待的線程 而且是在解鎖前
// [lock broadcast] // 通知所有線程
[lock unlock];
}];
NSConditionLock
對NSCondition的進一步封裝,在NSCondition基礎上,加了可控制的條件condition;通過條件變量,控制通知哪條線程;
@property (readonly) NSInteger condition;
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:1]; // 初始化,設置condition=1
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
[lock lockWhenCondition:1]; // 當condition=1時 獲取鎖成功 否則等待(但是首次使用lockWhenCondition時condition不對時也能獲取鎖成功)
NSLog(@"%@===A===start",[NSThread currentThread]);
sleep(2);
NSLog(@"%@===A===end",[NSThread currentThread]);
// unlock根據不同的條件 控制對應的線程
[lock unlockWithCondition:2]; // 解鎖,同時設置condition=2並signal;
// [lock unlockWithCondition:3];
}];
[queue addOperationWithBlock:^{
[lock lockWhenCondition:2];
NSLog(@"%@===B===start",[NSThread currentThread]);
sleep(1);
NSLog(@"%@===B===end",[NSThread currentThread]);
[lock unlock];
}];
[queue addOperationWithBlock:^{
[lock lockWhenCondition:3];
NSLog(@"%@===C===start",[NSThread currentThread]);
sleep(1);
NSLog(@"%@===C===end",[NSThread currentThread]);
[lock unlock];
}];
線程A解鎖時可以傳不同條件值,對應條件值的其他等待線程就會被喚醒;這裏條件值爲2,則執行線程B任務;條件設置爲3,則執行線程C任務;如果是其他值則線程B,C繼續一直等待;
NSThread: 0x282b66340>{number = 6, name = (null)}===A===start
NSThread: 0x282b66340>{number = 6, name = (null)}===A===end
NSThread: 0x282b68240>{number = 3, name = (null)}===B===start
NSThread: 0x282b68240>{number = 3, name = (null)}===B===end
@synchronized
是對mutex遞歸鎖的封裝;
@synchronized(obj)內部會生成obj對應的遞歸鎖,然後進行加鎖、解鎖操作;一個對象對應一把鎖;
NSObject *obj = [[NSObject alloc] init];
@synchronized (obj) {
// ...
}
GCD相關
dispatch_semaphore信號量
這個和上篇講的semaphore差不多;
// 創建信號量
dispatch_semaphore_t sem = dispatch_semaphore_create(1);
// 判斷信號量,如果=0則等待,否則信號值-1往下執行
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
// 發送信號量,信號值+1
dispatch_semaphore_signal(sem);
DISPATCH_QUEUE_SERIAL 串行隊列
串行隊列的任務就是同步執行的;
dispatch_queue_t queue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
// ThreadA dosomething....
});
dispatch_async(queue, ^{
// ThreadB dosomething....
});
dispatch_group
將任務分組,組內任務異步執行;當所有任務執行完後,可以通知其他線程執行任務:
// group必須使用自己創建的併發隊列 使用global全局隊列無效
dispatch_queue_t queue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
// dispatch_queue_t queue = dispatch_get_global_queue(0, 0); xxx
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
sleep(1);
NSLog(@"%@===TaskA",[NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
sleep(1);
NSLog(@"%@===TaskB",[NSThread currentThread]);
});
dispatch_group_notify(group, queue, ^{
NSLog(@"%@===TaskC",[NSThread currentThread]);
});
// dispatch_async(queue, ^{
// dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC))); // 可以設置等待的超時時間
// NSLog(@"%@===TaskC",[NSThread currentThread]);
// });
以上代碼對應的場景就是:A,B線程可以併發執行,但C線程一定要在AB線程執行完後再執行;
dispatch_group_notify也可以使用dispatch_group_wait替代,一樣是阻塞的作用,而dispatch_group_wait能設置等待超時時間;超過時間將不再阻塞,繼續任務;
還有一點需要注意的是,dispatch_group必須使用自己創建的併發隊列, 使用global全局隊列無效,使用串行隊列沒有意義;
dispatch_barrier
如同它的名字一樣,dispatch_barrier就是起到一個柵欄的作用;柵欄兩邊的任務可以併發執行,柵欄裏的任務必須等到柵欄上邊的任務執行完才執行,柵欄下邊的任務必須等柵欄裏的任務執行完後才執行;
dispatch_barrier其實就是阻塞隊列的作用;
這個其實也可以通過dispatch_group實現,但dispatch_barrier更加方便;
dispatch_queue_t queue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
sleep(1);
NSLog(@"%@===TaskA",[NSThread currentThread]);
});
dispatch_async(queue, ^{
sleep(1);
NSLog(@"%@===TaskB",[NSThread currentThread]);
});
// async不會阻塞當前線程(主線程)
dispatch_barrier_async(queue, ^{
NSLog(@"%@===Barrier",[NSThread currentThread]);
});
// sync會阻塞當前隊列(主隊列)
// dispatch_barrier_sync(queue, ^{
// NSLog(@"%@===Barrier",[NSThread currentThread]);
// });
dispatch_async(queue, ^{
sleep(1);
NSLog(@"%@===TaskC",[NSThread currentThread]);
});
dispatch_async(queue, ^{
sleep(1);
NSLog(@"%@===TaskD",[NSThread currentThread]);
});
NSLog(@"%@===MainTask",[NSThread currentThread]);
<NSThread: 0x2816bbc40>{number = 1, name = main}===MainTask
<NSThread: 0x2816fe440>{number = 3, name = (null)}===TaskB
<NSThread: 0x2816877c0>{number = 4, name = (null)}===TaskA
<NSThread: 0x2816877c0>{number = 4, name = (null)}===Barrier
<NSThread: 0x2816fe440>{number = 3, name = (null)}===TaskD
<NSThread: 0x2816877c0>{number = 4, name = (null)}===TaskC
dispatch_barrier的使用有兩種方式
- dispatch_barrier_async
- dispatch_barrier_sync
async不會阻塞當前隊列,sync同時會阻塞當前隊列;如果以上代碼換成dispatch_barrier_sync,最終的結果將是MainTask會在Barrier任務後;
基於barrier的這種特性,很容易實現一個讀寫鎖;柵欄內爲write,柵欄外爲read;這樣同樣能實現讀任務能異步執行,寫任務只能同步執行;同時在寫操作時,不允許讀操作;
dispatch_queue_t queue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 3; i ++) {
dispatch_async(queue, ^{
sleep(1);
NSLog(@"%@===read",[NSThread currentThread]);
});
}
for (int i = 0; i < 3; i ++) {
dispatch_barrier_async(queue, ^{
sleep(1);
NSLog(@"%@===write",[NSThread currentThread]);
});
}
for (int i = 0; i < 3; i ++) {
dispatch_async(queue, ^{
sleep(1);
NSLog(@"%@===read",[NSThread currentThread]);
});
}
<NSThread: 0x282a04880>{number = 4, name = (null)}===read
<NSThread: 0x282a13100>{number = 6, name = (null)}===read
<NSThread: 0x282a050c0>{number = 5, name = (null)}===read
<NSThread: 0x282a4ee40>{number = 1, name = main}===write
<NSThread: 0x282a4ee40>{number = 1, name = main}===write
<NSThread: 0x282a4ee40>{number = 1, name = main}===write
<NSThread: 0x282a050c0>{number = 5, name = (null)}===read
<NSThread: 0x282a13400>{number = 7, name = (null)}===read
<NSThread: 0x282a04880>{number = 4, name = (null)}===read
NSOperation相關
NSOperation是對GCD的封裝
最大併發數
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 最大併發數設置爲1,queue內任務同步執行
queue.maxConcurrentOperationCount = 1;
設置柵欄
// similarly to the `dispatch_barrier_async` function.
[queue addBarrierBlock:^{
}];
設置依賴關係
使用場景:線程B必須要等線程A任務執行完後才執行,即線程A依賴線程B:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *taskA = [NSBlockOperation blockOperationWithBlock:^{
sleep(2);
NSLog(@"%@===TaskA",[NSThread currentThread]);
}];
NSBlockOperation *taskB = [NSBlockOperation blockOperationWithBlock:^{
sleep(.5);
NSLog(@"%@===TaskB",[NSThread currentThread]);
}];
[taskB addDependency:taskA];
[queue addOperation:taskA];
[queue addOperation:taskB];
<NSThread: 0x281af5bc0>{number = 6, name = (null)}===TaskA
<NSThread: 0x281af5bc0>{number = 6, name = (null)}===TaskB
自旋鎖、互斥鎖比較
前面我們介紹了自旋鎖、互斥鎖機制的不同,它們各有優點;實際開發中的如何選擇呢?
適用自旋鎖的情況
- 線程等待時間比較短(這樣忙等的時間不會太長,不會有太大消耗)
- 加鎖的代碼(臨界區)經常被調用,但競爭情況很少發生
- CPU資源不緊張(自旋鎖比較耗CPU資源)
相反的,適用互斥鎖的情況
- 線程等待時間比較長
- 加鎖的代碼(臨界區)複雜,循環度大,或者有IO操作
- 加鎖的代碼(臨界區)競爭激烈
線程同步方案性能比較
這個直接引用大神的圖:
另外,os_unfair_lock鎖性能是最好的,可惜最低只支持iOS10;
參考:
不再安全的 OSSpinLock