iOS開發(fā)中的11種鎖整理

  • 多線程編程被普遍認(rèn)為復(fù)雜肢执,主要是因?yàn)槎嗑€程給程序引入了一定的不可預(yù)知性成畦,要控制這些不可預(yù)知性迫靖,就需要使用各種鎖各種同步機(jī)制自阱,不同的情況就應(yīng)該使用不同的鎖不同的機(jī)制瘩缆。

  • 什么事情一旦放到多線程環(huán)境背桐,要考慮的問題立刻就上升了好幾個(gè)量級(jí)优烧。多線程編程帶來的好處不可勝數(shù),然而工程師只要一不小心链峭,就很容易讓你的程序失去控制畦娄,所以你得用各種鎖各種機(jī)制管住它。

  • 要解決好這些問題弊仪,工程師們就要充分了解這些鎖機(jī)制纷责,分析不同的場景,選擇合適的解決方案撼短。

  • 臨界區(qū):指的是一塊對(duì)公共資源進(jìn)行訪問的代碼,并非一種機(jī)制或是算法挺勿。
  • 自旋鎖:是用于多線程同步的一種鎖曲横,線程反復(fù)檢查鎖變量是否可用。由于線程在這一過程中保持執(zhí)行不瓶,因此是一種忙等待禾嫉。一旦獲取了自旋鎖,線程會(huì)一直保持該鎖蚊丐,直至顯式釋放自旋鎖熙参。 自旋鎖避免了進(jìn)程上下文的調(diào)度開銷,因此對(duì)于線程只會(huì)阻塞很短時(shí)間的場合是有效的麦备。
  • 互斥鎖(Mutex:是一種用于多線程編程中孽椰,防止兩條線程同時(shí)對(duì)同一公共資源(比如全局變量)進(jìn)行讀寫的機(jī)制。該目的通過將代碼切片成一個(gè)一個(gè)的臨界區(qū)而達(dá)成凛篙。
  • 讀寫鎖:是計(jì)算機(jī)程序的并發(fā)控制的一種同步機(jī)制黍匾,也稱“共享-互斥鎖”、多讀者-單寫者鎖) 用于解決多線程對(duì)公共資源讀寫問題呛梆。讀操作可并發(fā)重入锐涯,寫操作是互斥的。 讀寫鎖通常用互斥鎖填物、條件變量纹腌、信號(hào)量實(shí)現(xiàn)霎终。
  • 信號(hào)量(semaphore:是一種更高級(jí)的同步機(jī)制,互斥鎖可以說是semaphore在僅取值0/1時(shí)的特例升薯。信號(hào)量可以有更多的取值空間夏跷,用來實(shí)現(xiàn)更加復(fù)雜的同步,而不單單是線程間互斥猖毫。
  • 條件鎖:就是條件變量吃度,當(dāng)進(jìn)程的某些資源要求不滿足時(shí)就進(jìn)入休眠,也就是鎖住了责语。當(dāng)資源被分配到了炮障,條件鎖打開,進(jìn)程繼續(xù)運(yùn)行坤候。
  • 死鎖:指兩個(gè)或兩個(gè)以上的進(jìn)程在執(zhí)行過程中胁赢,由于競爭資源或者由于彼此通信而造成的一種阻塞的現(xiàn)象,若無外力作用白筹,它們都將無法推進(jìn)下去智末,這些永遠(yuǎn)在互相等待的進(jìn)程稱為死鎖進(jìn)程。
  • 輪詢(Polling):一種CPU決策如何提供周邊設(shè)備服務(wù)的方式徒河,又稱“程控輸出入”系馆。輪詢法的概念是,由CPU定時(shí)發(fā)出詢問顽照,依序詢問每一個(gè)周邊設(shè)備是否需要其服務(wù)由蘑,有即給予服務(wù),服務(wù)結(jié)束后再問下一個(gè)周邊代兵,接著不斷周而復(fù)始尼酿。
1400498-2bfca138992bb7d8.png
//10000000
OSSpinLock:                 112.38 ms
dispatch_semaphore:         160.37 ms
os_unfair_lock:             208.87 ms
pthread_mutex:              302.07 ms
NSCondition:                320.11 ms
NSLock:                     331.80 ms
pthread_rwlock:             360.81 ms
pthread_mutex(recursive):   512.17 ms
NSRecursiveLock:            667.55 ms
NSConditionLock:            999.91 ms
@synchronized:             1654.92 ms
//1000
OSSpinLock:                   0.02 ms
dispatch_semaphore:           0.03 ms
os_unfair_lock:               0.04 ms
pthread_mutex:                0.06 ms
NSLock:                       0.06 ms
pthread_rwlock:               0.07 ms
NSCondition:                  0.07 ms
pthread_mutex(recursive):     0.09 ms
NSRecursiveLock:              0.12 ms
NSConditionLock:              0.18 ms
@synchronized:                0.33 ms
  • 互斥鎖
    • NSLock
    • pthread_mutex
    • pthread_mutex(recursive)遞歸鎖
    • @synchronized
  • 自旋鎖
    • OSSpinLock
    • os_unfair_lock
  • 讀寫鎖
    • pthread_rwlock
  • 遞歸鎖
    • NSRecursiveLock
    • pthread_mutex(recursive)(見上)
  • 條件鎖
    • NSCondition
    • NSConditionLock
  • 信號(hào)量
    • dispatch_semaphore

互斥鎖

1、NSLock

@protocol NSLocking

- (void)lock;
- (void)unlock;

@end

@interface NSLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);
@end
  • NSLock 遵循 NSLocking 協(xié)議植影,lock 方法是加鎖裳擎,unlock 是解鎖,tryLock 是嘗試加鎖思币,如果失敗的話返回 NO鹿响,lockBeforeDate:是在指定Date之前嘗試加鎖,如果在指定時(shí)間之前都不能加鎖谷饿,則返回NO抢野。
// 主線程中
NSLock *lock = [[NSLock alloc] init];
    
//線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [lock lock];
    NSLog(@"線程1");
    sleep(2);
    [lock unlock];
    sleep(1);
    NSLog(@"線程1解鎖成功");
});

