iOS底層原理總結 - 多線程的鎖

目錄:
1.為什么要線程安全
2.多線程安全隱患分析
3.多線程安全隱患的解決方案
4.鎖的分類-13種鎖
4.1.1OSSpinLock
4.1.2os_unfair_lock
4.1.3pthread_mutex
4.1.4NSLock
4.1.5NSRecursiveLock
4.1.6NSCondition
4.1.7NSConditionLock
4.1.8dispatch_semaphore
4.1.9dispatch_queue
4.1.10@synchronized
4.1.11atomic
4.1.12pthread_rwlock
4.1.13dispatch_barrier_async
5.性能對比
6.常見的死鎖


1.為什么要線程安全

多個線程訪問同一塊資源的時候,很容易引發數據混亂問題。

2.多線程安全隱患分析

01.png

3.多線程安全隱患的解決方案

02.png

4.13種鎖

1 OSSpinLock

何謂自旋鎖?它是為實現保護共享資源而提出一種鎖機制。其實,自旋鎖與互斥鎖比較類似,它們都是為了解決對某項資源的互斥使用。無論是互斥鎖,還是自旋鎖,在任何時刻,最多只能有一個保持者,也就說,在任何時刻最多只能有一個執行單元獲得鎖。但是兩者在調度機制上略有不同。對于互斥鎖,如果資源已經被占用,資源申請者只能進入睡眠狀態。但是自旋鎖不會引起調用者睡眠,如果自旋鎖已經被別的執行單元保持,調用者就一直循環在那里看是否該自旋鎖的保持者已經釋放了鎖,"自旋"一詞就是因此而得名。

OSSpinLock叫做”自旋鎖”,使用時需要導入頭文件#import <libkern/OSAtomic.h>
使用方式:

//初始化
OSSpinLock lock = OS_SPINLOCK_INIT;
//加鎖
OSSpinLockLock(&lock);
//解鎖
OSSpinLockUnlock(&lock);

demo:

#import "OSSpinLockDemo.h"
#import <libkern/OSAtomic.h>

@interface OSSpinLockDemo()
@property (assign, nonatomic) OSSpinLock ticketLock;
@end

@implementation OSSpinLockDemo

- (instancetype)init {
    
    self = [super init];
    if (self) {
        self.ticketLock = OS_SPINLOCK_INIT;
    }
    return self;
}


//賣票
- (void)sellingTickets {
    OSSpinLockLock(&_ticketLock);
    
    [super sellingTickets];
    
    OSSpinLockUnlock(&_ticketLock);
}

@end

運行結果:

image.png

OSSpinLock在iOS10.0以后就被棄用了,可以使用os_unfair_lock_lock替代。而且還有一些安全性問題,具體參考不再安全的 OSSpinLock


2 os_unfair_lock

os_unfair_lock用于取代不安全的OSSpinLock,從iOS10開始才支持 從底層調用看,等待os_unfair_lock鎖的線程會處于休眠狀態,并非忙等 需要導入頭文件#import <os/lock.h>

使用方式:

//初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//加鎖
os_unfair_lock_lock(&lock);
//解鎖
os_unfair_lock_unlock(&lock);

demo:

#import "os_unfair_lockDemo.h"
#import <os/lock.h>
@interface os_unfair_lockDemo()
@property (assign, nonatomic) os_unfair_lock ticketLock;
@end

@implementation os_unfair_lockDemo
- (instancetype)init
{
self = [super init];
if (self) {
self.ticketLock = OS_UNFAIR_LOCK_INIT;
}
return self;
}

//賣票
- (void)sellingTickets{
os_unfair_lock_lock(&_ticketLock);

[super sellingTickets];

os_unfair_lock_unlock(&_ticketLock);
}
@end

3 pthread_mutex

pthread除了創建互斥鎖,還可以創建遞歸鎖、讀寫鎖、once等鎖。

  • 靜態初始化: pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
  • 動態初始化: pthread_mutex_init()函數是以動態方式創建互斥鎖的,參數 attr 指定了新建互斥鎖的屬性。如果參數 attr 為 NULL ,使用默認的屬性,返回0代表初始化成功。這種方式可以初始化普通鎖、遞歸鎖(同 NSRecursiveLock ), 初始化方式有些復雜。

