細數iOS中的線程同步方案(二)

細數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;

完整demo

參考:
不再安全的 OSSpinLock

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章