iOS 底層原理 - 鎖分析

了解鎖的機制會有助于項目開發(fā),從而避免項目中多個線程訪問同一塊資源引發(fā)數(shù)據(jù)混亂的問題。

一 概念 鎖的歸類

基本的鎖就包括了三類 自旋鎖 互斥鎖 讀寫鎖,其他的比如條件鎖,遞歸鎖,信號量都是上層的封裝和實現(xiàn)。

自旋鎖

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

互斥鎖

互斥鎖:是一種用于多線程編程中,防止兩條線程同時對同一公共資源(比 如全局變量)進行讀寫的機制。該目的通過將代碼切片成一個一個的臨界區(qū) 而達成
這里屬于互斥鎖的有:

NSLock
pthread_mutex
@synchronized

條件鎖

條件鎖:就是條件變量,當進程的某些資源要求不滿足時就進入休眠,也就
是鎖住了。當資源被分配到了,條件鎖打開,進程繼續(xù)運行
NSCondition
NSConditionLock

遞歸鎖

遞歸鎖:就是同一個線程可以加鎖N次而不會引發(fā)死鎖
NSRecursiveLock
pthread_mutex(recursive)

信號量

信號量(semaphore):是一種更高級的同步機制,互斥鎖可以說是
semaphore在僅取值0/1時的特例。信號量可以有更多的取值空間,用來實
現(xiàn)更加復雜的同步,而不單單是線程間互斥。
dispatch_semaphore

讀寫鎖

讀寫鎖實際是一種特殊的自旋鎖,它把對共享資源的訪問者劃分成讀者和寫者,讀者只對共享資源進行讀訪問,寫者則需要對共享資源進行寫操作。這種鎖相對于自旋鎖而言,能提高并發(fā)性,因為在多處理器系統(tǒng)中,它允許同時有多個讀者來訪問共享資源,最大可能的讀者數(shù)為實際的邏輯CPU 數(shù)。寫者是排他性的,一個讀寫鎖同時只能有一個寫者或多個讀者(與CPU數(shù)相關),但不能同時既有讀者又有寫者。在讀寫鎖保持期間也是搶占失效的。
如果讀寫鎖當前沒有讀者,也沒有寫者,那么寫者可以立刻獲得讀寫鎖,否則它必須自旋在那里,直到?jīng)]有任何寫者或讀者。如果讀寫鎖沒有寫者,那么讀者可以立即獲得該讀寫鎖,否則讀者必須
自旋在那里,直到寫者釋放該讀寫鎖。一次只有一個線程可以占有寫模式的讀寫鎖, 但是可以有多個線程同時占有讀模式的讀寫鎖. 正
是因為這個特性,當讀寫鎖是寫加鎖狀態(tài)時, 在這個鎖被解鎖之前, 所有試圖對這個鎖加鎖的線程都會被阻塞.當讀寫鎖在讀加鎖狀態(tài)時, 所有試圖以讀模式對它進行加鎖的線程都可以得到訪問權, 但是如果
線程希望以寫模式對此鎖進行加鎖, 它必須直到所有的線程釋放鎖.
通常, 當讀寫鎖處于讀模式鎖住狀態(tài)時, 如果有另外線程試圖以寫模式加鎖, 讀寫鎖通常會阻塞隨后的讀模式鎖請求, 這樣可以避免讀模式鎖?期占用, 而等待的寫模式鎖請求?期阻塞.讀寫鎖適合于對數(shù)據(jù)結構的讀次數(shù)比寫次數(shù)多得多的情況. 因為, 讀模式鎖定時可以共享, 以寫
模式鎖住時意味著獨占, 所以讀寫鎖又叫共享-獨占鎖.讀寫鎖適合于對數(shù)據(jù)結構的讀次數(shù)比寫次數(shù)多得多的情況. 因為, 讀模式鎖定時可以共享, 以寫模式鎖住時意味著獨占, 所以讀寫鎖又叫共享-獨占鎖.

