OC底層原理21-鎖的原理

iOS--OC底層原理文章匯總

本文探索常用鎖以及@synchronized底層的原理。

鎖的分類

在開發(fā)中,使用最常見的恐怕就是@synchronized(互斥鎖)、NSLock(互斥鎖)、以及dispatch_semaphore(信號(hào)量)。其實(shí)還有許多種,總分類有:互斥鎖、自旋鎖,細(xì)分之下多出了: 讀寫鎖、遞歸鎖、條件鎖、信號(hào)量,后三者是對基本鎖的上層封裝。先介紹幾個(gè)概念。

自旋鎖】是用于多線程同步的一種鎖,線程反復(fù)檢查鎖變量是否可用(即可重入特性)。由于線程在這一過程中保持執(zhí)行,因此是一種忙等待。一旦獲取了自旋鎖,線程會(huì)一直保持該鎖,直至顯式釋放自旋鎖。 自旋鎖避免了進(jìn)程上下文的調(diào)度開銷,因此對于線程只會(huì)阻塞很短時(shí)間的場合是有效的。

【互斥鎖】是一種用于多線程編程中,防止兩條線程同時(shí)對同一公共資源(比如全局變量)進(jìn)行讀寫的機(jī)制。該目的通過將代碼切片成一個(gè)一個(gè)的臨界區(qū)而達(dá)成。

遞歸鎖(recursive_mutext_t)是一種特殊的互斥鎖。

【讀寫鎖】是計(jì)算機(jī)程序的并發(fā)控制的一種同步機(jī)制(也稱“共享-互斥鎖”、多讀-單寫鎖) 用于解決多線程對公共資源讀寫問題。讀的操作可并發(fā)重入,寫操作是互斥的。 讀寫鎖通常用互斥鎖、條件變量、信號(hào)量實(shí)現(xiàn)。

【信號(hào)量】是一種更高級(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)行。

對應(yīng)有以下鎖:
  1. OSSpinLock(自旋鎖)
  2. dispatch_semaphone(信號(hào)量)
  3. pthread_mutex(互斥鎖)
  4. NSLock(互斥鎖)
  5. NSCondition(條件鎖)
  6. os_unfair_lock (互斥鎖)
  7. pthread_mutex(recursive 互斥遞歸鎖)
  8. NSRecursiveLock(遞歸鎖)
  9. NSConditionLock(條件鎖)
  10. synchronized(互斥遞歸鎖)

OSSpinLock(自旋鎖)

  • 與互斥鎖(阻塞-睡眠)不同,自旋鎖加鎖后是進(jìn)入忙等狀態(tài)。
  • 如果共享數(shù)據(jù)已經(jīng)有其他線程加鎖了,線程會(huì)以忙等的方式等待鎖,一旦被訪問的資源被解鎖,則等待資源的線程會(huì)立即執(zhí)行。

OSSpinLock效率很高,但是已不再安全。如果一個(gè)低優(yōu)先級(jí)的線程獲得鎖并訪問共享資源,這時(shí)一個(gè)高優(yōu)先級(jí)的線程也嘗試獲得這個(gè)鎖,它會(huì)處于 spin lock 的忙等狀態(tài)從而占用大量 CPU。此時(shí)低優(yōu)先級(jí)線程無法與高優(yōu)先級(jí)線程爭奪 CPU 時(shí)間,從而導(dǎo)致任務(wù)遲遲完不成、無法釋放 lock。

ibireme 大神--<不再安全的 OSSpinLock>

各類型鎖的性能

所以蘋果已經(jīng)推薦使用os_unfair_lock。

  • os_unfair_lock基本使用
    關(guān)于os_unfair_lock是蘋果在iOS10之后推出,它屬于互斥鎖,os_unfair_lock加鎖會(huì)讓等待的線程進(jìn)入休眠狀態(tài),而不是忙等。這樣就提高了安全也降低了性能損耗。
#import <os/lock.h>
// 創(chuàng)建一個(gè) os_unfair_lock_t 鎖
os_unfair_lock_t unfairLock;
// 先分配此類型的變量并將其初始化為OS_UNFAIR_LOCK_INIT
unfairLock = &(OS_UNFAIR_LOCK_INIT);
// 嘗試加鎖,返回YES or NO
os_unfair_lock_trylock(unfairLock)
// 加鎖
os_unfair_lock_lock(unfairLock);
// 解鎖
os_unfair_lock_unlock(unfairLock);