//線程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [lock lock];
    NSLog(@"線程2");
    [lock unlock];
});

// 打印:
//   線程1
//   線程2
//   線程1解鎖成功
  • 如果是三個(gè)線程各墨,那么一個(gè)線程在加鎖的時(shí)候指孤,其余請求鎖的線程將形成一個(gè)等待隊(duì)列,按先進(jìn)先出原則
  • 注意:要求NSLocklockunlock需要在同一個(gè)Thread下面

2、pthread_mutex

  • pthread_mutexC 語言下多線程加互斥鎖的方式
  • 被這個(gè)鎖保護(hù)的臨界區(qū)就只允許一個(gè)線程進(jìn)入恃轩,其它線程如果沒有獲得鎖權(quán)限结洼,那就只能在外面等著。
// 用于靜態(tài)的mutex的初始化叉跛,采用默認(rèn)的attr松忍。比如: static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
#define PTHREAD_MUTEX_INITIALIZER {_PTHREAD_MUTEX_SIG_init, {0}}

// 動(dòng)態(tài)的初始化一個(gè)鎖,__restrict 為互斥鎖的類型筷厘,傳 NULL 為默認(rèn)類型鸣峭,一共有 4 類型。
int pthread_mutex_init(pthread_mutex_t * __restrict, const pthread_mutexattr_t * __restrict);

 // 請求鎖酥艳,如果當(dāng)前mutex已經(jīng)被鎖摊溶,那么這個(gè)線程就會(huì)卡在這兒,直到mutex被釋放
int pthread_mutex_lock(pthread_mutex_t *);

// 嘗試請求鎖充石,如果當(dāng)前mutex已經(jīng)被鎖或者不可用莫换,這個(gè)函數(shù)就直接return了,不會(huì)把線程卡住
int pthread_mutex_trylock(pthread_mutex_t *);

// 解鎖
int pthread_mutex_unlock(pthread_mutex_t *);

// 把mutex鎖干掉骤铃,并且釋放所有它所占有的資源
int pthread_mutex_destroy(pthread_mutex_t *);

int pthread_mutex_setprioceiling(pthread_mutex_t * __restrict, int,
  int * __restrict);

int pthread_mutex_getprioceiling(const pthread_mutex_t * __restrict,
  int * __restrict);
  • 鎖類型
PTHREAD_MUTEX_NORMAL 缺省類型拉岁,也就是普通鎖。當(dāng)一個(gè)線程加鎖以后惰爬,其余請求鎖的線程將形成一個(gè)等待隊(duì)列喊暖,并在解鎖后先進(jìn)先出原則獲得鎖。

PTHREAD_MUTEX_ERRORCHECK 檢錯(cuò)鎖撕瞧,如果同一個(gè)線程請求同一個(gè)鎖陵叽,則返回 EDEADLK,否則與普通鎖類型動(dòng)作相同风范。這樣就保證當(dāng)不允許多次加鎖時(shí)不會(huì)出現(xiàn)嵌套情況下的死鎖。

PTHREAD_MUTEX_RECURSIVE 遞歸鎖沪么,允許同一個(gè)線程對(duì)同一個(gè)鎖成功獲得多次硼婿,并通過多次 unlock 解鎖。

PTHREAD_MUTEX_DEFAULT 適應(yīng)鎖禽车,動(dòng)作最簡單的鎖類型寇漫,僅等待解鎖后重新競爭,沒有等待隊(duì)列殉摔。
  • mutex的初始化分兩種州胳,一種是用宏(PTHREAD_MUTEX_INITIALIZER),一種是用函數(shù)(pthread_mutex_init)逸月。
    • 如果沒有特殊的配置要求的話栓撞,使用宏比較好,因?yàn)樗容^快。只有真的需要配置的時(shí)候瓤湘,才需要用函數(shù)瓢颅。
    • 也就是說,凡是pthread_mutex_init(&mutex, NULL)的地方都可以使用PTHREAD_MUTEX_INITIALIZER弛说,因?yàn)樵?code>pthread_mutex_init這個(gè)函數(shù)里的實(shí)現(xiàn)其實(shí)也是用了PTHREAD_MUTEX_INITIALIZER
 ///////////////////// pthread_src/include/pthread/pthread.h
#define PTHREAD_MUTEX_INITIALIZER __PTHREAD_MUTEX_INITIALIZER

///////////////////// pthread_src/sysdeps/generic/bits/mutex.h
// mutex鎖本質(zhì)上是一個(gè)spin lock挽懦,空轉(zhuǎn)鎖
#  define __PTHREAD_MUTEX_INITIALIZER \
    { __PTHREAD_SPIN_LOCK_INITIALIZER, __PTHREAD_SPIN_LOCK_INITIALIZER, 0, 0, 0, 0, 0, 0 } 

///////////////////// pthread_src/sysdeps/generic/pt-mutex-init.c
// 你看,這里其實(shí)用的也是宏木人。就這一句是初始化信柿,下面都是在設(shè)置屬性。
int
_pthread_mutex_init (pthread_mutex_t *mutex,
             const pthread_mutexattr_t *attr)
{
  *mutex = (pthread_mutex_t) __PTHREAD_MUTEX_INITIALIZER; 

  if (! attr
      || memcmp (attr, &__pthread_default_mutexattr, sizeof (*attr) == 0))
    /* The default attributes.  */
    return 0;

  if (! mutex->attr
      || mutex->attr == __PTHREAD_ERRORCHECK_MUTEXATTR
      || mutex->attr == __PTHREAD_RECURSIVE_MUTEXATTR)
      //pthread_mutex_destroy釋放的就是這里的資源
     mutex->attr = malloc (sizeof *attr);                
  if (! mutex->attr) return ENOMEM;
  *mutex->attr = *attr;
  return 0;
}
  • 業(yè)界有另一種說法是:早年的POSIX只支持在static變量上使用PTHREAD_MUTEX_INITIALIZER醒第,所以PTHREAD_MUTEX_INITIALIZER盡量不要到處都用渔嚷,所以使用的時(shí)候你得搞清楚你的pthread的實(shí)現(xiàn)版本是不是比較老的。

  • 使用

#import <pthread.h>