鎖的性能數(shù)據(jù)

屏幕快照 2020-11-02 下午5.34.41.png

二 @synchronized

@synchronized 是一種遞歸互斥鎖。
應用如下

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.ticketCount = 20;
    [self lg_testSaleTicket];
}


- (void)lg_testSaleTicket{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 3; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 10; i++) {
            [self saleTicket];
        }
    });
}

- (void)saleTicket{
    // 枷鎖 - 線程安全
    @synchronized (self) {
        if (self.ticketCount > 0) {
            self.ticketCount--;
            sleep(0.1);
            NSLog(@"當前余票還剩:%ld張",self.ticketCount);
        }else{
            NSLog(@"當前車票已售罄");
        }
    }

}

下面我們來分析下它的底層實現(xiàn),通過在@synchronized (self) 打斷點,顯示匯編,可以看到底層調(diào)用了objc_sync_enter 和 objc_sync_exit方法,打符號斷點看到方法是在libobjc.dylib庫里面,也就是objc的源碼,我們打開源碼繼續(xù)分析。


屏幕快照 2020-11-02 下午5.43.07.png

搜索objc_sync_enter方法,具體實現(xiàn)如下:

// Begin synchronizing on 'obj'. 
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.  
int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        SyncData* data = id2data(obj, ACQUIRE);
        assert(data);
        data->mutex.lock();
    } else {
        // @synchronized(nil) does nothing
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

    return result;
}

這里可以發(fā)現(xiàn)如果對象不存在就會走方法objc_sync_nil(),這里底層沒有實現(xiàn),也就是什么事情都不做,所以有時候會出現(xiàn)加鎖了但是沒用,因為對象被釋放了,這里需要特別注意。

BREAKPOINT_FUNCTION(
    void objc_sync_nil(void)
);

這里我們看下SyncData的結構,父類里面有子類,是一個指針結點,發(fā)現(xiàn)是一個鏈表結構

typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;
} SyncData;

遞歸鎖與objc_sync_nil搭配的使用,可以防止死鎖現(xiàn)象發(fā)生。

繼續(xù)往下看方法id2data

static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;
    
    // @syn - vc model view 任何地方都可以直接使用 - 全局
    // data 存儲節(jié)點
    // map - obj - 表
    
