線程安全與鎖

NSInteger total = 0;
- (void)threadNotSafe {
    for (NSInteger index = 0; index < 3; index++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            total += 1;
            NSLog(@"total: %ld", total);
            total -= 1;
            NSLog(@"total: %ld", total);
        });
    }
}
//第一次輸出:
2017-11-28 23:34:11.551570+0800 BasicDemo[75679:5312246] total: 1
2017-11-28 23:34:11.551619+0800 BasicDemo[75679:5312248] total: 3
2017-11-28 23:34:11.551618+0800 BasicDemo[75679:5312249] total: 2
2017-11-28 23:34:11.552120+0800 BasicDemo[75679:5312246] total: 2
2017-11-28 23:34:11.552143+0800 BasicDemo[75679:5312248] total: 1
2017-11-28 23:34:11.552171+0800 BasicDemo[75679:5312249] total: 0
//第二次輸出
2017-11-28 23:34:55.738947+0800 BasicDemo[75683:5313401] total: 1
2017-11-28 23:34:55.738979+0800 BasicDemo[75683:5313403] total: 2
2017-11-28 23:34:55.738985+0800 BasicDemo[75683:5313402] total: 3
2017-11-28 23:34:55.739565+0800 BasicDemo[75683:5313401] total: 2
2017-11-28 23:34:55.739570+0800 BasicDemo[75683:5313402] total: 1
2017-11-28 23:34:55.739577+0800 BasicDemo[75683:5313403] total: 0
NSInteger total = 0;
NSLock *lock = [NSLock new];
- (void)threadSafe {
    for (NSInteger index = 0; index < 3; index++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [lock lock];
            total += 1;
            NSLog(@"total: %ld", total);
            total -= 1;
            NSLog(@"total: %ld", total);
            [lock unlock];
        });
    }
}
//第一次輸出
2017-11-28 23:35:37.696614+0800 BasicDemo[75696:5314483] total: 1
2017-11-28 23:35:37.696928+0800 BasicDemo[75696:5314483] total: 0
2017-11-28 23:35:37.696971+0800 BasicDemo[75696:5314481] total: 1
2017-11-28 23:35:37.696995+0800 BasicDemo[75696:5314481] total: 0
2017-11-28 23:35:37.697026+0800 BasicDemo[75696:5314482] total: 1
2017-11-28 23:35:37.697050+0800 BasicDemo[75696:5314482] total: 0
//第二次輸出
2017-11-28 23:36:01.790264+0800 BasicDemo[75700:5315159] total: 1
2017-11-28 23:36:01.790617+0800 BasicDemo[75700:5315159] total: 0
2017-11-28 23:36:01.790668+0800 BasicDemo[75700:5315161] total: 1
2017-11-28 23:36:01.790687+0800 BasicDemo[75700:5315161] total: 0
2017-11-28 23:36:01.790711+0800 BasicDemo[75700:5315160] total: 1
2017-11-28 23:36:01.790735+0800 BasicDemo[75700:5315160] total: 0

從上面能夠看出,第一個函數第一次和第二次調用的結果不同,換句話說,不能肯定代碼的運行順序和結果,是線程不安全的;第二個函數第一次和第二次輸出結果同樣,能夠肯定函數的執行結果,是線程安全的。html

線程不安全是因爲多線程訪問形成的,那麼如何解決?算法

 

1.既然線程安全問題是由多線程引發的,那麼,最極端的可使用單線程保證線程安全。api

2.線程安全是因爲多線程訪問和修改共享資源而引發不可預測的結果,所以,若是都是訪問共享資源而不去修改共享資源也能夠保證線程安全,好比:設置只讀屬性的全局變量。數組

3.使用鎖。緩存

  鎖是最經常使用的同步工具。一段代碼段在同一個時間只能容許被一個線程訪問,好比一個線程A進入加鎖代碼以後因爲已經加鎖,另外一個線程B就沒法訪問,只有等待前一個線程A執行完加鎖代碼後解鎖,B線程才能訪問加鎖代碼。不要將過多的其餘操做代碼放到裏面,不然一個線程執行的時候另外一個線程就一直在等待,就沒法發揮多線程的做用了。安全

 