static pthread_mutex_t theLock;
- (void)example5 {
    pthread_mutex_init(&theLock, NULL);
    
    pthread_t thread;
    pthread_create(&thread, NULL, threadMethord1, NULL);
    
    pthread_t thread2;
    pthread_create(&thread2, NULL, threadMethord2, NULL);
}
void *threadMethord1() {
    pthread_mutex_lock(&theLock);
    printf("線程1\n");
    sleep(2);
    pthread_mutex_unlock(&theLock);
    printf("線程1解鎖成功\n");
    return 0;
}
void *threadMethord2() {
    sleep(1);
    pthread_mutex_lock(&theLock);
    printf("線程2\n");
    pthread_mutex_unlock(&theLock);
    return 0;
}

// 打犹约ァ:
//   線程1
//   線程1解鎖成功
//   線程2
  • mutex鎖不是萬能靈藥
    • 基本上所有的問題都可以用互斥的方案去解決圃伶,大不了就是慢點(diǎn)兒,但不要不管什么情況都用互斥蒲列,都能采用這種方案不代表都適合采用這種方案窒朋。
    • 而且這里所說的慢不是說mutex的實(shí)現(xiàn)方案比較慢,而是互斥方案影響的面比較大蝗岖,本來不需要通過互斥就能讓線程進(jìn)入臨界區(qū)侥猩,但用了互斥方案之后,就使這樣的線程不得不等待互斥鎖的釋放抵赢,所以就慢了欺劳。
    • 甚至有些場合用互斥就很蛋疼,比如多資源分配铅鲤,線程步調(diào)通知等划提。 如果是讀多寫少的場合,就比較適合讀寫鎖邢享,如果臨界區(qū)比較短鹏往,就適合空轉(zhuǎn)鎖,后面有介紹
  • 預(yù)防死鎖
    • 1骇塘、如果要進(jìn)入一段臨界區(qū)需要多個(gè)mutex鎖伊履,那么就很容易導(dǎo)致死鎖,單個(gè)mutex鎖是不會(huì)引發(fā)死鎖的款违。
      • 要解決這個(gè)問題也很簡單唐瀑,只要申請鎖的時(shí)候按照固定順序,或者及時(shí)釋放不需要的mutex鎖就可以插爹,尤其是全局mutex鎖的時(shí)候哄辣,更需要遵守一個(gè)約定请梢。
      • 我的mutex鎖的命名規(guī)則就是:
        • 作用_mutex_序號(hào),比如LinkListMutex_mutex_1,OperationQueue_mutex_2柔滔,后面的序號(hào)在每次有新鎖的時(shí)候溢陪,就都加一個(gè)1。如果有哪個(gè)臨界區(qū)進(jìn)入的時(shí)候需要獲得多個(gè)mutex鎖的睛廊,我就按照序號(hào)的順序去進(jìn)行加鎖操作形真,這樣就能夠保證不會(huì)出現(xiàn)死鎖了。
        • 如果是屬于某個(gè)struct內(nèi)部的mutex鎖超全,也一樣咆霜,只不過序號(hào)可以不必跟全局鎖掛鉤,也可以從1開始數(shù)嘶朱。
    • 2蛾坯、還有另一種方案也非常有效,就是用pthread_mutex_trylock函數(shù)來申請加鎖疏遏,這個(gè)函數(shù)在mutex鎖不可用時(shí)脉课,不像pthread_mutex_lock那樣會(huì)等待。pthread_mutex_trylock在申請加鎖失敗時(shí)立刻就會(huì)返回錯(cuò)誤:EBUSY(鎖尚未解除)或者EINVAL(鎖變量不可用)财异。
      • 一旦在trylock的時(shí)候有錯(cuò)誤返回倘零,那就把前面已經(jīng)拿到的鎖全部釋放,然后過一段時(shí)間再來一遍戳寸。
      • 當(dāng)然也可以使用pthread_mutex_timedlock這個(gè)函數(shù)來申請加鎖呈驶,這個(gè)函數(shù)跟pthread_mutex_trylock類似,不同的是疫鹊,你可以傳入一個(gè)時(shí)間參數(shù)袖瞻,在申請加鎖失敗之后會(huì)阻塞一段時(shí)間等解鎖,超時(shí)之后才返回錯(cuò)誤拆吆。
    • 這兩種方案我更多會(huì)使用第一種聋迎,原因如下:
      • 一般情況下進(jìn)入臨界區(qū)需要加的鎖數(shù)量不會(huì)太多,第一種方案能夠hold住枣耀。如果多于2個(gè)霉晕,你就要考慮一下是否有些鎖是可以合并的了。第一種方案適合鎖比較少的情況奕枢,因?yàn)檫@不會(huì)導(dǎo)致非常大的阻塞延時(shí)娄昆。但是當(dāng)你要加的鎖非常多佩微,A缝彬、B、C哺眯、D谷浅、E,你加到D的時(shí)候阻塞了,然而其他線程可能只需要A一疯、B就可以運(yùn)行撼玄,就也會(huì)因?yàn)?code>A、B已經(jīng)被鎖住而阻塞墩邀,這時(shí)候才會(huì)采用第二種方案掌猛。如果要加的鎖本身就不多,只有A眉睹、B兩個(gè)荔茬,那么阻塞一下也還可以。
      • 第二種方案在面臨阻塞的時(shí)候竹海,要操作的事情太多慕蔚。當(dāng)你把所有的鎖都釋放以后,你的當(dāng)前線程的處理策略就會(huì)導(dǎo)致你的代碼復(fù)雜度上升:當(dāng)前線程總不能就此退出吧斋配,你得找個(gè)地方把它放起來孔飒,讓它去等待一段時(shí)間之后再去申請鎖,如果有多個(gè)線程出現(xiàn)了這樣的情況艰争,你就需要一個(gè)線程池來存放這些等待解鎖的線程坏瞄。如果臨界區(qū)是嵌套的,你在把這個(gè)線程掛起的時(shí)候园细,最好還要把外面的鎖也釋放掉惦积,要不然也會(huì)容易導(dǎo)致死鎖,這就需要你在一個(gè)地方記錄當(dāng)前線程使用鎖的情況猛频。這里要做的事情太多狮崩,復(fù)雜度比較大,容易出錯(cuò)鹿寻。
    • 所以總而言之睦柴,設(shè)計(jì)的時(shí)候盡量減少同一臨界區(qū)所需要mutex鎖的數(shù)量,然后采用第一種方案毡熏。如果確實(shí)有需求導(dǎo)致那么多mutex鎖坦敌,那么就只能采用第二種方案了,然后老老實(shí)實(shí)寫好周邊代碼痢法。
    • 但是到了semaphore情況下的死鎖處理方案時(shí)狱窘,上面兩種方案就都不頂用了,后面我會(huì)說财搁。另外蘸炸,還有一種死鎖是自己把自己鎖死了,這個(gè)我在后面也會(huì)說尖奔。