#if SUPPORT_DIRECT_THREAD_KEYS
    // Check per-thread single-entry fast cache for matching object
    // 檢查每線程單項快速緩存中是否有匹配的對象
    bool fastCacheOccupied = NO;
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {
        fastCacheOccupied = YES;

        if (data->object == object) {
            // Found a match in fast cache.
            uintptr_t lockCount;

            result = data;
            lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
            if (result->threadCount <= 0  ||  lockCount <= 0) {
                _objc_fatal("id2data fastcache is buggy");
            }

            switch(why) {
            case ACQUIRE: {
                lockCount++;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                break;
            }
            case RELEASE:
                lockCount--;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                if (lockCount == 0) {
                    // remove from fast cache
                    tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }
#endif

    // Check per-thread cache of already-owned locks for matching object
    // 檢查已擁有鎖的每個線程高速緩存中是否有匹配的對象
    SyncCache *cache = fetch_cache(NO);
    if (cache) {
        unsigned int I;
        for (i = 0; i < cache->used; i++) {
            SyncCacheItem *item = &cache->list[I];
            if (item->data->object != object) continue;

            // Found a match.
            result = item->data;
            if (result->threadCount <= 0  ||  item->lockCount <= 0) {
                _objc_fatal("id2data cache is buggy");
            }
                
            switch(why) {
            case ACQUIRE:
                item->lockCount++;
                break;
            case RELEASE:
                item->lockCount--;
                if (item->lockCount == 0) {
                    // remove from per-thread cache
                    cache->list[i] = cache->list[--cache->used];
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }

    // Thread cache didn't find anything.
    // Walk in-use list looking for matching object
    // Spinlock prevents multiple threads from creating multiple 
    // locks for the same new object.
    // We could keep the nodes in some hash table if we find that there are
    // more than 20 or so distinct locks active, but we don't do that now.
    
    lockp->lock();

    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        for (p = *listp; p != NULL; p = p->nextData) {
            if ( p->object == object ) {
                result = p;
                // atomic because may collide with concurrent RELEASE
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // no SyncData currently associated with object
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
    
        // an unused one was found, use it
        if ( firstUnused != NULL ) {
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            goto done;
        }
    }

    // Allocate a new SyncData and add to list.
    // XXX allocating memory with a global lock held is bad practice,
    // might be worth releasing the lock, allocating, and searching again.
    // But since we never free these guys we won't be stuck in allocation very often.
    posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
    result->object = (objc_object *)object;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    result->nextData = *listp;
    *listp = result;
    
 done:
    lockp->unlock();
    if (result) {
        // Only new ACQUIRE should get here.
        // All RELEASE and CHECK and recursive ACQUIRE are 
        // handled by the per-thread caches above.
        if (why == RELEASE) {
            // Probably some thread is incorrectly exiting 
            // while the object is held by another thread.
            return nil;
        }
        if (why != ACQUIRE) _objc_fatal("id2data is buggy");
        if (result->object != object) _objc_fatal("id2data is buggy");

#if SUPPORT_DIRECT_THREAD_KEYS
        if (!fastCacheOccupied) {
            // Save in fast thread cache
            tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
        } else 
#endif
        {
            // Save in thread cache
            if (!cache) cache = fetch_cache(YES);
            cache->list[cache->used].data = result;
            cache->list[cache->used].lockCount = 1;
            cache->used++;
        }
    }

    return result;
}

看第一句代碼里LOCK_FOR_OBJ,是一個宏,sDataLists的真實類型是SyncList,所以我們猜測StripedMap應該有個全局的哈希表來通過obj得到一個index,然后存儲SyncList,而SyncList存儲的都是一個個的節(jié)點SyncData。

#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data

static StripedMap<SyncList> sDataLists;
struct SyncList {
    SyncData *data;
    spinlock_t lock;

    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
屏幕快照 2020-11-02 下午2.48.54.png

總結:@synchronized使用簡單,但是性能不高,因為底層是對哈希表進行增刪改查,有漏洞。

@synchronized的注意點

我們先看一段代碼

for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            _testArray = [NSMutableArray array];
        });
    }

這里不斷創(chuàng)建線程創(chuàng)建訪問對象,會造成可變數(shù)組的不安全性,運行代碼,crash掉,如下所示:


屏幕快照 2020-11-02 下午3.14.18.png

創(chuàng)建的過程是產(chǎn)生一個new value,old value release掉的過程,會有一瞬間_testArray為nil,我們可以通過加鎖解決,這里如果使用@synchronized的話,不會產(chǎn)生作用,因為當對象為空時,鎖不起作用。

for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            @synchronized (_testArray) {
                _testArray = [NSMutableArray array];
            }
            
        });
    }

這時候可以使用NSLock來解決。

三 NSLock

NSLock是「非」遞歸互斥鎖,具體使用如下:

NSLock *lock = [[NSLock alloc] init];
    for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [lock lock];
            // old value ->
            _testArray = [NSMutableArray array];
            [lock unlock];

        });
    }

點進去發(fā)現(xiàn)NSLock在Foundation庫里面

屏幕快照 2020-11-02 下午3.27.00.png

NSLock 是一種互斥鎖,也是非遞歸的互斥鎖,所以在遞歸時,會造成線程的堵塞。

這里我們看一個例子,這里遞歸調(diào)用,使用互斥鎖NSLock,運行發(fā)現(xiàn)結果只打印10,產(chǎn)生了堵塞,所以遞歸調(diào)用這里不能使用互斥鎖,應該使用遞歸鎖NSRecursiveLock

NSLock *lock = [[NSLock alloc] init];

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);
                }
                [lock unlock];
                
            };
            testMethod(10);
        });