NSLock
在Cocoa程序中NSLock中實現了一個簡單的互斥鎖,實現了NSLocking protocol。
lock,加鎖
unlock,解鎖
tryLock,嘗試加鎖,若是失敗了,並不會阻塞線程,只是當即返回
NOlockBeforeDate:,在指定的date以前暫時阻塞線程(若是沒有獲取鎖的話),若是到期尚未獲取鎖,則線程被喚醒,函數當即返回NO
使用tryLock並不能成功加鎖,若是獲取鎖失敗就不會執行加鎖代碼了。
- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    [lock lock];
    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObject:imageName];
    }
    [lock unlock];
}

@synchronized代碼塊

每一個iOS開發最先接觸的線程鎖就是@synchronized,代碼簡單。數據結構

- (void)getIamgeName:(int)index{
    NSString *imageName;
    @synchronized(self) {
        if (imageNames.count>0) {
            imageName = [imageNames lastObject];
            [imageNames removeObject:imageName];
        }
    }
}

條件信號量dispatch_semaphore_t

dispatch_semaphore_tGCD中信號量,也能夠解決資源搶佔問題,支持信號通知和信號等待。每當發送一個信號通知,則信號量+1;每當發送一個等待信號時信號量-1,;若是信號量爲0則信號會處於等待狀態,直到信號量大於0開始執行。多線程

#import "DispatchSemaphoreViewController.h"

@interface DispatchSemaphoreViewController ()
{
    dispatch_semaphore_t semaphore;
}
@end

@implementation DispatchSemaphoreViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    semaphore = dispatch_semaphore_create(1);
    /**
     *  建立一個信號量爲1的信號
     *
     */
}

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    /**
     *  semaphore:等待信號
     DISPATCH_TIME_FOREVER:等待時間
     wait以後信號量-1,爲0
     */
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObject:imageName];
    }
    /**
     *  發送一個信號通知,這時候信號量+1,爲1
     */
    dispatch_semaphore_signal(semaphore);
}

@end

 

條件鎖NSCondition

NSCondition一樣實現了NSLocking協議,因此它和NSLock同樣,也有NSLocking協議的lock和unlock方法,能夠當作NSLock來使用解決線程同步問題,用法徹底同樣。併發

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    [lock lock];
    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObject:imageName];
    }
    [lock unlock];
}
  同時,NSCondition提供更高級的用法。wait和signal,和條件信號量相似。
  好比咱們要監聽imageNames數組的個數,當imageNames的個數大於0的時候就執行清空操做。思路是這樣的,當imageNames個數大於0時執行清空操做,不然,wait等待執行清空操做。當imageNames個數增長的時候發生signal信號,讓等待的線程喚醒繼續執行。
  NSCondition和NSLock、@synchronized等是不一樣的是,NSCondition能夠給每一個線程分別加鎖,加鎖後不影響其餘線程進入臨界區。這是很是強大。
  可是正是由於這種分別加鎖的方式,NSCondition使用wait並使用加鎖後並不能真正的解決資源的競爭。好比咱們有個需求:不能讓m<0。假設當前m=0,線程A要判斷到m>0爲假,執行等待;線程B執行了m=1操做,並喚醒線程A執行m-1操做的同時線程C判斷到m>0,由於他們在不一樣的線程鎖裏面,一樣判斷爲真也執行了m-1,這個時候線程A和線程C都會執行m-1,可是m=1,結果就會形成m=-1.
當我用數組作刪除試驗時,作增刪操做並非每次都會出現,大概3-4次後會出現。單純的使用lock、unlock是沒有問題的。
- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    [lock lock];    //加鎖
    static int m = 0;
    static int n = 0;
    static int p = 0;
    NSLog(@"removeObjectBegin count: %ld\n",imageNames.count);
    
    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObjectAtIndex:0];
        m++;
        NSLog(@"執行了%d次刪除操做",m);
    } else {
        p++;
        NSLog(@"執行了%d次等待",p);
        [lock wait];    //等待
        imageName = [imageNames lastObject];
        [imageNames removeObjectAtIndex:0];
        /**
         *  有時候點擊取出圖片會崩潰
         */
        n++;
        NSLog(@"執行了%d次繼續操做",n);
    }
    
    NSLog(@"removeObject count: %ld\n",imageNames.count);
    [lock unlock];     //解鎖
}
- (void)createImageName:(NSMutableArray *)imageNames{
    [lock lock];
    static int m = 0;
    [imageNames addObject:@"0"];
    m++;
    NSLog(@"添加了%d次",m);
    [lock signal];  //喚醒隨機一個線程取消等待繼續執行
    
//        [lock broadcast];   //喚醒全部線程取消等待繼續執行
    NSLog(@"createImageName count: %ld\n",imageNames.count);
    [lock unlock];
}