dispatch_semaphone(信號(hào)量)

信號(hào)量適用于異步線程同步操作的場景。

    // 創(chuàng)建使用
    dispatch_semaphore_create(long value); // 創(chuàng)建信號(hào)量
    dispatch_semaphore_signal(dispatch_semaphore_t deem); // 發(fā)送信號(hào)量
    dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); // 等待信號(hào)量
    //  ??注意: 發(fā)送信號(hào)量和信號(hào)等待是成對出現(xiàn)

    // 常見使用場景
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{ // ①
        
        NSLog(@"任務(wù)1:%@",[NSThread currentThread]);
        dispatch_semaphore_signal(sem); // ③
    });
    
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // ②
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任務(wù)2:%@",[NSThread currentThread]);
        dispatch_semaphore_signal(sem); // ⑤
    });
    
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // ④
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任務(wù)3:%@",[NSThread currentThread]); // ⑥
    });
    
    // 執(zhí)行順序:① - ② - ③ - ④ - ⑤ - ⑥

}

通過控制信號(hào)量通過數(shù),就可實(shí)現(xiàn)鎖的功能。

pthread_mutex(互斥鎖)

  • 阻塞線程并sleep(加鎖),加鎖過程中切換上下(主動(dòng)出讓時(shí)間片,線程休眠,等待下一次喚醒)、cpu的搶占、信號(hào)的發(fā)送等開銷。
  • 如果共享數(shù)據(jù)已經(jīng)有其他線程加鎖了,線程會(huì)進(jìn)入休眠狀態(tài)等待鎖。一旦被訪問的資源被解鎖,則等待資源的線程會(huì)被喚醒。
  • 互斥鎖范圍,應(yīng)該盡量?。绘i定范圍越大,效率越差。
  • 能夠給任意NSObject對象加鎖。

加解鎖流程:sleep(加鎖) -> 出讓時(shí)間片 -> 線程休眠 -> 等待喚醒 -> running(解鎖)

時(shí)間?(quantum):系統(tǒng)給每個(gè)正在運(yùn)行的進(jìn)程或線程微觀上的一段CPU時(shí)間。

//  導(dǎo)入互斥鎖頭文件--C語言
#import <pthread.h>
// 可添加成員變量
 pthread_mutex_t mutex;

- (void)myfun
{
    pthread_mutex_init(&mutex, NULL);
    
}
- (void)MyLockingFunction
{
    pthread_mutex_lock(&mutex);
    // Do something.
    pthread_mutex_unlock(&mutex);
}
- (void)dealloc
{  
     // 不用要釋放掉
    pthread_mutex_destroy(&mutex);
}
// 這只是簡單使用,具體還需針對進(jìn)行錯(cuò)誤代碼處理
互斥鎖 vs 自旋鎖

相同:都能保證同一時(shí)間只有一個(gè)線程訪問共享資源。都能保證線程安全。
不同:

  • 互斥鎖:如果共享數(shù)據(jù)已經(jīng)有其他線程加鎖了,線程會(huì)進(jìn)入休眠狀態(tài)等待鎖。一旦被訪問的資源被解鎖,則等待資源的線程會(huì)被喚醒。
  • 自旋鎖:如果共享數(shù)據(jù)已經(jīng)有其他線程加鎖了,線程會(huì)以忙等的方式等待鎖,一旦被訪問的資源被解鎖,則等待資源的線程會(huì)立即執(zhí)行。

NSLock(互斥鎖)

NSLock是對底層pthread_mutex的封裝。一般使用有:

self.lock = [[NSLock alloc] init];
[self.lock tryLock]; // 嘗試加鎖;返回YES or NO
[self.lock lock]; // 加鎖
[self.lock unlock]; // 解鎖

底層原理

NSLock - Swift Foundation

NSLockFoundation下的,閉源則源碼不可見。借助Swift的Foundation可以看看同集成NSLockingNSLock在底層做了什么操作。

    1. 調(diào)用必須初始化;而底層則直接調(diào)用了互斥鎖pthread_mutex_init.(可以知道性能相近的原因了)
    1. 底層實(shí)現(xiàn)也是調(diào)用了pthread_mutexlockunlock.即就是對pthread_mutex的封裝。

在Apple官方文檔中指出