此類初始化方法可設置鎖的類型,PTHREAD_MUTEX_ERRORCHECK 互斥鎖不會檢測死鎖, PTHREAD_MUTEX_ERRORCHECK 互斥鎖可提供錯誤檢查, PTHREAD_MUTEX_RECURSIVE 遞歸鎖, PTHREAD_PROCESS_DEFAULT 映射到 PTHREAD_PROCESS_NORMAL

使用方式:
mutex叫做”互斥鎖”,等待鎖的線程會處于休眠狀態。需要導入頭文件#import <pthread.h>使用步驟

  • 1、初始化鎖的屬性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);

/*
* Mutex type attributes
*/
#define PTHREAD_MUTEX_NORMAL        0
#define PTHREAD_MUTEX_ERRORCHECK    1
#define PTHREAD_MUTEX_RECURSIVE        2
#define PTHREAD_MUTEX_DEFAULT        PTHREAD_MUTEX_NORMAL
  • 2、初始化鎖
// 初始化鎖
pthread_mutex_init(mutex, &attr);
  • 3、初始化鎖結束以后,銷毀屬性
// 銷毀屬性
pthread_mutexattr_destroy(&attr);
  • 4、加鎖解鎖
pthread_mutex_lock(&_mutex);
pthread_mutex_unlock(&_mutex);
  • 5、銷毀鎖
pthread_mutex_destroy(&_mutex);

備注:我們可以不初始化屬性,在傳屬性的時候直接傳NULL,表示使用默認屬性
PTHREAD_MUTEX_NORMAL。pthread_mutex_init(mutex, NULL);

死鎖 我們稍微的修改一下代碼

//賣票
- (void)sellingTickets{
pthread_mutex_lock(&_ticketMutex);
[super sellingTickets];
[self sellingTickets2];
pthread_mutex_unlock(&_ticketMutex);
}

- (void)sellingTickets2{
pthread_mutex_lock(&_ticketMutex);
NSLog(@"%s",__func__);
pthread_mutex_unlock(&_ticketMutex);
}

上面的代碼就會造成線程死鎖,因為方法sellingTickets的結束需要sellingTickets2解鎖,方法sellingTickets2的結束需要sellingTickets解鎖,相互引用造成死鎖
但是pthread_mutex_t里面有一個屬性可以解決這個問題PTHREAD_MUTEX_RECURSIVE
PTHREAD_MUTEX_RECURSIVE遞歸鎖:允許同一個線程對同一把鎖進行重復加鎖。要考重點同一個線程和同一把鎖

- (instancetype)init
{
self = [super init];
if (self) {
// 初始化屬性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化鎖
pthread_mutex_init(&(_ticketMutex), &attr);
// 銷毀屬性
pthread_mutexattr_destroy(&attr);
}
return self;
}

對于上面的問題還有一個解決方案就是在方法sellingTickets2中重新在創建一把新的鎖,兩個方法的鎖對象不同,就不會造成線程死鎖了。

image.png

  • 條件
// 初始化屬性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化鎖
pthread_mutex_init(&_mutex, &attr);
// 銷毀屬性
pthread_mutexattr_destroy(&attr);

// 初始化條件
pthread_cond_t condition
pthread_cond_init(&_cond, NULL);

// 等待條件
pthread_cond_wait(&_cond, &_mutex);

//激活一個等待該條件的線程
pthread_cond_signal(&_cond);
//激活所有等待該條件的線程
pthread_cond_broadcast(&_cond);

//銷毀資源
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);

使用案例:假設我們有一個數組,里面有兩個線程,一個是添加數組,一個是刪除數組,我們先調用刪除數組,在調用添加數組,但是在數組為空的時候不調用刪除數組。

demo:

#import "pthread_mutexDemo1.h"
#import <pthread.h>

@interface pthread_mutexDemo1()
@property (assign, nonatomic) pthread_mutex_t mutex;
@property (assign, nonatomic) pthread_cond_t cond;
@property (strong, nonatomic) NSMutableArray *data;
@end

@implementation pthread_mutexDemo1