運行結果

2020-11-02 15:34:40.854430+0800 002-@synchronized注意點[3938:109845] current value = 10

四 NSRecursiveLock

修改上面例子,代碼如下:

NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];

    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);
            }
            [lock unlock];
            
        };
        testMethod(10);
    });

運行結果正常


屏幕快照 2020-11-02 下午3.39.33.png

如果我們給上面的代碼加一個for循環(huán),就會造成一個死鎖現(xiàn)象

NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];

    for (int i= 0; i<100; 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);
                }
                [lock unlock];
                
            };
            testMethod(10);
        });
    }
屏幕快照 2020-11-02 下午4.02.47.png

這里改為@synchronized就能解決死鎖,因為 @synchronized 鎖的是同一個對象,當下次再來的時候只會從 cache 里面取,而不會像 NSRecursiveLock 當線程 1 鎖了一次之后,線程 2 來了還會再鎖一次,所以造成了線程 1 等線程 2 解鎖,線程 2 等線程 1 解鎖的死鎖狀況。

for (int i= 0; i<100; i++) {
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
           
            static void (^testMethod)(int);
            
            testMethod = ^(int value){
                @synchronized (self) {
                    if (value > 0) {
                        NSLog(@"current value = %d",value);
                        testMethod(value - 1);
                    }
                }
               
                
            };
            testMethod(10);
        });
    }

總結

普通線程安全使用NSLock
遞歸調(diào)用線程安全使用NSRecursiveLock
產(chǎn)生循環(huán),受外界線程影響的時候要特別注意死鎖,就算損失一點點性能,也要保證安全

四 NSCondition

NSCondition 的對象實際上作為一個鎖和一個線程檢查器:鎖主要 為了當檢測條件時保護數(shù)據(jù)源,執(zhí)行條件引發(fā)的任務;線程檢查器 主要是根據(jù)條件決定是否繼續(xù)運行線程,即線程是否被阻塞。

1:[condition lock];//一般用于多線程同時訪問、修改同一個數(shù)據(jù)源,保證在同一 時間內(nèi)數(shù)據(jù)源只被訪問、修改一次,其他線程的命令需要在lock 外等待,只到 unlock ,才可訪問
2:[condition unlock];//與lock 同時使用
3:[condition wait];//讓當前線程處于等待狀態(tài)
4:[condition signal];//CPU發(fā)信號告訴線程不用在等待,可以繼續(xù)執(zhí)行

NSCondition 使用場景更適用于生產(chǎn)消費者模式,當你生產(chǎn)大于 0 的時候,我可以進行消費,如果一直等于 0 的時候,我就一直等你生產(chǎn),例子如下:

@interface ViewController ()
@property (nonatomic, assign) NSUInteger ticketCount;
@property (nonatomic, strong) NSCondition *testCondition;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.ticketCount = 0;
    [self lg_testConditonLock];
}



#pragma mark -- NSCondition

- (void)lg_testConditon{
    
    _testCondition = [[NSCondition alloc] init];
    //創(chuàng)建生產(chǎn)-消費者
    for (int i = 0; i < 50; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_producer];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_consumer];
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_consumer];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_producer];
        });
        
    }
}

- (void)lg_producer{
    [_testCondition lock];
    self.ticketCount = self.ticketCount + 1;
    NSLog(@"生產(chǎn)一個 現(xiàn)有 count %zd",self.ticketCount);
    [_testCondition signal];
    [_testCondition unlock];

}

- (void)lg_consumer{
    
    // 線程安全
    [_testCondition lock];

    while (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
        // 保證正常流程
        [_testCondition wait];
    }
    
    //注意消費行為,要在等待條件判斷之后
    self.ticketCount -= 1;
    NSLog(@"消費一個 還剩 count %zd ",self.ticketCount);
    [_testCondition unlock];
}



五 NSConditionLock