pthread_mutex(recursive)遞歸鎖

  • pthread_mutex鎖也支持遞歸搭儒,只需要設(shè)置PTHREAD_MUTEX_RECURSIVE即可
  • 這是pthread_mutex為了防止在遞歸的情況下出現(xiàn)死鎖而出現(xiàn)的遞歸鎖穷当。作用和NSRecursiveLock遞歸鎖類似。
  • 通過 pthread_mutexattr_t 來設(shè)置鎖的類型淹禾,如下面代碼就設(shè)置鎖為遞歸鎖馁菜。
- (void)example5 {
    // 定義mutex
    pthread_mutex_init(&theLock, NULL);
    
    // 定義mutexattr
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    
    pthread_mutex_init(&theLock, &attr);
    pthread_mutexattr_destroy(&attr);
    
    pthread_t thread;
    pthread_create(&thread, NULL, threadMethord, 5);
}

void *threadMethord(int value) {
    pthread_mutex_lock(&theLock);
    
    if (value > 0) {
        printf("Value:%i\n", value);
        sleep(1);
        threadMethord(value - 1);
    }
    pthread_mutex_unlock(&theLock);
    return 0;
}

// Value:5
// Value:4
// Value:3
// Value:2
// Value:1

3、@synchronized

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    @synchronized(self) {
        sleep(2);
        NSLog(@"線程1");
    }
    sleep(1);
    NSLog(@"線程1解鎖成功");
});
    
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    @synchronized(self) {
        NSLog(@"線程2");
    }
});

// 線程1
// 線程2
// 線程1解鎖成功
  • @synchronized(object) 指令使用的 object 為該鎖的唯一標(biāo)識(shí)铃岔,只有當(dāng)標(biāo)識(shí)相同時(shí)汪疮,才滿足互斥,所以如果線程 2 中的 @synchronized(self) 改為@synchronized(self.view)毁习,則線程2就不會(huì)被阻塞
  • @synchronized 指令實(shí)現(xiàn)鎖的優(yōu)點(diǎn)就是我們不需要在代碼中顯式的創(chuàng)建鎖對(duì)象铲咨,便可以實(shí)現(xiàn)鎖的機(jī)制,但作為一種預(yù)防措施蜓洪,@synchronized 塊會(huì)隱式的添加一個(gè)異常處理例程來保護(hù)代碼纤勒,該處理例程會(huì)在異常拋出的時(shí)候自動(dòng)的釋放互斥鎖。
  • 如果在 @sychronized(object){} 內(nèi)部 object 被釋放或被設(shè)為 nil隆檀,從測試的結(jié)果來看摇天,的確沒有問題,但如果 object 一開始就是 nil恐仑,則失去了鎖的功能泉坐。但 @synchronized([NSNull null]) 是完全可以的。

自旋鎖

1裳仆、OSSpinLock

  • OSSpinLock 是一種自旋鎖腕让,也只有加鎖,解鎖歧斟,嘗試加鎖三個(gè)方法纯丸。
  • NSLock 不同的是 NSLock 請求加鎖失敗的話,會(huì)先輪詢静袖,但一秒過后便會(huì)使線程進(jìn)入 waiting 狀態(tài)觉鼻,等待喚醒。
  • OSSpinLock 會(huì)一直輪詢队橙,等待時(shí)會(huì)消耗大量 CPU 資源坠陈,不適用于較長時(shí)間的任務(wù)。
  • 10.0棄用捐康,使用os_unfair_lock
typedef int32_t OSSpinLock;
//嘗試加鎖
bool    OSSpinLockTry( volatile OSSpinLock *__lock );
//加鎖
void    OSSpinLockLock( volatile OSSpinLock *__lock );
//解鎖
void    OSSpinLockUnlock( volatile OSSpinLock *__lock );
  • 使用
#import <libkern/OSSpinLockDeprecated.h>

__block OSSpinLock theLock = OS_SPINLOCK_INIT;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    OSSpinLockLock(&theLock);
    NSLog(@"需要線程同步的操作1 開始");
    sleep(3);
    NSLog(@"需要線程同步的操作1 結(jié)束");
    OSSpinLockUnlock(&theLock);
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    OSSpinLockLock(&theLock);
    sleep(1);
    NSLog(@"需要線程同步的操作2");
    OSSpinLockUnlock(&theLock);
});
// 需要線程同步的操作1 開始
// 需要線程同步的操作1 結(jié)束
// 需要線程同步的操作2

os_unfair_lock

  • os_unfair_lock 是蘋果官方推薦的替換OSSpinLock的方案仇矾,但是它在iOS10.0以上的系統(tǒng)才可以調(diào)用。
// 簡單使用
os_unfair_lock_t unfairLock;
unfairLock = &(OS_UNFAIR_LOCK_INIT);
os_unfair_lock_lock(unfairLock);
os_unfair_lock_unlock(unfairLock);

讀寫鎖