- (instancetype)init
{
if (self = [super init]) {
// 初始化屬性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化鎖
pthread_mutex_init(&_mutex, &attr);
// 銷毀屬性
pthread_mutexattr_destroy(&attr);

// 初始化條件
pthread_cond_init(&_cond, NULL);

self.data = [NSMutableArray array];
}
return self;
}
- (void)otherTest
{
[[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];

[[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
}

// 線程1
// 刪除數組中的元素
- (void)__remove
{
pthread_mutex_lock(&_mutex);
NSLog(@"__remove - begin");

if (self.data.count == 0) {
// 等待
pthread_cond_wait(&_cond, &_mutex);
}

[self.data removeLastObject];
NSLog(@"刪除了元素");

pthread_mutex_unlock(&_mutex);
}

// 線程2
// 往數組中添加元素
- (void)__add
{
pthread_mutex_lock(&_mutex);

sleep(1);

[self.data addObject:@"Test"];
NSLog(@"添加了元素");

// 激活一個等待該條件的線程
pthread_cond_signal(&_cond);

pthread_mutex_unlock(&_mutex);
}

- (void)dealloc
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}

為了準確測試我們可以在__add中sleep(1) //刪除操作再添加了元素之后才進行的。
示例結果:

image.png


4 NSLock
  • NSLock是對mutex普通鎖的封裝。pthread_mutex_init(mutex, NULL);
  • NSLock 遵循 NSLocking 協議。Lock 方法是加鎖,unlock 是解鎖,tryLock 是嘗試加鎖,如果失敗的話返回 NO,lockBeforeDate: 是在指定Date之前嘗試加鎖,如果在指定時間之前都不能加鎖,則返回NO

使用方式:

@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
@end

demo:

#import "LockDemo.h"
@interface LockDemo()
@property (strong, nonatomic) NSLock *ticketLock;
@end
@implementation LockDemo
//賣票
- (void)sellingTickets{
[self.ticketLock lock];
[super sellingTickets];
[self.ticketLock unlock];
}
@end

5 NSRecursiveLock(遞歸鎖)

NSRecursiveLock是對mutex遞歸鎖的封裝,API跟NSLock基本一致

demo:

#import "RecursiveLockDemo.h"
@interface RecursiveLockDemo()
@property (nonatomic,strong) NSRecursiveLock *ticketLock;
@end
@implementation RecursiveLockDemo
//賣票
- (void)sellingTickets{
[self.ticketLock lock];
[super sellingTickets];
[self.ticketLock unlock];
}
@end

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

  • NSCondition是對mutex和cond的封裝,更加面向對象,我們使用起來也更加的方便簡潔

  • NSCondition同樣實現了NSLocking協議,所以它和NSLock一樣,也有NSLocking協議的lock和unlock方法,可以當做NSLock來使用解決線程同步問題,用法完全一樣。

  • NSCondition提供更高級的用法。waitsignal,和條件信號量類似。比如我們要監聽imageNames數組的個數,當imageNames的個數大于0的時候就執行清空操作。思路是這樣的,當imageNames個數大于0時執行清空操作,否則,wait等待執行清空操作。當imageNames個數增加的時候發生signal信號,讓等待的線程喚醒繼續執行。

  • NSCondition和NSLock、@synchronized等是不同的是,NSCondition可以給每個線程分別加鎖,加鎖后不影響其他線程進入臨界區。這是非常強大。但是正是因為這種分別加鎖的方式,NSCondition使用wait并使用加鎖后并不能真正的解決資源的競爭。比如我們有個需求:不能讓m<0。假設當前m=0,線程A要判斷到m>0為假,執行等待;線程B執行了m=1操作,并喚醒線程A執行m-1操作的同時線程C判斷到m>0,因為他們在不同的線程鎖里面,同樣判斷為真也執行了m-1,這個時候線程A和線程C都會執行m-1,但是m=1,結果就會造成m=-1.

使用方式:

@interface NSCondition : NSObject <NSLocking> {
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;
@property (nullable, copy) NSString *name 
@end

對于上面那個數組操作的案例我們就可以變成這個樣子了
demo:

#import "NSConditionDemo.h"

@interface NSConditionDemo ()

@property (nonatomic,strong) NSCondition *condition;
@property (strong, nonatomic) NSMutableArray *data;
@end

@implementation NSConditionDemo

- (instancetype)init {
    if (self = [super init]) {
        
        self.data = [NSMutableArray array];
    }
    return self;
}

- (NSCondition *)condition {
    if (_condition == nil) {
        _condition = [[NSCondition alloc] init];
    }
    return _condition;
}

- (void)otherTest {
    [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
    [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
}

// 線程1
// 刪除數組中的元素
- (void)__remove {
    [self.condition lock];
    NSLog(@"__remove - begin");
    if (self.data.count == 0) {
        // 等待
        [self.condition wait];
    }
    [self.data removeLastObject];
    NSLog(@"刪除了元素");
    [self.condition unlock];
}

// 線程2
// 往數組中添加元素
- (void)__add {
    [self.condition lock];
    sleep(1);
    [self.data addObject:@"Test"];
    NSLog(@"添加了元素");
    // 信號
    [self.condition signal];
    [self.condition unlock];
}

@end

7 NSConditionLock
  • lock不分條件,如果鎖沒被申請,直接執行代碼
  • unlock不會清空條件,之后滿足條件的鎖還會執行
  • unlockWithCondition:我的理解就是設置解鎖條件(同一時刻只有一個條件,如果已經設置條件,相當于修改條件)
  • lockWhenCondition:滿足特定條件,執行相應代碼
  • NSConditionLock同樣實現了NSLocking協議,試驗過程中發現性能很低。
  • NSConditionLock也可以像NSCondition一樣做多線程之間的任務等待調用,而且是線程安全的。
    NSConditionLock是對NSCondition的進一步封裝,可以設置具體的條件值

使用方式:

@interface NSConditionLock : NSObject <NSLocking> {
 
- (instancetype)initWithCondition:(NSInteger)condition;

@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;
@property (nullable, copy) NSString *name;
@end

里面有三個常用的方法

  • 1、initWithCondition:初始化Condition,并且設置狀態值
  • 2、lockWhenCondition:(NSInteger)condition:當狀態值為condition的時候加鎖
  • 3、unlockWithCondition:(NSInteger)condition當狀態值為condition的時候解鎖

demo:

@interface NSConditionLockDemo()
@property (strong, nonatomic) NSConditionLock *conditionLock;
@end
@implementation NSConditionLockDemo
- (instancetype)init
{
if (self = [super init]) {
self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
}
return self;
}

- (void)otherTest
{
[[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(__two) object:nil] start];
}

- (void)__one
{
[self.conditionLock lock];
NSLog(@"__one");
sleep(1);
[self.conditionLock unlockWithCondition:2];
}

- (void)__two
{
[self.conditionLock lockWhenCondition:2];
NSLog(@"__two");
[self.conditionLock unlockWithCondition:3];
}
@end

示例結果:(即使test方法one和two順序換了也還是先執行one)

image.png


8 dispatch_semaphore
  • semaphore叫做”信號量”
  • 信號量的初始值,可以用來控制線程并發訪問的最大數量
  • 信號量的初始值為1,代表同時只允許1條線程訪問資源,保證線程同步

使用方式:

//表示最多開啟5個線程
dispatch_semaphore_create(5);
// 如果信號量的值 > 0,就讓信號量的值減1,然后繼續往下執行代碼
// 如果信號量的值 <= 0,就會休眠等待,直到信號量的值變成>0,就讓信號量的值減1,然后繼續往下執行代碼
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
// 讓信號量的值+1
dispatch_semaphore_signal(self.semaphore);

demo:

@interface dispatch_semaphoreDemo()
@property (strong, nonatomic) dispatch_semaphore_t semaphore;
@end
@implementation dispatch_semaphoreDemo
- (instancetype)init
{
if (self = [super init]) {
self.semaphore = dispatch_semaphore_create(1);
}
return self;
}
- (void)otherTest
{
for (int i = 0; i < 20; i++) {
[[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
}
}
- (void)test
{
// 如果信號量的值 > 0,就讓信號量的值減1,然后繼續往下執行代碼
// 如果信號量的值 <= 0,就會休眠等待,直到信號量的值變成>0,就讓信號量的值減1,然后繼續往下執行代碼
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);

sleep(2);
NSLog(@"test - %@", [NSThread currentThread]);

// 讓信號量的值+1
dispatch_semaphore_signal(self.semaphore);
}
@end

示例結果:


image.png

我們在運行代碼打印的時候發現,每隔一秒出現一次打印。雖然我們同時開啟20個線程,但是一次只能訪問一條線程的資源


9 dispatch_queue

使用GCD的串行隊列也可以實現線程同步的

使用方式:

dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
// 追加任務1
for (int i = 0; i < 2; ++i) {
NSLog(@"1---%@",[NSThread currentThread]);
}
});

dispatch_sync(queue, ^{
// 追加任務2
for (int i = 0; i < 2; ++i) {
NSLog(@"2---%@",[NSThread currentThread]);
}
});

10 @synchronized(互斥鎖)

在編程中,引入對象互斥鎖的概念,來保證共享數據操作的完整性。每個對象都對應于一個可稱為“互斥鎖”的標記,這個標記用來保證在任一時刻,只能有一個線程訪問對象。

@synchronized 關于 @synchronized,這兒比你想知道的還要多

@synchronized是對mutex遞歸鎖的封裝, @synchronized(obj)內部會生成obj對應的遞歸鎖,然后進行加鎖、解鎖操作

//賣票
- (void)sellingTickets{
@synchronized ([self class]) {
[super sellingTickets];
}
}

對是實現底層我們可以在objc4的objc-sync.mm文件中找到 synchronized就是在開始和結束的時候調用了objc_sync_enter&objc_sync_exit方法。
objc_sync_enter實現

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;
}

就是根據id2data方法找到一個data對象,然后在對data對象進行mutex.lock()加鎖操作。我們點擊進入id2data方法繼續查找

#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;

發現獲取data對象的方法其實就是根據sDataLists[obj].data這個方法來實現的,也就是一個哈希表。


11 atomic
  • atomic用于保證屬性setter、getter的原子性操作,相當于在getter和setter內部加了線程同步的鎖
  • 可以參考源碼objc4的objc-accessors.mm
  • 它并不能保證使用屬性的過程是線程安全的

12 pthread_rwlock

pthread_rwlock經常用于文件等數據的讀寫操作,需要導入頭文件#import <pthread.h>
iOS中的讀寫安全方案需要注意一下場景

1、同一時間,只能有1個線程進行寫的操作
2、同一時間,允許有多個線程進行讀的操作
3、同一時間,不允許既有寫的操作,又有讀的操作

使用方式:

//初始化鎖
pthread_rwlock_t lock;
pthread_rwlock_init(&_lock, NULL);

//讀加鎖
pthread_rwlock_rdlock(&_lock);
//讀嘗試加鎖
pthread_rwlock_trywrlock(&_lock)

//寫加鎖
pthread_rwlock_wrlock(&_lock);
//寫嘗試加鎖
pthread_rwlock_trywrlock(&_lock)

//解鎖
pthread_rwlock_unlock(&_lock);
//銷毀
pthread_rwlock_destroy(&_lock);

demo:

#import <pthread.h>
@interface pthread_rwlockDemo ()
@property (assign, nonatomic) pthread_rwlock_t lock;
@end

@implementation pthread_rwlockDemo

- (instancetype)init
{
self = [super init];
if (self) {
// 初始化鎖
pthread_rwlock_init(&_lock, NULL);
}
return self;
}

- (void)otherTest{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
[self read];
});
dispatch_async(queue, ^{
[self write];
});
}
}
- (void)read {
pthread_rwlock_rdlock(&_lock);
sleep(1);
NSLog(@"%s", __func__);
pthread_rwlock_unlock(&_lock);
}
- (void)write
{
pthread_rwlock_wrlock(&_lock);
sleep(1);
NSLog(@"%s", __func__);
pthread_rwlock_unlock(&_lock);
}
- (void)dealloc
{
pthread_rwlock_destroy(&_lock);
}
@end

示例結果:

image.png

我們可以發現讀操作1s有可能出現多次,但是寫操作不會


13 dispatch_barrier_async

這個函數傳入的并發隊列必須是自己通過dispatch_queue_cretate創建的 如果傳入的是一個串行或是一個全局的并發隊列,那這個函數便等同于dispatch_async函數的效果

//初始化
self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
//讀操作
dispatch_async(self.queue, ^{
});
//寫操作
dispatch_barrier_async(self.queue, ^{
 
});

5.性能對比

基礎表現-所操作耗時

image.png

上圖是常規的鎖操作性能測試(iOS7.0SDK,iPhone6模擬器,Yosemite 10.10.5),垂直方向表示耗時,單位是秒,總耗時越小越好,水平方向表示不同類型鎖的鎖操作,具體又分為兩部分,左邊的常規lock操作(比如NSLock)或者讀read操作(比如ANReadWriteLock),右邊則是寫write操作,圖上僅有ANReadWriteLockANRecursiveRWLock支持,其它不支持的則默認為0,圖上看出,單從性能表現,原子操作是表現最佳的(0.057412秒),@synchronized則是最耗時的(1.753565秒) (測試代碼) 。

多線程鎖刪除數組性能測試

  • 模擬器環境:i5 2.6GH+8G 內存,xcode 7.2.1 (7C1002)+iPhone6SP(9.2)
image.png
  • 真機環境:xcode 7.2.1 (7C1002)+iPhone6(國行)


    image.png

通過測試發現模擬器和真機的區別還是很大的,模擬器上明顯的階梯感,真機就沒有,模擬器上NSConditionLock的性能非常差,我沒有把它的參數加在表格上,不然其他的就看不到了。不過真機上面性能還好。

這些性能測試只是一個參考,沒必要非要去在意這些,畢竟前端的編程一般線程要求沒那么高,可以從其他的地方優化。線程安全中注意避坑,另外選擇自己喜歡的方式,這樣你可以研究的更深入,使用的更熟練。

聲明: 測試結果僅僅代表一個參考,因為各種因素的影響,并沒有那么準確。
綜合比較


image.png

可以看到除了 OSSpinLock 外,dispatch_semaphore 和 pthread_mutex 性能是最高的。有消息稱,蘋果在新的系統中已經優化了 pthread_mutex 的性能,所有它看上去和 dispatch_semaphore 差距并沒有那么大了。


6.常見的死鎖

案例一:
NSLog(@"1"); // 任務1
dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"2"); // 任務2
});
NSLog(@"3"); // 任務3

