iOS開發(fā)中的鎖

iOS開發(fā)中的鎖

本人對鎖沒有深入理解,只是看了幾篇文章混槐,在這里做一下簡單的總結(jié)戈锻。

iOS開發(fā)中歼跟,鎖是用來解決線程安全的問題的工具。那么線程安全是什么格遭?

線程安全


線程安全:多線程操作共享數(shù)據(jù)的時候哈街,如果出現(xiàn)了意想不到的結(jié)果,就是線程不安全拒迅。反之就是線程安全骚秦;

或者這么說是不是更容易聽懂,多個線程同時對一個數(shù)據(jù)進行修改的時候璧微,如果不能保證多個線程的執(zhí)行順序作箍,就會出現(xiàn)意想不到的結(jié)果,這個時候就線程不安全了前硫。

貌似怎么說都不行了蒙揣,那么就舉個例子吧;

- (void)threadNotSafe {
   __block NSInteger total = 0;
    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);
        });
    }
}
/*
第一次打印結(jié)果
2019-05-06 11:04:33.937462+0800 DCLockStudy[5270:410073] total: 1
2019-05-06 11:04:33.937462+0800 DCLockStudy[5270:410074] total: 2
2019-05-06 11:04:33.937466+0800 DCLockStudy[5270:410075] total: 3
2019-05-06 11:04:33.937617+0800 DCLockStudy[5270:410075] total: 2
2019-05-06 11:04:33.937617+0800 DCLockStudy[5270:410073] total: 1
2019-05-06 11:04:33.937617+0800 DCLockStudy[5270:410074] total: 2
第二次打印結(jié)果
2019-05-06 11:06:50.198993+0800 DCLockStudy[5320:416449] total: 1
2019-05-06 11:06:50.198994+0800 DCLockStudy[5320:416450] total: 2
2019-05-06 11:06:50.199020+0800 DCLockStudy[5320:416452] total: 3
2019-05-06 11:06:50.199187+0800 DCLockStudy[5320:416450] total: 2
2019-05-06 11:06:50.199187+0800 DCLockStudy[5320:416449] total: 1
2019-05-06 11:06:50.199253+0800 DCLockStudy[5320:416452] total: 0
*/

上面這段代碼开瞭,分別執(zhí)行兩次懒震,打印結(jié)果不一樣。也就是不能確定代碼執(zhí)行順序和執(zhí)行結(jié)果嗤详,是線程不安全的个扰;

再看下面這段代碼

- (void)threadSafe {
    __block NSInteger total = 0;
    NSLock *myLock = [[NSLock alloc]init];
    for (NSInteger index = 0; index < 3; index++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [myLock lock];
            total += 1;
            NSLog(@"total: %ld", total);
            total -= 1;
            NSLog(@"total: %ld", total);
            [myLock unlock];
        });
    }
}
/*
第一次打印結(jié)果
2019-05-06 11:10:03.678707+0800 DCLockStudy[5351:422830] total: 1
2019-05-06 11:10:03.678872+0800 DCLockStudy[5351:422830] total: 0
2019-05-06 11:10:03.678978+0800 DCLockStudy[5351:422829] total: 1
2019-05-06 11:10:03.679057+0800 DCLockStudy[5351:422829] total: 0
2019-05-06 11:10:03.679189+0800 DCLockStudy[5351:422828] total: 1
2019-05-06 11:10:03.679286+0800 DCLockStudy[5351:422828] total: 0
第二次打印結(jié)果
2019-05-06 11:14:52.524955+0800 DCLockStudy[5406:431979] total: 1
2019-05-06 11:14:52.525092+0800 DCLockStudy[5406:431979] total: 0
2019-05-06 11:14:52.525224+0800 DCLockStudy[5406:431980] total: 1
2019-05-06 11:14:52.525303+0800 DCLockStudy[5406:431980] total: 0
2019-05-06 11:14:52.525413+0800 DCLockStudy[5406:431978] total: 1
2019-05-06 11:14:52.525511+0800 DCLockStudy[5406:431978] total: 0
*/

兩次打印結(jié)果一樣,為什么呢葱色?因為加了鎖递宅,哈哈哈;那么接下來我們來簡單說一下鎖;