Warning
The NSLock class uses POSIX threads to implement its locking behavior. When sending an
unlock message to an NSLock object, you must be sure that message is sent from the
same thread that sent the initial lock message. Unlocking a lock from a different thread can result in undefined behavior.
Tra:本NSLock類使用POSIX線程執(zhí)行其鎖定行為。向NSLock對象發(fā)送解鎖消息時(shí),必須確保該消
息是從發(fā)送初始鎖定消息的同一線程發(fā)送的。從其他線程解鎖鎖可能導(dǎo)致未定義的行為。

所有它僅限用于同一線程中,且也不應(yīng)使用此類來實(shí)現(xiàn)遞歸鎖。lock在同一線程上兩次調(diào)用該方法將永久鎖定您的線程。原因是加鎖還未解鎖又再一次加鎖,一直在加鎖就會(huì)陷入死鎖狀態(tài)。如下:

NSLock *lock = [[NSLock alloc] init];
for (int i= 0; i<50; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);
        testMethod = ^(int value){
            [lock lock];
            if (value > 0) {
              NSLog(@"current value = %d",value);
              testMethod(value - 1);
            }
        };
        testMethod(10);
        [lock unlock];
    });
}  

可以使用NSRecursiveLock來實(shí)現(xiàn)遞歸鎖,也可使用@synchronized替代處理。

@synchronized

@synchronized是開發(fā)中用的非常廣泛的一種鎖,目的就是防止不同的線程同時(shí)執(zhí)行同一段代碼。但是就其性能而言,可謂慘不忍睹。常言存在即合理,廣泛使用,面試中常常被提及,就需要探索一下其底層原理。

首先確定下研究方法:1. 匯編;2. Clang。

有這樣一個(gè)例子,「onePx」奶茶店生意很好,奶茶就剩20杯的量,三個(gè)窗口賣(類似三條條線程),這還有越賣越多情況,就不符合逾期了。這就多線程對同一資源訪問,發(fā)生了數(shù)據(jù)錯(cuò)亂。

多個(gè)窗口賣奶茶

當(dāng)然了,主題是鎖,就通過加鎖即可解決。譬如加一個(gè)@synchronized,就完美控制。
image.png

匯編

@synchronized打下一個(gè)端點(diǎn),打開匯編,Xcode菜單欄,Debug -> Debug Workflow -> Always Show Disassembly

匯編斷點(diǎn)-無鎖

匯編斷點(diǎn)-加鎖

通過匯編可以窺見一二,在加鎖和無鎖的情況下有很大區(qū)別,執(zhí)行流程變得更加復(fù)雜,且多出兩個(gè)關(guān)鍵方法:objc_sync_enter、objc_sync_exit,這就是@synchronized的進(jìn)出口方法。

Clang

在main.m中編寫一個(gè)@synchronized方法

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        @synchronized (appDelegateClassName) {
        }
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

然后對其Clang指令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp編譯出已C++文件,查看底層

main.cpp的main部分底層

由圖中可驗(yàn)證匯編形式下,@synchronized的底層會(huì)調(diào)用objc_sync_enter、objc_sync_exit。如果捕獲到異常,就把異常拋出。

objc_sync_enter

在之前的工程中下一個(gè)方法為objc_sync_enter的符號(hào)斷點(diǎn),我們可以知道底層源碼是在libobjc.A.dylib中。也可以在@synchronized處下斷點(diǎn),進(jìn)入?yún)R編然后調(diào)試到objc_sync_enter,點(diǎn)擊底部調(diào)試菜單欄step into進(jìn)入objc_sync_enter的深一層匯編,也可以知道其歸屬于libobjc.A.dylib范疇。

libobjc.A.dylib -> objc_sync_enter

其實(shí)從這里就可以知道它在底層大致做了什么,做一個(gè)等值判斷,根據(jù)判斷結(jié)果,它會(huì)調(diào)用id2Data的一個(gè)方法,然后再會(huì)調(diào)用一個(gè)os_unfair_recursive_lock_lock_with_options;否則就跳轉(zhuǎn)調(diào)試超父類,調(diào)用一個(gè)方法objc_sync_nil。嚴(yán)謹(jǐn)一點(diǎn)還是要走底層代碼摸索一波。

打開一份前面文章分析用過的objc源碼,我們可以查找到對應(yīng)的源碼