1解总、pthread_rwlock

  • 前面互斥鎖mutex有個(gè)缺點(diǎn)贮匕,就是只要鎖住了,不管其他線程要干什么倾鲫,都不允許進(jìn)入臨界區(qū)粗合。
    • 設(shè)想這樣一種情況:臨界區(qū)foo變量在被bar1線程讀著,加了個(gè)mutex鎖乌昔,bar2線程如果也要讀foo變量隙疚,因?yàn)楸?code>bar1加了個(gè)互斥鎖,那就不能讀了磕道。
    • 但事實(shí)情況是供屉,讀取數(shù)據(jù)不影響數(shù)據(jù)內(nèi)容本身,所以即便被1個(gè)線程讀著溺蕉,另外一個(gè)線程也應(yīng)該允許他去讀伶丐。除非另外一個(gè)線程是寫操作,為了避免數(shù)據(jù)不一致的問題疯特,寫線程就需要等讀線程都結(jié)束了再寫哗魂。
  • 因此誕生了讀寫鎖,有的地方也叫共享鎖漓雅。
  • 讀寫鎖的特性是這樣的
    • 當(dāng)一個(gè)線程加了讀鎖訪問臨界區(qū)录别,另外一個(gè)線程也想訪問臨界區(qū)讀取數(shù)據(jù)的時(shí)候,也可以加一個(gè)讀鎖邻吞,這樣另外一個(gè)線程就能夠成功進(jìn)入臨界區(qū)進(jìn)行讀操作了组题。此時(shí)讀鎖線程有兩個(gè)。
    • 當(dāng)?shù)谌齻€(gè)線程需要進(jìn)行寫操作時(shí)抱冷,它需要加一個(gè)寫鎖崔列,這個(gè)寫鎖只有在讀鎖的擁有者為0時(shí)才有效。也就是等前兩個(gè)讀線程都釋放讀鎖之后旺遮,第三個(gè)線程就能進(jìn)去寫了赵讯。
  • 總結(jié)一下就是,讀寫鎖里耿眉,讀鎖能允許多個(gè)線程同時(shí)去讀瘦癌,但是寫鎖在同一時(shí)刻只允許一個(gè)線程去寫。
  • 這樣更精細(xì)的控制跷敬,就能減少mutex導(dǎo)致的阻塞延遲時(shí)間讯私。雖然用mutex也能起作用,但這種場合西傀,明顯讀寫鎖更好斤寇!
PTHREAD_RWLOCK_INITIALIZER

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

//加讀鎖
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

//加寫鎖
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

//解鎖
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

// 這個(gè)函數(shù)在Linux和Mac的man文檔里都沒有,新版的pthread.h里面也沒有拥褂,舊版的能找到
int pthread_rwlock_timedrdlock_np(pthread_rwlock_t *rwlock, const struct timespec *deltatime); 
// 同上
int pthread_rwlock_timedwrlock_np(pthread_rwlock_t *rwlock, const struct timespec *deltatime); 

注意的地方

  • 命名
    • 跟上面提到的寫muetx互斥鎖的約定一樣娘锁,操作,類別饺鹃,序號(hào)最好都要有莫秆。比如OperationQueue_rwlock_1间雀。
  • 認(rèn)真區(qū)分使用場合
    • 由于讀寫鎖的性質(zhì),在默認(rèn)情況下是很容易出現(xiàn)寫線程饑餓的镊屎。因?yàn)樗仨氁鹊剿凶x鎖都釋放之后惹挟,才能成功申請寫鎖。不過不同系統(tǒng)的實(shí)現(xiàn)版本對(duì)寫線程的優(yōu)先級(jí)實(shí)現(xiàn)不同缝驳。Solaris下面就是寫線程優(yōu)先连锯,其他系統(tǒng)默認(rèn)讀線程優(yōu)先。
    • 比如在寫線程阻塞的時(shí)候用狱,有很多讀線程是可以一個(gè)接一個(gè)地在那兒插隊(duì)的(在默認(rèn)情況下运怖,只要有讀鎖在,寫鎖就無法申請夏伊,然而讀鎖可以一直申請成功摇展,就導(dǎo)致所謂的插隊(duì)現(xiàn)象),那么寫線程就不知道什么時(shí)候才能申請成功寫鎖了溺忧,然后它就餓死了吗购。
    • 為了控制寫線程饑餓,必須要在創(chuàng)建讀寫鎖的時(shí)候設(shè)置PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE砸狞,不要用PTHREAD_RWLOCK_PREFER_WRITER_NP捻勉,這個(gè)似乎沒什么用,感覺應(yīng)該是個(gè)bug
////////////////////////////// /usr/include/pthread.h

/* Read-write lock types.  */
#if defined __USE_UNIX98 || defined __USE_XOPEN2K
enum
{
  PTHREAD_RWLOCK_PREFER_READER_NP,
  PTHREAD_RWLOCK_PREFER_WRITER_NP, // 媽蛋刀森,沒用踱启,一樣reader優(yōu)先
  PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP,
  PTHREAD_RWLOCK_DEFAULT_NP = PTHREAD_RWLOCK_PREFER_READER_NP
};
  • 總的來說,這樣的鎖建立之后一定要設(shè)置優(yōu)先級(jí)研底,不然就容易出現(xiàn)寫線程饑餓埠偿。而且讀寫鎖適合讀多寫少的情況,如果讀榜晦、寫一樣多冠蒋,那這時(shí)候還是用mutex互斥鎖比較合理。

遞歸鎖

  • 遞歸鎖有一個(gè)特點(diǎn)乾胶,就是同一個(gè)線程可以加鎖N次而不會(huì)引發(fā)死鎖抖剿。

1、NSRecursiveLock

@interface NSRecursiveLock : NSObject <NSLocking> {
@private
    void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);
@end
  • NSRecursiveLock 是遞歸鎖识窿,他和 NSLock 的區(qū)別在于斩郎,NSRecursiveLock 可以在一個(gè)線程中重復(fù)加鎖(反正單線程內(nèi)任務(wù)是按順序執(zhí)行的,不會(huì)出現(xiàn)資源競爭問題)喻频,NSRecursiveLock 會(huì)記錄上鎖和解鎖的次數(shù)缩宜,當(dāng)二者平衡的時(shí)候,才會(huì)釋放鎖,其它線程才可以上鎖成功锻煌。
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    static void (^RecursiveBlock)(int);
    RecursiveBlock = ^(int value) {
        [lock lock];
        if (value > 0) {
            NSLog(@"value:%d", value);
            RecursiveBlock(value - 1);
        }
        [lock unlock];
    };
    RecursiveBlock(2);
});