#pragma mark - 多線程取出圖片後刪除
- (void)getImageNameWithMultiThread{
    [lock broadcast];
    NSMutableArray *imageNames = [[NSMutableArray alloc]init];
    dispatch_group_t dispatchGroup = dispatch_group_create();
    __block double then, now;
    then = CFAbsoluteTimeGetCurrent();
    for (int i=0; i<10; i++) {
        dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
            [self getIamgeName:imageNames];
        });
        dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
            [self createImageName:imageNames];
        });
    }
    dispatch_group_notify(dispatchGroup, self.synchronizationQueue, ^(){
        now = CFAbsoluteTimeGetCurrent();
        printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
    });
    
}

條件鎖NSConditionLock

也有人說這是個互斥鎖
NSConditionLock一樣實現了NSLocking協議,試驗過程當中發現性能很低。async

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    [lock lock];
    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObject:imageName];
    }
    [lock unlock];
}

NSConditionLock也能夠像NSCondition同樣作多線程之間的任務等待調用,並且是線程安全的。

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    [lock lockWhenCondition:1];    //加鎖
    if (imageNames.count>0) {
        imageName = [imageNames lastObject];
        [imageNames removeObjectAtIndex:0];
    }
    [lock unlockWithCondition:0];     //解鎖
}
- (void)createImageName:(NSMutableArray *)imageNames{
    [lock lockWhenCondition:0];
    [imageNames addObject:@"0"];
    [lock unlockWithCondition:1];
}

#pragma mark - 多線程取出圖片後刪除
- (void)getImageNameWithMultiThread{
    NSMutableArray *imageNames = [[NSMutableArray alloc]init];
    dispatch_group_t dispatchGroup = dispatch_group_create();
    __block double then, now;
    then = CFAbsoluteTimeGetCurrent();
    for (int i=0; i<10000; i++) {
        dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
            [self getIamgeName:imageNames];
        });
        dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
            [self createImageName:imageNames];
        });
    }
    dispatch_group_notify(dispatchGroup, self.synchronizationQueue, ^(){
        now = CFAbsoluteTimeGetCurrent();
        printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
    });
}

遞歸鎖NSRecursiveLock

  有時候「加鎖代碼」中存在遞歸調用,遞歸開始前加鎖,遞歸調用開始後會重複執行此方法以致於反覆執行加鎖代碼最終形成死鎖,這個時候可使用遞歸鎖來解決。使用遞歸鎖能夠在一個線程中反覆獲取鎖而不形成死鎖,這個過程當中會記錄獲取鎖和釋放鎖的次數,只有最後二者平衡鎖才被最終釋放。

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    [lock lock];
    if (imageNames.count>0) {
        imageName = [imageNames firstObject];
        [imageNames removeObjectAtIndex:0];
        [self getIamgeName:imageNames];
    }
    [lock unlock];
}
- (void)getImageNameWithMultiThread{
    NSMutableArray *imageNames = [NSMutableArray new];
    int count = 1024*10;
    for (int i=0; i<count; i++) {
        [imageNames addObject:[NSString stringWithFormat:@"%d",i]];
    }
    dispatch_group_t dispatchGroup = dispatch_group_create();
    __block double then, now;
    then = CFAbsoluteTimeGetCurrent();
    dispatch_group_async(dispatchGroup, self.synchronizationQueue, ^(){
        [self getIamgeName:imageNames];
    });
    dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^(){
        now = CFAbsoluteTimeGetCurrent();
        printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
    });
    
}