結果,控制臺輸出:

1

分析

  • dispatch_sync表示一個同步線程;
  • dispatch_get_main_queue表示運行在主線程中的主隊列;
  • 任務2是同步線程的任務。

首先執行任務1,這是肯定沒問題的,只是接下來,程序遇到了同步線程,那么它會進入等待,等待任務2執行完,然后執行任務3。但這是隊列,有任務來,當然會將任務加到隊尾,然后遵循FIFO原則執行任務。那么,現在任務2就會被加到最后,任務3排在了任務2前面,問題來了:

任務3要等任務2執行完才能執行,任務2由排在任務3后面,意味著任務2要在任務3執行完才能執行,所以他們進入了互相等待的局面。【既然這樣,那干脆就卡在這里吧】這就是死鎖。

image.png

案例二
NSLog(@"1"); // 任務1
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    NSLog(@"2"); // 任務2
});
NSLog(@"3"); // 任務3

結果,控制臺輸出:

1
2
3

分析
首先執行任務1,接下來會遇到一個同步線程,程序會進入等待。等待任務2執行完成以后,才能繼續執行任務3。從dispatch_get_global_queue可以看出,任務2被加入到了全局的并行隊列中,當并行隊列執行完任務2以后,返回到主隊列,繼續執行任務3。

image.png