// Begin synchronizing on 'obj'.  開始同步
// Allocates recursive mutex associated with 'obj' if needed.
// 如果需要,分配與“ obj”關(guān)聯(lián)的遞歸互斥體。 這里可以知道,它是一把遞歸互斥鎖,具體看底層。
// Returns OBJC_SYNC_SUCCESS once lock is acquired. 
// 成功時(shí)返回一個(gè) OBJC_SYNC_SUCCESS
int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    // 先判空
    if (obj) { 
        // id2data -> 關(guān)鍵方法。obj 不為空,從id2Data 獲取一個(gè)SyncData類型數(shù)據(jù),然后加鎖。
        SyncData* data = id2data(obj, ACQUIRE); 
        ASSERT(data);
        // 加鎖
        /**
         mutex 類型為 recursive_mutex_t;
        */
        data->mutex.lock();
    } else {
        // @synchronized(nil) does nothing 如果加鎖傳入的obj為空,什么也不做
        // 如果obj 為空,報(bào)以奔潰  objc_sync_nil()
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        } 
        objc_sync_nil();
    }
    return result;
}
  • SyncData結(jié)構(gòu) + SyncList結(jié)構(gòu)
typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData; // SyncData -> SyncData ,不斷的鏈接。這證明是一個(gè)鏈表結(jié)構(gòu)
    DisguisedPtr<objc_object> object; //通過運(yùn)算使指針隱藏于系統(tǒng)工具,同時(shí)保持指針的能力,其作用是通過計(jì)算把保存的 T 的指針隱藏起來,實(shí)現(xiàn)指針到整數(shù)的映射。
    int32_t threadCount;  // number of THREADS using this block。該代碼塊的線程數(shù)
    recursive_mutex_t mutex; // 證明其底部就是遞歸互斥
} SyncData;

struct SyncList {
    SyncData *data;
    spinlock_t lock;

    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
// Use multiple parallel lists to decrease contention among unrelated objects.使用多個(gè)并行列表以減少不相關(guān)對象之間的爭用。
// 哈希出對象的數(shù)組下標(biāo),然后取出數(shù)組對應(yīng)元素的 lock 或 data
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;

SyncData做為一個(gè)一個(gè)的節(jié)點(diǎn),依次存儲(chǔ),每個(gè) SyncList 結(jié)構(gòu)體都有個(gè)指向 SyncData 節(jié)點(diǎn)鏈表頭部的指針,也有一個(gè)用于防止多個(gè)線程對此列表做并發(fā)修改的鎖。類似SyncData -> SyncData -> SyncData ...,它是鏈表形式存儲(chǔ),不同的并行列表間互相不對數(shù)據(jù)進(jìn)行訪問。
鏈表的底層是通過哈希算法來存儲(chǔ)的,即StripedMap底層就是是將對象指針在內(nèi)存的地址轉(zhuǎn)化為無符號(hào)整型,通過算法((addr >> 4) ^ (addr >> 9)) % StripeCount, 來獲取下標(biāo)indexForPointer。

StripedMap

SyncList示意圖

SyncData單向存儲(chǔ)鏈表
  • obj == nil時(shí)啥也不做


    obj = nil --> do nothing

無論是objc_sync_enter還是objc_sync_exit 都調(diào)用了一個(gè)關(guān)鍵方法id2Data(),則我們的側(cè)重點(diǎn)就是在里面的實(shí)現(xiàn)。

id2Data

宏觀查看id2Data
  • 第一個(gè)情況 : SUPPORT_DIRECT_THREAD_KEYS = 1;即為快速緩存查找
    線程緩存池中快速查找
  1. 在線程緩存池中進(jìn)行快速查找對象,獲取鎖的數(shù)量,再做一些異常判斷;
  2. 判斷why,如果類型為ACQUIRE,則使得lockCount 加1,再存儲(chǔ)下來;
    如果類型為RELEASE,則使得lockCount 減1,再存儲(chǔ)下來;
    如果類型為CHECK,啥也不做。
  • 第二個(gè)情況:SyncCache緩存查找
    SyncCache緩存查找

SyncCache結(jié)構(gòu),與tls線程緩存相似,它也是鏈?zhǔn)酱鎯?chǔ)。

typedef struct {
    SyncData *data;
    unsigned int lockCount;  // number of times THIS THREAD locked this block 線程加鎖次數(shù)
} SyncCacheItem;

typedef struct SyncCache {
    unsigned int allocated;
    unsigned int used;
    SyncCacheItem list[0]; 
} SyncCache;

有所不同的是,它如果有多條線程時(shí),就會(huì)有多個(gè)這樣的list。


SyncCacheItem存儲(chǔ)SyncData