NSDistributedLock

  NSDistributedLock是MAC開發中的跨進程的分佈式鎖,底層是用文件系統實現的互斥鎖。NSDistributedLock沒有實現NSLocking協議,因此沒有lock方法,取而代之的是非阻塞的tryLock方法。

NSDistributedLock *lock = [[NSDistributedLock alloc] initWithPath:@"/Users/mac/Desktop/lock.lock"];
    while (![lock tryLock])
    {
        sleep(1);
    }
    
    //do something
    [lock unlock];
當執行到do something時程序退出,程序再次啓動以後tryLock就不再能成功了,陷入死鎖狀態.其餘應用也不能訪問受保護的共享資源。在這種狀況下,你可使用breadLock方法來打破現存的鎖以便你能夠獲取它。可是一般應該避免打破鎖,除非你肯定擁有進程已經死亡並不可能再釋放該鎖。

互斥鎖POSIX

POSIX和dispatch_semaphore_t很像,可是徹底不一樣。POSIX是Unix/Linux平臺上提供的一套條件互斥鎖的API。
新建一個簡單的POSIX互斥鎖,引入頭文件 #import <pthread.h>聲明並初始化一個pthread_mutex_t的結構。使用pthread_mutex_lock和pthread_mutex_unlock函數。調用pthread_mutex_destroy來釋放該鎖的數據結構。
#import <pthread.h>
@interface MYPOSIXViewController ()
{
    pthread_mutex_t mutex;  //聲明pthread_mutex_t的結構
}
@end

@implementation MYPOSIXViewController
- (void)dealloc{
    pthread_mutex_destroy(&mutex);  //釋放該鎖的數據結構
}
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    pthread_mutex_init(&mutex, NULL);
    /**
     *  初始化
     *
     */
}

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    /**
     *  加鎖
     */
    pthread_mutex_lock(&mutex);
    if (imageNames.count>0) {
        imageName = [imageNames firstObject];
        [imageNames removeObjectAtIndex:0];
    }
    /**
     *  解鎖
     */
    pthread_mutex_unlock(&mutex);
}

POSIX還能夠建立條件鎖,提供了和NSCondition同樣的條件控制,初始化互斥鎖同時使用pthread_cond_init來初始化條件數據結構,

// 初始化
    int pthread_cond_init (pthread_cond_t *cond, pthread_condattr_t *attr);
    
    // 等待(會阻塞)
    int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mut);
    
    // 定時等待
    int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mut, const struct timespec *abstime);
    
    // 喚醒
    int pthread_cond_signal (pthread_cond_t *cond);
    
    // 廣播喚醒
    int pthread_cond_broadcast (pthread_cond_t *cond);
    
    // 銷燬
    int pthread_cond_destroy (pthread_cond_t *cond);
OSIX還提供了不少函數,有一套完整的API,包含Pthreads線程的建立控制等等,很是底層,能夠手動處理線程的各個狀態的轉換即管理生命週期,甚至能夠實現一套本身的多線程,感興趣的能夠繼續深刻了解。推薦一篇詳細文章,但不是基於iOS的,是基於Linux的,可是介紹的很是詳細 Linux 線程鎖詳解
 

自旋鎖OSSpinLock

首先要提的是OSSpinLock已經出現了BUG,致使並不能徹底保證是線程安全的。