鎖的幾個的定義


  • 臨界區(qū):每個進程中訪問臨界資源的那段程序稱為臨界區(qū)办龄,每次只允許一個進程進入臨界區(qū)烘绽,進入后不允許其他進程進入。

  • 互斥鎖:用于保護臨界區(qū)俐填,確保同一時間只有一個線程訪問數(shù)據(jù)安接。對共享資源的訪問,先對互斥量進行加鎖英融,如果互斥量已經(jīng)上鎖盏檐,調(diào)用線程會阻塞,直到互斥量被解鎖驶悟。在完成了對共享資源的訪問后胡野,要對互斥量進行解鎖。

接下來主要講幾種鎖:自旋鎖OSSpinLock痕鳍、信號量硫豆、pthread_mutex、NSLock笼呆、NSCondition熊响、NSRecursiveLock、NSConditionLock抄邀、@synchronized耘眨。這里參考了深入理解iOS開發(fā)中的鎖昼榛;

然后這些鎖我在學(xué)習(xí)過程中寫了一個簡單的demo境肾,里面有他們的使用方法;

自旋鎖OSSpinLock


自旋鎖與互斥鎖類似胆屿,它不是通過休眠使進程阻塞奥喻,而是在獲取鎖之前一直處于忙等(自旋)阻塞狀態(tài)。一般用于鎖持有的時間短非迹,而且線程并不希望在重新調(diào)度上花太多的成本环鲤。

自旋鎖與互斥鎖的區(qū)別:線程在申請自旋鎖的時候,線程不會被掛起憎兽,而是處于忙等的狀態(tài)冷离。

我所知道的自旋鎖只有OSSpinLock,不過YY大神已經(jīng)說過OSSpinLock不再安全了纯命,因此這里不做過多的介紹西剥,如果有興趣可以去看不再安全的 OSSpinLock;

信號量


dispatch_semaphore的實現(xiàn)原理和自旋鎖不一樣,是根據(jù)信號量判斷的亿汞,首先會將信號量-1瞭空,并判斷是否大于等于0,如果是,則返回0咆畏,并繼續(xù)執(zhí)行后續(xù)代碼南捂,否則,使線程進入睡眠狀態(tài)旧找,讓出cpu時間溺健。直到信號量大于0或者超時,則線程會被重新喚醒執(zhí)行后續(xù)操作钦讳。

使用方法如下

- (void)__dispatch_semaphore{
    /**
        dispatch_semaphore_create(1): 傳入值必須 >=0, 若傳入為 0 則阻塞線程并等待timeout,時間到后會執(zhí)行其后的語句
        dispatch_semaphore_wait(signal, overTime):可以理解為 lock,會使得 signal 值 -1
        dispatch_semaphore_signal(signal):可以理解為 unlock,會使得 signal 值 +1
    
        停車場剩余4個車位矿瘦,那么即使同時來了四輛車也能停的下。如果此時來了五輛車愿卒,那么就有一輛需要等待缚去。
        信號量的值(signal)就相當(dāng)于剩余車位的數(shù)目,dispatch_semaphore_wait 函數(shù)就相當(dāng)于來了一輛車琼开,dispatch_semaphore_signal 就相當(dāng)于走了一輛車易结。停車位的剩余數(shù)目在初始化的時候就已經(jīng)指明了(dispatch_semaphore_create(long value)),調(diào)用一次 dispatch_semaphore_signal柜候,剩余的車位就增加一個搞动;調(diào)用一次dispatch_semaphore_wait 剩余車位就減少一個;當(dāng)剩余車位為 0 時渣刷,再來車(即調(diào)用 dispatch_semaphore_wait)就只能等待鹦肿。有可能同時有幾輛車等待一個停車位。有些車主沒有耐心辅柴,給自己設(shè)定了一段等待時間箩溃,這段時間內(nèi)等不到停車位就走了,如果等到了就開進去停車碌嘀。而有些車主就像把車停在這涣旨,所以就一直等下去。
    */
    dispatch_semaphore_t signal = dispatch_semaphore_create(1);//傳入值必須>=0;如果傳入0股冗,
    dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW,2.0f * NSEC_PER_SEC);
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"線程1等待中霹陡。。止状。");
        dispatch_semaphore_wait(signal, overTime);
        NSLog(@"線程1");
        sleep(1);
        dispatch_semaphore_signal(signal);
        NSLog(@"線程1發(fā)送信號");
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"線程2等待中烹棉。。怯疤。");
        dispatch_semaphore_wait(signal, overTime);
        NSLog(@"線程2");
        sleep(1);
        dispatch_semaphore_signal(signal);
        NSLog(@"線程2發(fā)送信號");
    });
}

