8. iOS中的多線程——鎖

題記:雖然有些事情的發(fā)生可能是你預料之中的,但是當它真正的發(fā)生了的時候,還是很難以接受的,還是需要一點時間,去緩和這種消極的情緒,盡快站起來吧!加油!花了一天半的時間,各種查閱資料,總結(jié)了iOS中關(guān)于線程鎖的知識,希望我能從中學到一些,也希望可以幫到同樣有需要的你!(文中如有錯誤,還請?zhí)岢觯黄鸾涣鳎?/em>


本文主要介紹:

  • 互斥鎖
  • 遞歸鎖
  • 讀寫鎖
  • 自旋鎖
  • 分布鎖
  • 條件變量
  • 信號量
  • 柵欄
  • 一些常用鎖的性能。
1. 互斥鎖(Mutex)

常用,當一個線程試圖獲取被另一個線程占用的鎖時,它就會被掛起,讓出CPU,直到該鎖被釋放。

  • 互斥鎖的實現(xiàn)方式:
    • @synchronized:實現(xiàn)單例模式
    • NSLock:不能迭代加鎖,如果發(fā)生兩次lock,而未unlock過,則會產(chǎn)生死鎖問題。

1.@synchronized 同步鎖

  • 例程:
/**
 *設置屬性值
 */
-(void)setMyTestString:(NSString *)myTestString{
    @synchronized(self) {
        // todo something
        _myTestString = myTestString;
    }
}
  • 常用于單例模式的設計:

    例程:

+(instancetype)shareInstance{
    // 1.定義一個靜態(tài)實例,初值nil
    static TestSynchronized *myClass = nil;
    // 2.添加同步鎖,創(chuàng)建實例
    @synchronized(self) {
        // 3.判斷實例是否創(chuàng)建過,創(chuàng)建過則退出同步鎖,直接返回該實例
        if (!myClass) {
            // 4.未創(chuàng)建過,則新建一個實例并返回
            myClass = [[self alloc] init];
        }
    }
    return myClass;
}
此時為了保證單例模式的更加嚴謹,需要重寫`allocWithZone`方法,保證其他開發(fā)者使用`alloc`和`init`方法時,不再創(chuàng)建新的對象。必要的時候還需要重寫`copyWithZone`方法防止`copy`屬性對單例模式的影響。

iOS中還有一種更加輕便的方法實現(xiàn)單例模式,即使用GCD中的dispatch_once函數(shù)實現(xiàn)。

例程:

+(instancetype)shareInstance{
    static TestSynchronized *myClass = nil;
    static dispatch_once_t once_token;
    dispatch_once(&once_token, ^{
        myClass = [[self alloc] init];
    });
    return myClass;
}

2.NSLock

  • 例程:
static NSLock *mylock;
-(void)viewDidLoad {
    [super viewDidLoad];
    mylock = [[NSLock alloc] init];
}
-(void)myLockTest1{
    if ([mylock tryLock]) {
        // to do something
        [mylock unlock];
    }
}
-(void)myLockTest2{
    [mylock lock];
    // to do something
    [mylock unlock];
}
2. 遞歸鎖(Recursive Lock)
  • 遞歸鎖可以被同一線程多次請求,而不會引起死鎖,即在多次被同一個線程進行加鎖時,不會造成死鎖。這主要是用在循環(huán)或遞歸操作中。

  • 可以允許同一線程多次加鎖,而不會造成死鎖。

  • 遞歸鎖會跟蹤它被lock的次數(shù)。每次成功的lock都必須平衡調(diào)用unlock操作。只有所有達到這種平衡,鎖最后才能被釋放,以供其它線程使用。

  • 例程:

NSRecursiveLock *myRecursiveLock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        static void (^MyRecursiveLockBlk)(int value);
        MyRecursiveLockBlk = ^(int value){
            [myRecursiveLock lock];
            if (value > 0) {
                // to do something
                NSLog(@"MyRecursiveLockBlk value = %d", value);
                MyRecursiveLockBlk(value - 1);
            }
            [myRecursiveLock unlock];
        };
        MyRecursiveLockBlk(6);
    });

此時如果將例程中的遞歸鎖換成互斥鎖:
NSRecursiveLock *myRecursiveLock = [[NSRecursiveLock alloc] init];換成
NSLock *myLock = [[NSLock alloc] init];,則會發(fā)生死鎖問題。

3. 讀寫鎖(Read-write Lock)
  • 讀寫鎖將訪問者分為讀出寫入兩種,當讀寫鎖在讀加鎖模式下,所有以讀加鎖方式訪問該資源時,都會獲得訪問權(quán)限,而所有試圖以寫加鎖方式對其加鎖的線程都將阻塞,直到所有的讀鎖釋放。

  • 當在寫加鎖模式下,所有試圖對其加鎖的線程都將阻塞。

  • 例程:

#import "ViewController.h"
#import <pthread.h>
@interface ViewController ()
@property(nonatomic, copy) NSString *rwStr;
@end
@implementation ViewController
pthread_rwlock_t rwlock;
-(void)viewDidLoad {
    [super viewDidLoad];
    // 初始化讀寫鎖
    pthread_rwlock_init(&rwlock,NULL);
    __block int i;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        i = 5;
        while (i>=0) {
            NSString *temp = [NSString stringWithFormat:@"writing == %d", i];
            [self writingLock:temp];
            i--;
        }  
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        i = 5;
        while (i>=0) {
            [self readingLock];
            i--;
        }
    });
}
// 寫加鎖
-(void)writingLock:(NSString *)temp{
    pthread_rwlock_wrlock(&rwlock);
    // writing
    self.rwStr = temp;
    NSLog(@"%@", temp);
    pthread_rwlock_unlock(&rwlock);
}
// 讀加鎖
-(NSString *)readingLock{
    pthread_rwlock_rdlock(&rwlock);
    // reading
    NSString *str = self.rwStr;
    NSLog(@"reading == %@",self.rwStr);
    pthread_rwlock_unlock(&rwlock);
    return str;
}
@end
4. 自旋鎖(Spin Lock)
  • 自旋鎖與互斥鎖類似

  • 但不同的是:自旋鎖是非阻塞的,當一個線程無法獲取自旋鎖時,會自旋,直到該鎖被釋放,等待的過程中線程并不會掛起。(實質(zhì)上就是,如果自旋鎖已經(jīng)被別的執(zhí)行單元保持,調(diào)用者就一直循環(huán)在等待該自旋鎖的保持著已經(jīng)釋放了鎖)。

  • 自旋鎖的使用者一般保持鎖的時間很短,此時其效率遠高于互斥鎖。

  • 自旋鎖保持期間是搶占失效的

    優(yōu)點:效率高,不用進行線程的切換

    缺點:如果一個線程霸占鎖的時間過長,自旋會消耗CPU資源

  • 例程:

// 頭文件
#import <libkern/OSAtomic.h>
// 初始化自旋鎖
static OSSpinLock myLock = OS_SPINLOCK_INIT;
// 自旋鎖的使用
-(void)SpinLockTest{
    OSSpinLockLock(&myLock);
    // to do something
    OSSpinLockUnlock(&myLock);
}
5. 分布鎖(Didtributed Lock)
  • 跨進程的分布式鎖,是進程間同步的工具,底層是用文件系統(tǒng)實現(xiàn)的互斥鎖,并不強制進程休眠,而是起到告知的作用。
  • NSDistributedLock沒有實現(xiàn)NSLocking協(xié)議,所以沒有會阻塞線程的lock方法,取而代之的是非阻塞的tryLock方法來獲取鎖,用unlock方法釋放鎖。
  • 如果一個獲取鎖的進程在釋放鎖之前就退出了,那么鎖就一直不能釋放,此時可以通過breakLock強行獲取鎖。
6. 條件變量(Condition Variable)
  • 使用情況:如果一個線程需要等待某一條件出現(xiàn)才能繼續(xù)執(zhí)行,而這個條件是由別的線程產(chǎn)生的,這個時候就用到條件變量。常見的情況是:生產(chǎn)者-消費者問題。

  • 條件變量可以讓一個線程等待某一條件,當條件滿足時,會收到通知。在獲取條件變量并等待條件發(fā)生的過程中,也會產(chǎn)生多線程的競爭,所以條件變量通常和互斥鎖一起工作。

    • NSCondition:是互斥鎖和條件鎖的結(jié)合,即一個線程在等待signal而阻塞時,可以被另一個線程喚醒,由于操作系統(tǒng)實現(xiàn)的差異,即使沒有發(fā)送signal消息,線程也有可能被喚醒,所以需要增加謂詞變量來保證程序的正確性。
    • NSConditionLock:與NSCondition的實現(xiàn)機制不一樣,當定義的條件成立的時候會獲取鎖,反之,釋放鎖。

    NSCondition的例程:

    // 創(chuàng)建鎖
     NSCondition *condition = [[NSCondition alloc] init];
    static int count = 0;
    // 生產(chǎn)者
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while(count<20)
        {
            [condition lock];
            // 生產(chǎn)
            count ++;
            NSLog(@"生產(chǎn) = %d",count);
            [condition signal];
            [condition unlock];
        }
    });
    // 消費者
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while (count>0)
        {
            [condition lock];
            // 消耗
            count --;
            NSLog(@"消耗剩余 = %d",count);
            [condition unlock];
        }
    });