// value:2
// value:1
  • 如上面的示例妓布,如果用 NSLock 的話,lock 先鎖上了宋梧,但未執(zhí)行解鎖的時(shí)候匣沼,就會(huì)進(jìn)入遞歸的下一層,而再次請求上鎖乃秀,阻塞了該線程,線程被阻塞了圆兵,自然后面的解鎖代碼不會(huì)執(zhí)行跺讯,而形成了死鎖。而 NSRecursiveLock 遞歸鎖就是為了解決這個(gè)問題殉农。主要是用在循環(huán)或遞歸操作中刀脏。
  • 這段代碼是一個(gè)典型的死鎖情況。在我們的線程中超凳,RecursiveMethod是遞歸調(diào)用的愈污。所以每次進(jìn)入這個(gè)block時(shí),都會(huì)去加一次鎖轮傍,而從第二次開始暂雹,由于鎖已經(jīng)被使用了且沒有解鎖,所以它需要等待鎖被解除创夜,這樣就導(dǎo)致了死鎖杭跪,線程被阻塞住了。

2驰吓、pthread_mutex(recursive)

見上文

條件鎖

1涧尿、NSCondition

@interface NSCondition : NSObject <NSLocking> {
@private
    void *_priv;
}

- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;
  • 一種最基本的條件鎖。手動(dòng)控制線程waitsignal檬贰。
  • 遵循NSLocking協(xié)議姑廉,使用的時(shí)候同樣是lock,unlock加解鎖,wait是傻等翁涤,waitUntilDate:方法是等一會(huì)桥言,都會(huì)阻塞掉線程,signal是喚起一個(gè)在等待的線程葵礼,broadcast是廣播全部喚起限书。
  • NSCondition 的對(duì)象實(shí)際上作為一個(gè)鎖和一個(gè)線程檢查器,鎖上之后其它線程也能上鎖章咧,而之后可以根據(jù)條件決定是否繼續(xù)運(yùn)行線程倦西,即線程是否要進(jìn)入 waiting 狀態(tài),經(jīng)測試赁严,NSCondition 并不會(huì)像上文的那些鎖一樣扰柠,先輪詢粉铐,而是直接進(jìn)入 waiting 狀態(tài),當(dāng)其它線程中的該鎖執(zhí)行 signal 或者 broadcast 方法時(shí)卤档,線程被喚醒蝙泼,繼續(xù)運(yùn)行之后的方法。
NSCondition *lock = [[NSCondition alloc] init];
NSMutableArray *array = [[NSMutableArray alloc] init];
//線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [lock lock];
    while (!array.count) {
        [lock wait];
    }
    [array removeAllObjects];
    NSLog(@"array removeAllObjects");
    [lock unlock];
});
    
//線程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(1);//以保證讓線程2的代碼后執(zhí)行
    [lock lock];
    [array addObject:@1];
    NSLog(@"array addObject:@1");
    [lock signal];
    [lock unlock];
});

// array addObject:@1
// array removeAllObjects
  • 其中 signalbroadcast 方法的區(qū)別在于劝枣,signal 只是一個(gè)信號(hào)量汤踏,只能喚醒一個(gè)等待的線程,想喚醒多個(gè)就得多次調(diào)用舔腾,而 broadcast 可以喚醒所有在等待的線程溪胶。如果沒有等待的線程,這兩個(gè)方法都沒有作用稳诚。

2哗脖、NSConditionLock

@interface NSConditionLock : NSObject <NSLocking> {
@private
    void *_priv;
}
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

@property (readonly) NSInteger condition;

- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
  • NSConditionLockNSLock 類似,都遵循 NSLocking 協(xié)議扳还,方法都類似才避,只是多了一個(gè) condition 屬性,以及每個(gè)操作都多了一個(gè)關(guān)于 condition 屬性的方法氨距,只有 condition 參數(shù)與初始化時(shí)候的 condition 相等桑逝,lock 才能正確進(jìn)行加鎖操作。而 unlockWithCondition: 并不是當(dāng) Condition 符合條件時(shí)才解鎖俏让,而是解鎖之后肢娘,修改 Condition 的值,這個(gè)結(jié)論可以從下面的例子中得出舆驶。
//主線程中
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:0];
    
//線程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [lock lockWhenCondition:1];
    NSLog(@"線程1");
    sleep(2);
    [lock unlock];
});
    
//線程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(1);//以保證讓線程2的代碼后執(zhí)行
    if ([lock tryLockWhenCondition:0]) {
        NSLog(@"線程2");
        [lock unlockWithCondition:2];
        NSLog(@"線程2解鎖成功");
    } else {
        NSLog(@"線程2嘗試加鎖失敗");
    }
});
    
//線程3
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(2);//以保證讓線程2的代碼后執(zhí)行
    if ([lock tryLockWhenCondition:2]) {
        NSLog(@"線程3");
        [lock unlock];
        NSLog(@"線程3解鎖成功");
    } else {
        NSLog(@"線程3嘗試加鎖失敗");
    }
});
    
//線程4
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(3);//以保證讓線程2的代碼后執(zhí)行
    if ([lock tryLockWhenCondition:2]) {
        NSLog(@"線程4");
        [lock unlockWithCondition:1];    
        NSLog(@"線程4解鎖成功");
    } else {
        NSLog(@"線程4嘗試加鎖失敗");
    }
});
    
// 線程2
// 線程2解鎖成功
// 線程3
// 線程3解鎖成功
// 線程4
// 線程4解鎖成功
// 線程1
  • 從上面可以得出橱健,NSConditionLock 還可以實(shí)現(xiàn)任務(wù)之間的依賴。

信號(hào)量

dispatch_semaphore

//傳入的參數(shù)為long沙廉,輸出一個(gè)dispatch_semaphore_t類型且值為value的信號(hào)量拘荡。
//值得注意的是,這里的傳入的參數(shù)value必須大于或等于0撬陵,否則dispatch_semaphore_create會(huì)返回NULL珊皿。
dispatch_semaphore_create(long value);

//這個(gè)函數(shù)會(huì)使傳入的信號(hào)量dsema的值減1;
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

