目錄:
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.多線程安全隱患分析
3.多線程安全隱患的解決方案
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
運行結果:
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
中重新在創建一把新的鎖,兩個方法的鎖對象不同,就不會造成線程死鎖了。
- 條件
// 初始化屬性
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) //刪除操作再添加了元素之后才進行的。
示例結果:
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提供更高級的用法。
wait
和signal
,和條件信號量類似。比如我們要監聽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)
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
示例結果:
我們在運行代碼打印的時候發現,每隔一秒出現一次打印。雖然我們同時開啟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
示例結果:
我們可以發現讀操作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.性能對比
基礎表現-所操作耗時
上圖是常規的鎖操作性能測試(iOS7.0SDK,iPhone6模擬器,Yosemite 10.10.5),垂直方向表示耗時,單位是秒,總耗時越小越好,水平方向表示不同類型鎖的鎖操作,具體又分為兩部分,左邊的常規lock操作(比如NSLock)或者讀read操作(比如ANReadWriteLock),右邊則是寫write操作,圖上僅有ANReadWriteLock和ANRecursiveRWLock支持,其它不支持的則默認為0,圖上看出,單從性能表現,原子操作是表現最佳的(0.057412秒),@synchronized則是最耗時的(1.753565秒) (測試代碼) 。
多線程鎖刪除數組性能測試
- 模擬器環境:i5 2.6GH+8G 內存,xcode 7.2.1 (7C1002)+iPhone6SP(9.2)
-
真機環境:xcode 7.2.1 (7C1002)+iPhone6(國行)
image.png
通過測試發現模擬器和真機的區別還是很大的,模擬器上明顯的階梯感,真機就沒有,模擬器上NSConditionLock的性能非常差,我沒有把它的參數加在表格上,不然其他的就看不到了。不過真機上面性能還好。
這些性能測試只是一個參考,沒必要非要去在意這些,畢竟前端的編程一般線程要求沒那么高,可以從其他的地方優化。線程安全中注意避坑,另外選擇自己喜歡的方式,這樣你可以研究的更深入,使用的更熟練。
聲明: 測試結果僅僅代表一個參考,因為各種因素的影響,并沒有那么準確。
綜合比較
可以看到除了 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執行完才能執行,所以他們進入了互相等待的局面。【既然這樣,那干脆就卡在這里吧】這就是死鎖。
案例二
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。
案例三
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執行完以后再執行。這就又陷入了無限的等待中,造成死鎖。
案例四
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后面。
案例五
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順序不定的結果。
總結
- 總的來看,推薦pthread_mutex作為實際項目的首選方案;
- 對于耗時較大又易沖突的讀操作,可以使用讀寫鎖代替pthread_mutex;
- 如果確認僅有set/get的訪問操作,可以選用原子操作屬性;
- 對于性能要求苛刻,可以考慮使用OSSpinLock,需要確保加鎖片段的耗時足夠小;
- 條件鎖基本上使用面向對象的NSCondition和NSConditionLock即可;
- @synchronized則適用于低頻場景如初始化或者緊急修復使用;
蘋果為多線程、共享內存提供了多種同步解決方案(鎖),對于這些方案的比較,大都討論了鎖的用法以及鎖操作的開銷。個人認為最優秀的選用還是看應用場景,高頻接口VS低頻接口、有限沖突VS激烈競爭、代碼片段耗時的長短,都是選擇的重要依據,選擇適用于當前應用場景的方案才是王道。
參考文章:
iOS多線程安全-13種線程鎖
談談iOS多線程的鎖