NSConditionLock 是對 NSCondition 的進一步封裝,可以設置具體的條件值對象。NSConditionLock 可以確保只有在滿足特定條件時,線程才能獲得鎖。
1.1 NSConditionLock 是鎖,一旦一個線程獲得鎖,其他線程一定等待
1.2 [xxxx lock]; 表示 xxx 期待獲得鎖,如果沒有其他線程獲得鎖(不需要判斷內(nèi)部的 condition) 那它能執(zhí)行此行以下代碼,如果已經(jīng)有其他線程獲得鎖(可能是條件鎖,或者無條件 鎖),則等待,直至其他線程解鎖
1.3 [xxx lockWhenCondition:A條件]; 表示如果沒有其他線程獲得該鎖,但是該鎖內(nèi)部的 condition不等于A條件,它依然不能獲得鎖,仍然等待。如果內(nèi)部的condition等于A條件,并 且沒有其他線程獲得該鎖,則進入代碼區(qū),同時設置它獲得該鎖,其他任何線程都將等待它代碼 的完成,直至它解鎖。
1.4 [xxx unlockWithCondition:A條件]; 表示釋放鎖,同時把內(nèi)部的condition設置為A條件
1.5 return = [xxx lockWhenCondition:A條件 beforeDate:A時間]; 表示如果被鎖定(沒獲得 鎖),并超過該時間則不再阻塞線程。但是注意:返回的值是NO,它沒有改變鎖的狀態(tài),這個函 數(shù)的目的在于可以實現(xiàn)兩種狀態(tài)下的處理
1.6 所謂的condition就是整數(shù),內(nèi)部通過整數(shù)比較條件

具體使用如下:

#pragma mark -- NSConditionLock
- (void)lg_testConditonLock{
    // 信號量
    NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
       [conditionLock lockWhenCondition:1];
       NSLog(@"線程 1");
       [conditionLock unlockWithCondition:0];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
       
       [conditionLock lockWhenCondition:2];
       
       NSLog(@"線程 2");
       
       [conditionLock unlockWithCondition:1];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       
       [conditionLock lock];
       NSLog(@"線程 3");
       [conditionLock unlock];
    });
}

打印結果是321。

線程 1 調(diào)用[NSConditionLock lockWhenCondition:],此時此刻因為不滿足當前條件,所
以會進入 waiting 狀態(tài),當前進入到 waiting 時,會釋放當前的互斥鎖。
此時當前的線程 3 調(diào)用[NSConditionLock lock:],本質(zhì)上是調(diào)用 [NSConditionLock
lockBeforeDate:],這里不需要比對條件值,所以線程 3 會打印
接下來線程 2 執(zhí)行[NSConditionLock lockWhenCondition:],因為滿足條件值,所以線程
2 會打印,打印完成后會調(diào)用[NSConditionLock unlockWithCondition:],這個時候講
value 設置為 1,并發(fā)送 boradcast, 此時線程 1 接收到當前的信號,喚醒執(zhí)行并打印。
自此當前打印為 線程 3->線程 2 -> 線程 1。
[NSConditionLock lockWhenCondition:]:這里會根據(jù)傳入的 condition 值和 Value 值進
行對比,如果不相等,這里就會阻塞,進入線程池,否則的話就繼續(xù)代碼執(zhí)行
[NSConditionLock unlockWithCondition:]: 這里會先更改當前的 value 值,然后進行廣
播,喚醒當前的線程。

六 atomic

atomic原理

所有為屬性賦值的方法在底層調(diào)用的是reallySetProperty方法,在源碼里全局搜索,可以看到原子性和非原子性調(diào)用方法的區(qū)別

void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, true, false, false);
}

void objc_setProperty_nonatomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, false, false, false);
}

主要區(qū)別是第五個參數(shù)

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
          //自旋鎖
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

取值的時候在底層會調(diào)用objc_getProperty方法,全局搜索:

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    if (offset == 0) {
        return object_getClass(self);
    }

    // Retain release world
    id *slot = (id*) ((char*)self + offset);
    if (!atomic) return *slot;
        
    // Atomic retain release world
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
    
    // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
    return objc_autoreleaseReturnValue(value);
}