//這個(gè)函數(shù)會(huì)使傳入的信號(hào)量dsema的值加1巨税;
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
  • dispatch_semaphoreGCD 用來同步的一種方式蟋定,與他相關(guān)的只有三個(gè)函數(shù),一個(gè)是創(chuàng)建信號(hào)量草添,一個(gè)是等待信號(hào)驶兜,一個(gè)是發(fā)送信號(hào)。
  • 這個(gè)函數(shù)的作用是這樣的
    • 如果dsema信號(hào)量的值大于0,該函數(shù)所處線程就繼續(xù)執(zhí)行下面的語句抄淑,并且將信號(hào)量的值減1屠凶;
    • 如果desema的值為0,那么這個(gè)函數(shù)就阻塞當(dāng)前線程等待timeout(注意timeout的類型為dispatch_time_t肆资,不能直接傳入整形或float型數(shù))
    • 如果等待的期間desema的值被dispatch_semaphore_signal函數(shù)加1了矗愧,且該函數(shù)(即dispatch_semaphore_wait)所處線程獲得了信號(hào)量,那么就繼續(xù)向下執(zhí)行并將信號(hào)量減1郑原。
    • 如果等待期間沒有獲取到信號(hào)量或者信號(hào)量的值一直為0唉韭,那么等到timeout時(shí),其所處線程自動(dòng)執(zhí)行其后語句犯犁。
  • dispatch_semaphore 是信號(hào)量属愤,但當(dāng)信號(hào)總量設(shè)為 1 時(shí)也可以當(dāng)作鎖來。在沒有等待情況出現(xiàn)時(shí)栖秕,它的性能比 pthread_mutex 還要高春塌,但一旦有等待情況出現(xiàn)時(shí)晓避,性能就會(huì)下降許多簇捍。相對(duì)于 OSSpinLock 來說,它的優(yōu)勢在于等待時(shí)不會(huì)消耗 CPU 資源俏拱。
// 超時(shí)時(shí)間overTime設(shè)置成>2
dispatch_semaphore_t signal = dispatch_semaphore_create(1);
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    dispatch_semaphore_wait(signal, overTime);
        NSLog(@"需要線程同步的操作1 開始");
        sleep(2);
        NSLog(@"需要線程同步的操作1 結(jié)束");
    dispatch_semaphore_signal(signal);
});
    
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    sleep(1);
    dispatch_semaphore_wait(signal, overTime);
        NSLog(@"需要線程同步的操作2");
    dispatch_semaphore_signal(signal);
});
// 需要線程同步的操作1 開始
// 需要線程同步的操作1 結(jié)束
// 需要線程同步的操作2

// 超時(shí)時(shí)間設(shè)置為<2s的時(shí)候
//...
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC);
//...
// 需要線程同步的操作1 開始
// 需要線程同步的操作2
// 需要線程同步的操作1 結(jié)束
  • 如上的代碼暑塑,如果超時(shí)時(shí)間overTime設(shè)置成>2,可完成同步操作锅必。如果overTime<2的話事格,在線程1還沒有執(zhí)行完成的情況下,此時(shí)超時(shí)了搞隐,將自動(dòng)執(zhí)行下面的代碼驹愚。
  • dispatch_semaphoreNSCondition 類似,都是一種基于信號(hào)的同步方式劣纲,但 NSCondition 信號(hào)只能發(fā)送逢捺,不能保存(如果沒有線程在等待,則發(fā)送的信號(hào)會(huì)失效)癞季。而 dispatch_semaphore 能保存發(fā)送的信號(hào)劫瞳。dispatch_semaphore 的核心是 dispatch_semaphore_t 類型的信號(hào)量。
  • dispatch_semaphore_create(1) 方法可以創(chuàng)建一個(gè) dispatch_semaphore_t 類型的信號(hào)量绷柒,設(shè)定信號(hào)量的初始值為 1志于。注意,這里的傳入的參數(shù)必須大于或等于 0废睦,否則 dispatch_semaphore_create 會(huì)返回 NULL伺绽。
  • dispatch_semaphore_wait(signal, overTime);方法會(huì)判斷 signal 的信號(hào)值是否大于 0。大于 0 不會(huì)阻塞線程,消耗掉一個(gè)信號(hào)憔恳,執(zhí)行后續(xù)任務(wù)瓤荔。
    • 如果信號(hào)值為 0,該線程會(huì)和 NSCondition 一樣直接進(jìn)入 waiting 狀態(tài)钥组,等待其他線程發(fā)送信號(hào)喚醒線程去執(zhí)行后續(xù)任務(wù)输硝,或者當(dāng) overTime 時(shí)限到了,也會(huì)執(zhí)行后續(xù)任務(wù)程梦。
  • dispatch_semaphore_signal(signal); 發(fā)送信號(hào)点把,如果沒有等待的線程接受信號(hào),則使 signal 信號(hào)值加1(做到對(duì)信號(hào)的保存)屿附。
  • NSLocklockunlock類似郎逃,區(qū)別只在于有信號(hào)量這個(gè)參數(shù),lock unlock 只能同一時(shí)間挺份,一個(gè)線程訪問被保護(hù)的臨界區(qū)褒翰,而如果 dispatch_semaphore 的信號(hào)量初始值為 x ,則可以有 x 個(gè)線程同時(shí)訪問被保護(hù)的臨界區(qū)匀泊。

補(bǔ)充

pthread_cleanup_push() & pthread_cleanup_pop()

  • 線程是允許在退出的時(shí)候优训,調(diào)用一些回調(diào)方法的。如果你需要做類似的事情各聘,那么就用以下這兩種方法:
    void pthread_cleanup_push(void (*callback)(void *), void *arg);
    void pthread_cleanup_pop(int execute);
  • 正如名字所暗示的揣非,它背后有一個(gè)stack,你可以塞很多個(gè)callback函數(shù)進(jìn)去躲因,然后調(diào)用的時(shí)候按照先入后出的順序調(diào)用這些callback早敬。所以你在塞callback的時(shí)候,如果是關(guān)心調(diào)用順序的大脉,那就得注意這一點(diǎn)了搞监。
  • 但是!你塞進(jìn)去的callback只有在以下情況下才會(huì)被調(diào)用:
    • 線程通過pthread_exit()函數(shù)退出
    • 線程被pthread_cancel()取消
    • pthread_cleanup_pop(int execute)時(shí)镰矿,execute傳了一個(gè)非0
  • 也就是說琐驴,如果你的線程函數(shù)是這么寫的,那在線程結(jié)束的時(shí)候就不會(huì)調(diào)到你塞進(jìn)去的那些callback了:
static void * thread_function(void *args)
{
    ...
    ...
    ...
    ...
    return 0; // 線程退出時(shí)沒有調(diào)用pthread_exit()退出衡怀,而是直接return棍矛,此時(shí)是不會(huì)調(diào)用棧內(nèi)callback的
}
  • pthread_cleanup_push塞入的callback可以用來記錄線程結(jié)束的點(diǎn),般不太會(huì)在這里執(zhí)行業(yè)務(wù)邏輯抛杨。在線程結(jié)束之后如果要執(zhí)行業(yè)務(wù)邏輯够委,一般用下面提到的pthread_join
注意事項(xiàng):callback函數(shù)是可以傳參數(shù)的
  • pthread_cleanup_push函數(shù)中怖现,第二個(gè)參數(shù)的值會(huì)作為callback函數(shù)的第一個(gè)參數(shù)茁帽,拿來打打日志也不錯(cuò)
void callback(void *callback_arg)
{
    printf("arg is : %s\n", (char *)callback_arg);
}

static void * thread_function(void *thread_arg)
{
    ...
    pthread_cleanup_push(callback, "this is a queue thread, and was terminated.");
    ...
    pthread_exit((void *) 0); // 這句不調(diào)用玉罐,線程結(jié)束就不會(huì)調(diào)用你塞進(jìn)去的callback函數(shù)。
    return ((void *) 0);
}

int main ()
{
    ...
    ...
    error = pthread_create(&tid, NULL, thread_function, (void *)thread_arg)
    ...
    ...
    return 0;
}
要保持callback棧平衡
pthread_cleanup_pop(0); // 傳遞參數(shù)0潘拨,在pop的時(shí)候就不會(huì)調(diào)用對(duì)應(yīng)的callback吊输,如果傳遞非0值剩拢,pop的時(shí)候就會(huì)調(diào)用對(duì)應(yīng)callback了稠炬。
pthread_cleanup_pop(0); // push了兩次就pop兩次,你要是只pop一次就不能編譯通過
  • pthread對(duì)于這兩個(gè)函數(shù)是通過宏來實(shí)現(xiàn)的妈拌,如果沒有一一對(duì)應(yīng)章郁,編譯器就會(huì)報(bào)} missing的錯(cuò)誤。其相關(guān)實(shí)現(xiàn)代碼如下:
/* ./include/pthread/pthread.h */
#define pthread_cleanup_push(rt, rtarg) __pthread_cleanup_push(rt, rtarg)
#define pthread_cleanup_pop(execute) __pthread_cleanup_pop(execute)

/* ./sysdeps/generic/bits/cancelation.h */
#define __pthread_cleanup_push(rt, rtarg) \
    { \
      struct __pthread_cancelation_handler **__handlers \
        = __pthread_get_cleanup_stack (); \
      struct __pthread_cancelation_handler __handler = \
        { \
          (rt), \
          (rtarg), \
          *__handlers \
        }; \
      *__handlers = &__handler;

#define __pthread_cleanup_pop(execute) \
      if (execute) \
        __handler.handler (__handler.arg); \
       *__handlers = __handler.next; \
    } \

具體細(xì)節(jié)請參考推薦文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末纯命,一起剝皮案震驚了整個(gè)濱河市均牢,隨后出現(xiàn)的幾起案子嘉冒,更是在濱河造成了極大的恐慌涩禀,老刑警劉巖料滥,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異艾船,居然都是意外死亡葵腹,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門屿岂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來践宴,“玉大人,你說我怎么就攤上這事雁社≡【” “怎么了晒骇?”我有些...
    開封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵霉撵,是天一觀的道長。 經(jīng)常有香客問我洪囤,道長徒坡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任瘤缩,我火速辦了婚禮喇完,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘剥啤。我一直安慰自己锦溪,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開白布府怯。 她就那樣靜靜地躺著刻诊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪牺丙。 梳的紋絲不亂的頭發(fā)上则涯,一...
    開封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天复局,我揣著相機(jī)與錄音,去河邊找鬼粟判。 笑死亿昏,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的档礁。 我是一名探鬼主播角钩,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼呻澜!你這毒婦竟也來了彤断?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤易迹,失蹤者是張志新(化名)和其女友劉穎宰衙,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體睹欲,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡供炼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了窘疮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片袋哼。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖闸衫,靈堂內(nèi)的尸體忽然破棺而出涛贯,到底是詐尸還是另有隱情,我是刑警寧澤蔚出,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布弟翘,位于F島的核電站,受9級(jí)特大地震影響骄酗,放射性物質(zhì)發(fā)生泄漏稀余。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一趋翻、第九天 我趴在偏房一處隱蔽的房頂上張望睛琳。 院中可真熱鬧,春花似錦踏烙、人聲如沸师骗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辟癌。三九已至,卻和暖如春步脓,著一層夾襖步出監(jiān)牢的瞬間愿待,已是汗流浹背浩螺。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留仍侥,地道東北人要出。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像农渊,于是被迫代替她去往敵國和親患蹂。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

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

  • 瀏覽器兼容問題一:不同瀏覽器的標(biāo)簽?zāi)J(rèn)的外補(bǔ)丁和內(nèi)補(bǔ)丁不同 問題癥狀:隨便寫幾個(gè)標(biāo)簽砸紊,不加樣式控制的情況下传于,各自的...
    電影里的夢i閱讀 182評(píng)論 0 0
  • 走心不可怕,習(xí)慣才可怕
    Lula_Lula0閱讀 244評(píng)論 0 1
  • 報(bào)錯(cuò)提示 錯(cuò)誤原因 Xcode7.1之后新特性要求App內(nèi)訪問網(wǎng)絡(luò)請求醉顽,要采用 HTTPS 協(xié)議沼溜。 解決方法 在I...
    Jackson_Chan閱讀 201評(píng)論 0 0