pthread_mutex


pthread_mutex 表示互斥鎖浆洗。互斥鎖的實現(xiàn)原理與信號量非常相似旅薄,不是使用忙等辅髓,而是阻塞線程并睡眠泣崩,需要進行上下文切換。

使用方法如下:

- (void)__pthread_mutex_t{
    /**
     聲明 pthread_mutex_t pMutex;
        創(chuàng)建一個互斥鎖pthread_mutex_init(&pMutex,PTHREAD_MUTEX_NORMAL);
         PTHREAD_MUTEX_NORMAL 缺省類型洛口,也就是普通鎖矫付。當(dāng)一個線程加鎖以后,其余請求鎖的線程將形成一個等待隊列第焰,并在解鎖后先進先出原則獲得鎖买优。
         PTHREAD_MUTEX_ERRORCHECK 檢錯鎖,如果同一個線程請求同一個鎖挺举,則返回 EDEADLK杀赢,否則與普通鎖類型動作相同。這樣就保證當(dāng)不允許多次加鎖時不會出現(xiàn)嵌套情況下的死鎖湘纵。
         PTHREAD_MUTEX_RECURSIVE 遞歸鎖脂崔,允許同一個線程對同一個鎖成功獲得多次,并通過多次 unlock 解鎖梧喷。
         PTHREAD_MUTEX_DEFAULT 適應(yīng)鎖砌左,動作最簡單的鎖類型,僅等待解鎖后重新競爭铺敌,沒有等待隊列汇歹。
        加鎖 pthread_mutex_lock(&pMutex);
        解鎖 pthread_mutex_unlock(&pMutex);
 */
    //線程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"線程1加鎖");
        pthread_mutex_lock(&pMutex);
        sleep(1);
        NSLog(@"線程1");
        pthread_mutex_unlock(&pMutex);
        NSLog(@"線程1解鎖");
    });
    //線程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1); //保證線程1先加鎖
        NSLog(@"線程2加鎖");
        pthread_mutex_lock(&pMutex);
        NSLog(@"線程2");
        pthread_mutex_unlock(&pMutex);
        NSLog(@"線程2解鎖");
    });
}

pthread_mutex 還支持遞歸鎖,只要將類型設(shè)置為PTHREAD_MUTEX_RECURSIVE就可以偿凭。

NSLock


NSLock是OC以對象的形式暴露給開發(fā)者的一種鎖产弹,其實NSLock只是在內(nèi)部封裝了一個pthread_mutex,屬性為PTHREAD_MUTEX_ERRORCHECK,它會損失一定性能換來錯誤提示。

使用方法如下:

NSLock *lock = [NSLock new];
[lock lock];
//需要執(zhí)行的代碼
[lock unlock];

NSLock遵守了NSLocking協(xié)議弯囊,NSLocking協(xié)議其實很簡單痰哨,只需要滿足兩個方法

@protocol NSLocking
- (void)lock;
- (void)unlock;
@end

在這個基礎(chǔ)上,NSLock還自己提供兩個方法

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
  • tryLock 嘗試獲取鎖常挚,如果這個時候作谭,別的線程添加了鎖稽物,則返回NO奄毡,不會阻塞線程;

  • lockBeforeDate嘗試在某個時間之前獲取鎖,如果在這個時間內(nèi)沒有獲取到鎖則返回NO贝或,不會阻塞線程;

NSCondition


NSCondition其實是通過封裝了一個互斥鎖和條件變量吼过,把互斥鎖的lock方法和條件變量的wait/signal統(tǒng)一在NSCondition對象中,暴露給使用者咪奖。

NSCondition的加鎖過程和NSLock幾乎一致盗忱,耗時上應(yīng)該差不多。