NSConditionLock的例程:

    // 創(chuàng)建鎖
    NSConditionLock *condLock = [[NSConditionLock alloc] initWithCondition:ConditionHASNOT];
    static int count = 0;
    // 生產(chǎn)者
    while(true)
    {
        [condLock lock];
        // 生產(chǎn)
        count ++;
        [condLock unlockWithCondition:ConditionHAS];
    }
    // 消費者
    while (true)
    {
        [condLock lockWhenCondition:ConditionHAS];
        // 消耗
        count --;
        [condLock unlockWithCondition:(count<=0 ? ConditionHASNOT : ConditionHAS)];
    }
7. 信號量(Semaphore)
  • 信號量:可以是一種特殊的互斥鎖,可以是資源的計數(shù)器
  • 可以使用GCD中的Dispatch Semaphore實現(xiàn),Dispatch Semaphore是持有計數(shù)的信號,該計數(shù)是多線程編程中的計數(shù)類型信號。計數(shù)為0時等待,計數(shù)大于等于1時,減1為不等待。
8. 柵欄/屏障(Barrier)
  • 柵欄必須單獨執(zhí)行,不能與其他任務并發(fā)執(zhí)行,柵欄只對并發(fā)隊列有意義。
  • 柵欄只有等待當前隊列所有并發(fā)任務都執(zhí)行完畢后,才會單獨執(zhí)行,帶起執(zhí)行完畢,再按照正常的方式繼續(xù)向下執(zhí)行。

iOS中線程鎖的性能對比:

點擊這里,參考網(wǎng)址

  • No1.自旋鎖OSSpinLock耗時最少
  • No2.pthread_mutex
  • No3.NSLock/NSCondition/NSRecursiveLock 耗時接近
  • No4.@synchronized
  • No5.NSConditionLock
  • 柵欄的性能并沒有很好,在實際開發(fā)中也很少用到(筆者在最近一次面試中就遇到,問柵欄的性能怎么樣?當時并不知道柵欄在實際應用中的性能并不是很理想,又被問到蘋果官方常使用的鎖是什么?應該是自旋鎖,--然而筆者當時還是不知道。。。)

> 劃重點:自旋鎖是線程不安全的在 ibireme 的 不再安全的 OSSpinLock有解釋,進一步的ibireme在文中也有提到蘋果在新系統(tǒng)中已經(jīng)優(yōu)化了 pthread_mutex 的性能,所以它看上去和 OSSpinLock 差距并沒有那么大,所以筆者覺得不妨多了解了解pthread_mutex
  • pthread_mutex
    __block pthread_mutex_t theLock;
    pthread_mutex_init(&theLock, NULL);
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            pthread_mutex_lock(&theLock);
            NSLog(@"需要線程同步的操作1 開始");
            sleep(3);
            NSLog(@"需要線程同步的操作1 結(jié)束");
            pthread_mutex_unlock(&theLock);
        
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            sleep(1);
            pthread_mutex_lock(&theLock);
            NSLog(@"需要線程同步的操作2");
            pthread_mutex_unlock(&theLock);
        
    });

c語言定義下多線程加鎖方式。

  1. pthread_mutex_init(pthread_mutex_t * mutex,const pthread_mutexattr_t attr);
    初始化鎖變量mutex。attr為鎖屬性,NULL值為默認屬性。
  2. pthread_mutex_lock(pthread_mutex_t* mutex);加鎖
  3. pthread_mutex_tylock(pthread_mutex_t* mutex);加鎖,但是與2不一樣的是當鎖已經(jīng)在使用的時候,返回為EBUSY,而不是掛起等待。
  4. pthread_mutex_unlock(pthread_mutex_t* mutex);釋放鎖
  5. pthread_mutex_destroy(pthread_mutex_t* *mutex);使用完后釋放

代碼執(zhí)行操作結(jié)果如下:

2016-06-30 21:13:32.440 SafeMultiThread[31429:548869] 需要線程同步的操作1 開始
2016-06-30 21:13:35.445 SafeMultiThread[31429:548869] 需要線程同步的操作1 結(jié)束
2016-06-30 21:13:35.446 SafeMultiThread[31429:548866] 需要線程同步的操作2

  • pthread_mutex(recursive)
    __block pthread_mutex_t theLock;
    //pthread_mutex_init(&theLock, NULL);
    
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&lock, &attr);
    pthread_mutexattr_destroy(&attr);
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        static void (^RecursiveMethod)(int);
        
        RecursiveMethod = ^(int value) {
            
            pthread_mutex_lock(&theLock);
            if (value > 0) {
                
                NSLog(@"value = %d", value);
                sleep(1);
                RecursiveMethod(value - 1);
            }
            pthread_mutex_unlock(&theLock);
        };
        
        RecursiveMethod(5);
    });

這是pthread_mutex為了防止在遞歸的情況下出現(xiàn)死鎖而出現(xiàn)的遞歸鎖。作用和NSRecursiveLock遞歸鎖類似。
如果使用pthread_mutex_init(&theLock, NULL);初始化鎖的話,上面的代碼會出現(xiàn)死鎖現(xiàn)象,但是改成使用遞歸鎖的形式,則沒有問題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容