案例三
dispatch_queue_t queue = dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"1"); // 任務1
dispatch_async(queue, ^{
    NSLog(@"2"); // 任務2
    dispatch_sync(queue, ^{  
        NSLog(@"3"); // 任務3
    });
    NSLog(@"4"); // 任務4
});
NSLog(@"5"); // 任務5

結果,控制臺輸出:

1
5
2
// 5和2的順序不一定復制代碼

分析
這個案例沒有使用系統提供的串行或并行隊列,而是自己通過dispatch_queue_create函數創建了一個DISPATCH_QUEUE_SERIAL的串行隊列。

  • 執行任務1;
  • 遇到異步線程,將【任務2、同步線程、任務4】加入串行隊列中。因為是異步線程,所以在主線程中的任務5不必等待異步線程中的所有任務完成;
  • 因為任務5不必等待,所以2和5的輸出順序不能確定;
  • 任務2執行完以后,遇到同步線程,這時,將任務3加入串行隊列;
  • 又因為任務4比任務3早加入串行隊列,所以,任務3要等待任務4完成以后,才能執行。但是任務3所在的同步線程會阻塞,所以任務4必須等任務3執行完以后再執行。這就又陷入了無限的等待中,造成死鎖。
image.png

案例四
NSLog(@"1"); // 任務1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"2"); // 任務2
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"3"); // 任務3
    });
    NSLog(@"4"); // 任務4
});
NSLog(@"5"); // 任務5