這個(gè)情況下就是在SyncCache的緩存列表里,不同線程間,查找是否有匹配對象,如果找到匹配對應(yīng)的類型,進(jìn)行lockCount加和減操作。

  • 第一次進(jìn)來
    首次進(jìn)入鎖模塊

    先在線程池中遍歷獲取p->nextData,通過 p =*listp取出第一個(gè)SyncData數(shù)據(jù), 判斷p->object 是否為傳入object, 如果相等,則存儲(chǔ)下object的持有者SyncDatap;跳轉(zhuǎn)到done,通過判斷fastCacheOccupied, YES時(shí)存儲(chǔ)到快速線程緩存中;否則存儲(chǔ)到線程緩存(SyncCache)中。

【總結(jié)】

  • 首次進(jìn)來: 沒有鎖,threadCount = 1,lockCount = 1, 存到tls_set_direct中;
  • 不是第一次進(jìn)來,是在tls鏈表進(jìn)行快速緩存查找的,它們是在同一個(gè)線程進(jìn)行lockCount加,并且將result存到tls_set_direct;
  • 不是第一次但是在SyncCache中查找的,則可能在一個(gè)線程或多個(gè)線程中遍歷SyncCacheItem類型的list單向鏈表,查找SyncData,查找到也會(huì)對lockCount加操作。

objc_sync_exit

// End synchronizing on 'obj'. 
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) {
        SyncData* data = id2data(obj, RELEASE); 
        if (!data) {
            result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        } else {
            bool okay = data->mutex.tryUnlock();
            if (!okay) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            }
        }
    } else {
        // @synchronized(nil) does nothing
    }
    return result;
}

這解鎖就是反向操作了,這可不是像某些人反向吸Y那樣子。??
它要判斷obj,然后在id2Data里面做release操作,使得lockCount減少;
如果拿到的非空data閑嘗試解鎖,解鎖成功的即返回result;解鎖失敗的,及時(shí)報(bào)錯(cuò)。

【@synchnized 坑點(diǎn)】
坑點(diǎn)1:經(jīng)過以上的分析,@synchnized慢的原因就非常清楚了,它的加鎖解鎖都經(jīng)過了一些列的增刪改查再加緩存,鏈表接口的存取都會(huì)影響速度。
坑點(diǎn)2:也是一個(gè)面試題

- (void)testSynchronized {
    for (int i = 0; i < 10000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
                self.dataSources = [NSMutableArray array];
        });
    }
}

執(zhí)行之后會(huì)卡死。可以通過開啟

打開Zombie Objects(僵尸對象)

再次運(yùn)行就可以看到奔潰。原因是在反復(fù)初始化,調(diào)用setter retain 新值,釋放舊值。線程不斷的release舊值,導(dǎo)致了野指針。
嘗試鎖一下

- (void)testSynchronized {
    for (int i = 0; i < 10000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
             @synchronized (self.dataSources) { 
                self.dataSources = [NSMutableArray array];
            }
        });
    }
}

但是會(huì)出現(xiàn)同樣的奔潰,原因是:self.dataSources的生命周期,它是會(huì)被釋放的,釋放后為nil,在前面分析中就可以知道,如果加鎖對象為nil,則在「鎖」內(nèi)就do nothing

所以@synchnized一般鎖self,保證鎖住的objc的生命周期未結(jié)束。但亦不能一直鎖self,在底層objc_sync_enter的時(shí)候,self的鏈表會(huì)很多,就會(huì)導(dǎo)致鏈表查詢很繁瑣,性能降低更加明顯。

補(bǔ)充:atomic & nonatomic

atomic

  • atomic 原?屬性(線程安全),針對多線程設(shè)計(jì)的,需要消耗?量的資源
  • atomic 本身就有?把鎖(?旋鎖)
  • 保證同?時(shí)間只有?個(gè)線程能夠?qū)?,但是同?個(gè)時(shí)間多個(gè)線程都可以取值。(單寫多讀:單個(gè)線程寫?,多個(gè)線程可以讀?。?/li>

nonatomic

  • nonatomic ?原?屬性
  • nonatomic:?線程安全,適合內(nèi)存?的移動(dòng)設(shè)備。

屬性應(yīng)都聲明為 nonatomic ;
盡量避免多線程搶奪同?塊資源;
盡量將加鎖、資源搶奪的業(yè)務(wù)邏輯交給服務(wù)器端處理,減?移動(dòng)客戶端的壓?。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容