使用方法如下:

- (void)__NSCondition{
    /** 條件變量
     wait:進入等待狀態(tài)
     waitUntilDate::讓一個線程等待一定的時間
     signal:喚醒一個等待的線程
     broadcast:喚醒所有等待的線程
     */
    //線程1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self.condition lock];
        NSLog(@"線程1獲取到鎖羊赵,并進入等待狀態(tài)");
        [self.condition wait];
        NSLog(@"線程1等待完成");
        [self.condition unlock];
        NSLog(@"線程1解鎖");
    });
    
    //線程2
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self.condition lock];
        NSLog(@"線程2獲取到鎖趟佃,并進入等待狀態(tài)");
        [self.condition wait];
        NSLog(@"線程2等待完成");
        [self.condition unlock];
        NSLog(@"線程2解鎖");
    });
    
    //線程3
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        NSLog(@"線程3扇谣,喚醒一個等待線程");
        [self.condition signal];
        
        sleep(2);
        NSLog(@"線程3,喚醒所有等待線程");
        [self.condition broadcast];
    });
}

NSConditionLock


NSConditionLock 借助 NSCondition 來實現(xiàn)闲昭,它的本質(zhì)就是一個生產(chǎn)者-消費者模型罐寨。“條件被滿足”可以理解為生產(chǎn)者提供了新的內(nèi)容序矩。NSConditionLock 的內(nèi)部持有一個 NSCondition 對象鸯绿,以及 _condition_value 屬性,在初始化時就會對這個屬性進行賦值簸淀。

使用方法如下:

- (void)__NSConditionLock{
    /**條件鎖  NSConditionLock
        因為遵守了NSLocking協(xié)議瓶蝴,所以可以無條件加鎖lock,
     */
    dispatch_queue_t conditionLockQueue = dispatch_queue_create("conditionLockQueue", DISPATCH_QUEUE_CONCURRENT);
    //線程1
    dispatch_async(conditionLockQueue, ^{
        NSLog(@"進入線程1租幕,添加條件鎖 = 2舷手,如果condition != 2,則線程阻塞");
        [self.conditionLock lockWhenCondition:2];
        NSLog(@"線程1加鎖成功,lockWhenCondition:2");
        [self.conditionLock unlock];
        NSLog(@"線程1解鎖鎖成功劲绪,unlockWithCondition:1");
    });
    
    //線程2
    dispatch_async(conditionLockQueue, ^{
        sleep(1); //保證線程3先執(zhí)行
        NSLog(@"進入線程2聚霜,嘗試添加條件鎖 = 1");
        if([self.conditionLock tryLockWhenCondition:1]){
            NSLog(@"線程2加鎖成功,lockWhenCondition:1");
            [self.conditionLock unlockWithCondition:2];
            NSLog(@"線程2解鎖成功珠叔,unlockWithCondition:1");
        }else{
            NSLog(@"線程2加鎖失敗");
        }
    });
    
    //線程3
    dispatch_async(conditionLockQueue, ^{
//        sleep(1); //保證線程2先執(zhí)行
        NSLog(@"進入線程3蝎宇,嘗試添加條件鎖 = 0");
        if([self.conditionLock tryLockWhenCondition:0]){
            NSLog(@"線程3加鎖成功,lockWhenCondition:0");
            [self.conditionLock unlockWithCondition:1];
            NSLog(@"線程3解鎖成功祷安,unlockWithCondition:1");
        }else{
            NSLog(@"線程3加鎖失敗");
        }
    });
    
    /** 先進入線程1姥芥,條件不滿足,線程1阻塞汇鞭,
        進入線程3凉唐,線程3滿足條件,線程3加鎖霍骄,線程3解鎖并將條件設(shè)置為1台囱;
        進入線程2,線程滿足條件读整,線程2加鎖簿训,線程2解鎖,并將條件設(shè)置為2米间;
        因為條件為2强品,線程1滿足條件了,線程1不在阻塞屈糊,線程1加鎖的榛,線程1解鎖;
     */
}

NSRecursiveLock


遞歸鎖是通過pthread_mutex_lock函數(shù)來實現(xiàn)的逻锐。