結果,控制臺輸出:

1
2
5
3
4
// 5和2的順序不一定復制代碼

分析

  • 首先,將【任務1、異步線程、任務5】加入Main Queue中,異步線程中的任務是:【任務2、同步線程、任務4】。
  • 所以,先執行任務1,然后將異步線程中的任務加入到Global Queue中,因為異步線程,所以任務5不用等待,結果就是2和5的輸出順序不一定。
  • 然后再看異步線程中的任務執行順序。任務2執行完以后,遇到同步線程。將同步線程中的任務加入到Main Queue中,這時加入的任務3在任務5的后面。
  • 當任務3執行完以后,沒有了阻塞,程序繼續執行任務4。
  • 從以上的分析來看,得到的幾個結果:1最先執行;2和5順序不一定;4一定在3后面。
image.png

案例五
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"1"); // 任務1
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"2"); // 任務2
    });
    NSLog(@"3"); // 任務3
});
NSLog(@"4"); // 任務4
while (1) {
}
NSLog(@"5"); // 任務5

結果,控制臺輸出:

1
4
// 1和4的順序不一定復制代碼

分析

  • 和上面幾個案例的分析類似,先來看看都有哪些任務加入了Main Queue:【異步線程、任務4、死循環、任務5】。
  • 在加入到Global Queue異步線程中的任務有:【任務1、同步線程、任務3】。
    第一個就是異步線程,任務4不用等待,所以結果任務1和任務4順序不一定。
  • 任務4完成后,程序進入死循環,Main Queue阻塞。但是加入到Global Queue的異步線程不受影響,繼續執行任務1后面的同步線程。
  • 同步線程中,將任務2加入到了主線程,并且,任務3等待任務2完成以后才能執行。這時的主線程,已經被死循環阻塞了。所以任務2無法執行,當然任務3也無法執行,在死循環后的任務5也不會執行。
  • 最終,只能得到1和4順序不定的結果。
image.png

總結

    1. 總的來看,推薦pthread_mutex作為實際項目的首選方案;
    1. 對于耗時較大又易沖突的讀操作,可以使用讀寫鎖代替pthread_mutex;
    1. 如果確認僅有set/get的訪問操作,可以選用原子操作屬性;
    1. 對于性能要求苛刻,可以考慮使用OSSpinLock,需要確保加鎖片段的耗時足夠小;
    1. 條件鎖基本上使用面向對象的NSCondition和NSConditionLock即可;
    1. @synchronized則適用于低頻場景如初始化或者緊急修復使用;

蘋果為多線程、共享內存提供了多種同步解決方案(鎖),對于這些方案的比較,大都討論了鎖的用法以及鎖操作的開銷。個人認為最優秀的選用還是看應用場景,高頻接口VS低頻接口、有限沖突VS激烈競爭、代碼片段耗時的長短,都是選擇的重要依據,選擇適用于當前應用場景的方案才是王道。

參考文章:
iOS多線程安全-13種線程鎖
談談iOS多線程的鎖

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375