新版 iOS 中,系統維護了 5 個不一樣的線程優先級/QoS: background,utility,default,user-initiated,user-interactive。高優先級線程始終會在低優先級線程前執行,一個線程不會受到比它更低優先級線程的干擾。這種線程調度算法會產生潛在的優先級反轉問題,從而破壞了 spin lock。
具體來講,若是一個低優先級的線程得到鎖並訪問共享資源,這時一個高優先級的線程也嘗試得到這個鎖,它會處於 spin lock 的忙等狀態從而佔用大量 CPU。此時低優先級線程沒法與高優先級線程爭奪 CPU 時間,從而致使任務遲遲完不成、沒法釋放 lock。這並不僅是理論上的問題,libobjc 已經遇到了不少次這個問題了,因而蘋果的工程師停用了 OSSpinLock。
蘋果工程師 Greg Parker 提到,對於這個問題,一種解決方案是用 truly unbounded backoff 算法,這能避免 livelock 問題,但若是系統負載高時,它仍有可能將高優先級的線程阻塞數十秒之久;另外一種方案是使用 handoff lock 算法,這也是 libobjc 目前正在使用的。鎖的持有者會把線程 ID 保存到鎖內部,鎖的等待者會臨時貢獻出它的優先級來避免優先級反轉的問題。理論上這種模式會在比較複雜的多鎖條件下產生問題,但實踐上目前還一切都好。
OSSpinLock 自旋鎖,性能最高的鎖。原理很簡單,就是一直 do while 忙等。它的缺點是當等待時會消耗大量 CPU 資源,因此它不適用於較長時間的任務。對於內存緩存的存取來講,它很是合適。
因此說不建議再繼續使用,不過能夠拿來玩耍一下,導入頭文件 #import <libkern/OSAtomic.h>
#import <libkern/OSAtomic.h>
@interface MYOSSpinLockViewController ()
{
    OSSpinLock spinlock;  //聲明pthread_mutex_t的結構
}
@end

@implementation MYOSSpinLockViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    spinlock = OS_SPINLOCK_INIT;
    /**
     *  初始化
     *
     */
}

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    /**
     *  加鎖
     */
    OSSpinLockLock(&spinlock);
    if (imageNames.count>0) {
        imageName = [imageNames firstObject];
        [imageNames removeObjectAtIndex:0];
    }
    /**
     *  解鎖
     */
    OSSpinLockUnlock(&spinlock);
}
@end

 

GCD線程阻斷dispatch_barrier_async/dispatch_barrier_sync

dispatch_barrier_async/dispatch_barrier_sync在必定的基礎上也能夠作線程同步,會在線程隊列中打斷其餘線程執行當前任務,也就是說只有用在併發的線程隊列中才會有效,由於串行隊列原本就是一個一個的執行的,你打斷執行一個和插入一個是同樣的效果。兩個的區別是是否等待任務執行完成。

注意:若是在當前線程調用dispatch_barrier_sync打斷會發生死鎖。

@interface MYdispatch_barrier_syncViewController ()
{
        __block double then, now;
}
@end

@implementation MYdispatch_barrier_syncViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
}
- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    if (imageNames.count>0) {
        imageName = [imageNames firstObject];
        [imageNames removeObjectAtIndex:0];
    }else{
        now = CFAbsoluteTimeGetCurrent();
        printf("thread_lock: %f sec\nimageNames count: %ld\n", now-then,imageNames.count);
    }
}

- (void)getImageNameWithMultiThread{
    NSMutableArray *imageNames = [NSMutableArray new];
    int count = 1024*11;
    for (int i=0; i<count; i++) {
        [imageNames addObject:[NSString stringWithFormat:@"%d",i]];
    }
    then = CFAbsoluteTimeGetCurrent();
    for (int i=0; i<count+1; i++) {
        //100來測試鎖有沒有正確的執行
        dispatch_barrier_async(self.synchronizationQueue, ^{
             [self getIamgeName:imageNames];
        });
    }
}

總結

@synchronized:適用線程很少,任務量不大的多線程加鎖 NSLock:其實NSLock並無想象中的那麼差,不知道你們爲何不推薦使用 dispatch_semaphore_t:使用信號來作加鎖,性能提高顯著 NSCondition:使用其作多線程之間的通訊調用不是線程安全的 NSConditionLock:單純加鎖性能很是低,比NSLock低不少,可是能夠用來作多線程處理不一樣任務的通訊調用 NSRecursiveLock:遞歸鎖的性能出奇的高,可是隻能做爲遞歸使用,因此限制了使用場景 NSDistributedLock:由於是MAC開發的,就不討論了 POSIX(pthread_mutex):底層的api,複雜的多線程處理建議使用,而且能夠封裝本身的多線程 OSSpinLock:性能也很是高,惋惜出現了線程問題 dispatch_barrier_async/dispatch_barrier_sync:測試中發現dispatch_barrier_sync比dispatch_barrier_async性能要高,真是大出意外

相關文章
相關標籤/搜索