NSRecursiveLockNSLock 的區(qū)別在于內(nèi)部封裝的 pthread_mutex_t 對象的類型不同夫晌,前者的類型為 PTHREAD_MUTEX_RECURSIVE雕薪。

使用方法如下:

- (void)__NSRecursiveLock{
    /** NSRecursiveLock遞歸鎖
        它可以被同一線程多次請求,但不會引起死鎖晓淀。這主要是用在循環(huán)或者遞歸操作場景中蹦哼。
     */
    /** 如果用普通鎖,當(dāng)?shù)诙芜M入遞歸方法時要糊,嘗試加鎖纲熏,但是這個時候該線程還處于鎖定,所以線程阻塞锄俄,相互等待造成死鎖局劲。
     所以這個時候可以用一個遞歸鎖,允許同一線程多次請求奶赠,并且不會死鎖鱼填;
     NSRecursiveLock 遞歸鎖和NSLock一樣也有tryLock和lockBeforeDate,用法一樣毅戈;
     */
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        static void (^ recursiveMethod)(NSInteger);
        recursiveMethod = ^(NSInteger num){
            //    [self.myLock lock];
            [self.recursiveLock lock];
            if(num > 0){
                num --;
                NSLog(@"start num = %ld, mutableArray[0] = %@",num,self.mutableArray[0]);
                sleep(1);
                [self.mutableArray replaceObjectAtIndex:0 withObject:[NSString stringWithFormat:@"replace = %ld",num]];
                NSLog(@"end num = %ld, mutableArray[0] = %@",num,self.mutableArray[0]);
                recursiveMethod(num);
            }
            //    [self.myLock unlock];
            [self.recursiveLock unlock];
        };
        recursiveMethod(5);
    });
}

@synchronized


這其實是一個OC層面的鎖苹丸,主要通過犧牲新能來換取語法上的簡潔與可讀。

我們知道 @synchronized 后面需要緊跟一個 OC 對象苇经,它實際上是把這個對象當(dāng)做鎖來使用赘理。這是通過一個哈希表來實現(xiàn)的,OC 在底層使用了一個互斥鎖的數(shù)組(你可以理解為鎖池)扇单,通過對對象去哈希值來得到對應(yīng)的互斥鎖商模。

具體實現(xiàn)原理可以參考 關(guān)于 @synchronized,這兒比你想知道的還要多蜘澜。

[demo]

這是我自己學(xué)習(xí)的時候?qū)懙囊粋€demo施流,是各種鎖的使用。上面的代碼基本都是這個demo的代碼鄙信;

參考資料


1瞪醋、深入理解iOS開發(fā)中的鎖

2、iOS的線程安全與鎖

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末装诡,一起剝皮案震驚了整個濱河市银受,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赏酥,死亡現(xiàn)場離奇詭異医增,居然都是意外死亡,警方通過查閱死者的電腦和手機谅河,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門咱旱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來确丢,“玉大人,你說我怎么就攤上這事吐限∠式模” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵诸典,是天一觀的道長描函。 經(jīng)常有香客問我,道長狐粱,這世上最難降的妖魔是什么舀寓? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮肌蜻,結(jié)果婚禮上互墓,老公的妹妹穿的比我還像新娘。我一直安慰自己蒋搜,他們只是感情好篡撵,可當(dāng)我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著豆挽,像睡著了一般育谬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上帮哈,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天斑司,我揣著相機與錄音,去河邊找鬼但汞。 笑死宿刮,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的私蕾。 我是一名探鬼主播僵缺,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼踩叭!你這毒婦竟也來了磕潮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤容贝,失蹤者是張志新(化名)和其女友劉穎自脯,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體斤富,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡膏潮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了满力。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片焕参。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡轻纪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出叠纷,到底是詐尸還是另有隱情刻帚,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布涩嚣,位于F島的核電站崇众,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏航厚。R本人自食惡果不足惜校摩,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望阶淘。 院中可真熱鬧衙吩,春花似錦、人聲如沸溪窒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽澈蚌。三九已至摹芙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間宛瞄,已是汗流浹背浮禾。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留份汗,地道東北人盈电。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像杯活,于是被迫代替她去往敵國和親匆帚。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,877評論 2 345

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