所以atomic的本質(zhì)是對setter和getter(reallySetProperty,objc_getProperty)方法分別加了一把鎖,生成了原子性的get,set方法。

注意點

atomic修飾屬性加的鎖,僅僅是在setter和getter內(nèi)部加的鎖,保證取值/賦值時的線程安全性,但是如果像下面這樣使用并不能保證線程安全。
例子一

@property (atomic, strong) NSArray *array;

- (void)lg_test_atomic2{
    //Thread A
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 100000; i ++) {
            if (i % 2 == 0) {
                self.array = @[@"Hank", @"CC", @"Cooci"];
            }
            else {
                self.array = @[@"Kody"];
            }
            NSLog(@"Thread A: %@\n", self.array);
        }
    });
    
    //Thread B
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 100000; i ++) {
            if (self.array.count >= 2) {
                NSString* str = [self.array objectAtIndex:1];
            }
            NSLog(@"Thread B: %@\n",self.array);
        }
    });
}

運行代碼崩潰

看第二個例子:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 10000; i ++) {
            self.num = self.num + 1;
            NSLog(@"Thread A: %@--%ld", NSThread.currentThread,(long)self.num);
        }
    });
    
    //Thread B
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 10000; i ++) {
            self.num = self.num + 1;
            NSLog(@"Thread A: %@--%ld", NSThread.currentThread,(long)self.num);
        }
    });

理想結果應該是20000,運行代碼,結果為19833,證明在某個時刻兩個線程訪問了一個數(shù)據(jù),重復賦值,self.num的setter方法和getter方法是線程安全的,但是self.num + 1這句代碼不是線程安全的,不受保護。
總而言之,atomic只能保證代碼進入getter或者setter函數(shù)內(nèi)部時是安全的,一旦出了getter和setter,多線程安全只能靠程序員自己保障了。所以atomic屬性和使用property的多線程安全并沒什么直接的聯(lián)系。
平時不建議使用atomic,會消耗更多的資源,性能會很低,要比nonatomic慢20倍。

七 自定義讀寫鎖

我們可以創(chuàng)建一個并發(fā)隊列,對于讀操作,我們可以用同步函數(shù)進行處理,對于寫操作我們可以用異步柵欄函數(shù)這操作,這樣就簡單實現(xiàn)了一個讀寫鎖

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LGRWLock : NSObject
// 讀數(shù)據(jù)
- (id)lg_objectForKey:(NSString *)key;
// 寫數(shù)據(jù)
- (void)lg_setObject:(id)obj forKey:(NSString *)key;
@end

@interface LGRWLock ()
// 定義一個并發(fā)隊列:
@property (nonatomic, strong) dispatch_queue_t concurrent_queue;
// 用戶數(shù)據(jù)中心, 可能多個線程需要數(shù)據(jù)訪問:
@property (nonatomic, strong) NSMutableDictionary *dataCenterDic;
@end

@implementation LGRWLock

- (id)init{
    self = [super init];
    if (self){
        // 創(chuàng)建一個并發(fā)隊列:
        self.concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
        // 創(chuàng)建數(shù)據(jù)字典:
        self.dataCenterDic = [NSMutableDictionary dictionary];
    }
    return self;
}

#pragma mark - 讀數(shù)據(jù)
- (id)lg_objectForKey:(NSString *)key{
    __block id obj;
    // 同步讀取指定數(shù)據(jù):
    dispatch_sync(self.concurrent_queue, ^{
        obj = [self.dataCenterDic objectForKey:key];
    });
    return obj;
}

#pragma mark - 寫數(shù)據(jù)
- (void)lg_setObject:(id)obj forKey:(NSString *)key{
    // 異步柵欄調(diào)用設置數(shù)據(jù):
    dispatch_barrier_async(self.concurrent_queue, ^{
        [self.dataCenterDic setObject:obj forKey:key];
    });
}

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

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