iOS底層原理 多線程之安全鎖以及經常使用的讀寫鎖 --(11)

上篇文章講了GCD的用法,只要提到了多線程就應該想到線程安全,那麼怎麼作才能作到在多個線程中保證安全呢? 這篇文章主要講解線程安全。html

線程安全

線程安全是什麼呢?摘抄一段百度百科的一段話ios

線程安全是多線程編程時的計算機程序代碼中的一個概念。在擁有共享數據的多條線程並行執行的程序中,線程安全的代碼會經過同步機制保證各個線程均可以正常且正確的執行,不會出現數據污染等意外狀況。git

爲何須要線程安全

ATM確定用過,你要是邊取錢,邊存錢,會出問題嗎?當你取錢的時候,正在取,結果有人匯款正好到帳,原本1000塊取了100剩下900,結果到帳200,1000+200=1200,由於你取的時候,還沒取完,匯款到帳告終果數字又加上去了。你取的錢跑哪裏去了,這裏就須要取錢的時候不能寫入數據,就是匯款須要在你取錢完成以後再匯款,不能同時進行。github

那麼在iOS中,鎖是如何使用的呢?macos

自旋鎖 OS_SPINLOCK

什麼是優先級反轉

簡單從字面上來講,就是低優先級的任務先於高優先級的任務執行了,優先級搞反了。那在什麼狀況下會生這種狀況呢?編程

假設三個任務準備執行,A,B,C,優先級依次是A>B>C;數組

首先:C處於運行狀態,得到CPU正在執行,同時佔有了某種資源;sass

其次:A進入就緒狀態,由於優先級比C高,因此得到CPU,A轉爲運行狀態;C進入就緒狀態;安全

第三:執行過程當中須要使用資源,而這個資源又被等待中的C佔有的,因而A進入阻塞狀態,C回到運行狀態;bash

第四:此時B進入就緒狀態,由於優先級比C高,B得到CPU,進入運行狀態;C又回到就緒狀態;

第五:若是這時又出現B2,B3等任務,他們的優先級比C高,但比A低,那麼就會出現高優先級任務的A不能執行,反而低優先級的B,B2,B3等任務能夠執行的奇怪現象,而這就是優先反轉。

OS_SPINLOCK叫作自旋鎖,等待鎖的進程會處於忙等(busy-wait)狀態,一直佔用着CPU資源,目前已經不安全,可能會出現優先級翻轉問題。

OS_SPINLOCKAPI

//初始化 通常是0,或者直接數字0也是ok的。
#define OS_SPINLOCK_INIT 0
//鎖的初始化
OSSpinLock lock = OS_SPINLOCK_INIT;
//嘗試加鎖
bool ret = OSSpinLockTry(&lock);
//加鎖
OSSpinLockLock(&lock);
//解鎖
OSSpinLockUnlock(&lock);
複製代碼

OSSpinLock簡單實現12306如何賣票

//基類實現的賣票
- (void)__saleTicket{
    NSInteger oldCount = self.ticketsCount;
	if (isLog) {
		sleep(sleepTime);
	}
    oldCount --;
    self.ticketsCount = oldCount;
	if (isLog) {
	printf("還剩% 2ld 張票 - %s \n",(long)oldCount,[NSThread currentThread].description.UTF8String);
	}
	
}



- (void)ticketTest{
    self.ticketsCount = 10000;
	NSInteger count = self.ticketsCount/3;
	dispatch_queue_t queue = dispatch_queue_create("tick.com", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
		if (time1 == 0) {
			time1 = CFAbsoluteTimeGetCurrent();
		}
        for (int i = 0; i < count; i ++) {
            [self __saleTicket];
        }
    });
    
    dispatch_async(queue, ^{
		if (time1 == 0) {
			time1 = CFAbsoluteTimeGetCurrent();
		}
        for (int i = 0; i < count; i ++) {
            [self __saleTicket];
        }
    });
    dispatch_async(queue, ^{
		if (time1 == 0) {
			time1 = CFAbsoluteTimeGetCurrent();
		}
        for (int i = 0; i < count; i ++) {
            [self __saleTicket];
        }
    });
	dispatch_barrier_async(queue, ^{
		CFAbsoluteTime time = CFAbsoluteTimeGetCurrent() - time1;
		printf("tick cost time:%f",time);
	});
}
- (void)__getMonery{
    OSSpinLockLock(&_moneyLock);
    [super __getMonery];
    OSSpinLockUnlock(&_moneyLock);
}
- (void)__saleTicket{
    OSSpinLockLock(&_moneyLock);
    [super __saleTicket];
    OSSpinLockUnlock(&_moneyLock);
}
- (void)__saveMonery{
    OSSpinLockLock(&_moneyLock);
    [super __saveMonery];
    OSSpinLockUnlock(&_moneyLock);
}

- (void)__saleTicket{
    NSInteger oldCount = self.ticketsCount;
    oldCount --;
    self.ticketsCount = oldCount;
}
//log
還剩 9 張票 - <NSThread: 0x600003dc6080>{number = 3, name = (null)} 
還剩 8 張票 - <NSThread: 0x600003dc6080>{number = 3, name = (null)} 
還剩 7 張票 - <NSThread: 0x600003dc6080>{number = 3, name = (null)} 
還剩 6 張票 - <NSThread: 0x600003df3a00>{number = 4, name = (null)} 
還剩 5 張票 - <NSThread: 0x600003df3a00>{number = 4, name = (null)} 
還剩 4 張票 - <NSThread: 0x600003df3a00>{number = 4, name = (null)} 
還剩 3 張票 - <NSThread: 0x600003dc0000>{number = 5, name = (null)} 
還剩 2 張票 - <NSThread: 0x600003dc0000>{number = 5, name = (null)} 
還剩 1 張票 - <NSThread: 0x600003dc0000>{number = 5, name = (null)} 
複製代碼

彙編分析

for (NSInteger i = 0; i < 5; i ++) {
	[[[NSThread alloc]initWithTarget:self selector:@selector(__saleTicket) object:nil] start];
}

而後將睡眠時間設置爲600s,方便咱們調試。
- (void)__saleTicket{
    OSSpinLockLock(&_moneyLock);//此行打斷點
    [super __saleTicket];
    OSSpinLockUnlock(&_moneyLock);
}
複製代碼

到了斷點進入Debug->Debug WorkFlow ->Always Show Disassembly,到了彙編界面,在LLDB輸入stepi,而後一直按enter,一直重複執行上句命令,直到進入了循環,就是相似下列的三行,發現ja跳轉到地址0x103f3d0f9,每次執行到ja老是跳轉到0x103f3d0f9,直到線程睡眠結束。

->  0x103f3d0f9 <+241>: movq   %rcx, (%r8)
0x103f3d0fc <+244>: addq   $0x8, %r8
0x103f3d100 <+248>: cmpq   %r8, %r9
0x103f3d103 <+251>: ja     0x103f3d0f9
複製代碼

能夠經過彙編分析瞭解到自旋鎖是真的忙等,閒不住的鎖。

os_unfair_lock

os_unfair_lock被系統定義爲低級鎖,通常低級鎖都是閒的時候在睡眠,在等待的時候被內核喚醒,目的是替換已棄用的OSSpinLock,並且必須使用OS_UNFAIR_LOCK_INIT來初始化,加鎖和解鎖必須在相同的線程,不然會中斷進程,使用該鎖須要系統在__IOS_AVAILABLE(10.0),鎖的數據結構是一個結構體

OS_UNFAIR_LOCK_AVAILABILITY
typedef struct os_unfair_lock_s {
	uint32_t _os_unfair_lock_opaque;
} os_unfair_lock, *os_unfair_lock_t;
複製代碼

os_unfair_lock使用很是簡單,只須要在任務前加鎖,任務後解鎖便可。

@interface FYOSUnfairLockDemo : FYBaseDemo
@property (nonatomic,assign) os_unfair_lock lock;
@end

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

- (void)__saveMonery{
	os_unfair_lock_lock(&_unlock);
	[super __saveMonery];
	os_unfair_lock_unlock(&_unlock);
}
- (void)__getMonery{
	os_unfair_lock_lock(&_unlock);
	[super __getMonery];
	os_unfair_lock_unlock(&_unlock);
}
- (void)__saleTicket{
	os_unfair_lock_lock(&_unlock);
	[super __saleTicket];
	os_unfair_lock_unlock(&_unlock);
}
@end
//log
還剩 9 張票 - <NSThread: 0x600002eb4bc0>{number = 3, name = (null)} 
還剩 8 張票 - <NSThread: 0x600002eb4bc0>{number = 3, name = (null)} 
還剩 7 張票 - <NSThread: 0x600002eb4bc0>{number = 3, name = (null)} 
還剩 6 張票 - <NSThread: 0x600002eb1500>{number = 4, name = (null)} 
還剩 5 張票 - <NSThread: 0x600002eb1500>{number = 4, name = (null)} 
還剩 4 張票 - <NSThread: 0x600002eb1500>{number = 4, name = (null)} 
還剩 3 張票 - <NSThread: 0x600002ed4340>{number = 5, name = (null)} 
還剩 2 張票 - <NSThread: 0x600002ed4340>{number = 5, name = (null)} 
還剩 1 張票 - <NSThread: 0x600002ed4340>{number = 5, name = (null)} 
複製代碼

彙編分析

LLDB 中命令stepi遇到函數會進入到函數,nexti會跳過函數。咱們將斷點打到添加鎖的位置

- (void)__saleTicket{
 	os_unfair_lock_lock(&_unlock);//斷點位置
	[super __saleTicket];
	os_unfair_lock_unlock(&_unlock);
}
複製代碼

執行si,一直enter,最終是中止該位子,模擬器缺跳出來了,再enter也沒用了,由於線程在睡眠了。syscall是調用系統函數的命令。

libsystem_kernel.dylib`__ulock_wait:
    0x107a3b9d4 <+0>:  movl   $0x2000203, %eax          ; imm = 0x2000203 
    0x107a3b9d9 <+5>:  movq   %rcx, %r10
->  0x107a3b9dc <+8>:  syscall
複製代碼

互斥鎖 pthread_mutex_t

mutex叫互斥鎖,等待鎖的線程會處於休眠狀態。

-(void)dealloc{
	pthread_mutex_destroy(&_plock);
	pthread_mutexattr_destroy(&t);
}
-(instancetype)init{
	if (self =[super init]) {
		//初始化鎖的屬性 
//		pthread_mutexattr_init(&t);
//		pthread_mutexattr_settype(&t, PTHREAD_MUTEX_NORMAL);
//		//初始化鎖
//		pthread_mutex_init(&_plock, &t);
		
		pthread_mutex_t plock = PTHREAD_MUTEX_INITIALIZER;
		self.plock = plock;
	}
	return self;
}
-(void)__saleTicket{
	pthread_mutex_lock(&_plock);
	[super __saleTicket];
	pthread_mutex_unlock(&_plock);
}
- (void)__getMonery{
	pthread_mutex_lock(&_plock);
	[super __getMonery];
	pthread_mutex_unlock(&_plock);
}
- (void)__saveMonery{
	pthread_mutex_lock(&_plock);
	[super __saveMonery];
	pthread_mutex_unlock(&_plock);
}
//log

還剩 9 張票 - <NSThread: 0x6000014e3600>{number = 3, name = (null)} 
還剩 8 張票 - <NSThread: 0x6000014c8d80>{number = 4, name = (null)} 
還剩 7 張票 - <NSThread: 0x6000014c8f40>{number = 5, name = (null)} 
還剩 4 張票 - <NSThread: 0x6000014c8f40>{number = 5, name = (null)} 
還剩 3 張票 - <NSThread: 0x6000014c8f40>{number = 5, name = (null)} 
還剩 5 張票 - <NSThread: 0x6000014c8d80>{number = 4, name = (null)} 
還剩 6 張票 - <NSThread: 0x6000014e3600>{number = 3, name = (null)} 
還剩 2 張票 - <NSThread: 0x6000014c8d80>{number = 4, name = (null)} 
還剩 1 張票 - <NSThread: 0x6000014e3600>{number = 3, name = (null)} 
複製代碼

互斥鎖有三個類型

/*
 * 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
複製代碼

當咱們這樣子函數調用函數會出現死鎖的問題,這是怎麼出現的呢?第一把鎖是鎖住狀態,而後進入第二個函數,鎖在鎖住狀態,在等待,可是這把鎖須要向後執行纔會解鎖,到時無限期的等待。

- (void)otherTest{
	pthread_mutex_lock(&_plock);
	NSLog(@"%s",__func__);
	[self otherTest2];
	pthread_mutex_unlock(&_plock);
}
- (void)otherTest2{
	pthread_mutex_lock(&_plock);
	NSLog(@"%s",__func__);
	pthread_mutex_unlock(&_plock);
}

//log
-[FYPthread_mutex2 otherTest]
複製代碼

上面這個需求須要使用兩把鎖,或者使用遞歸鎖來解決問題。

- (void)otherTest{
	pthread_mutex_lock(&_plock);
	NSLog(@"%s",__func__);
	[self otherTest2];
	pthread_mutex_unlock(&_plock);
}
- (void)otherTest2{
	pthread_mutex_lock(&_plock2);
	NSLog(@"%s",__func__);
	pthread_mutex_unlock(&_plock2);
}

//log
-[FYPthread_mutex2 otherTest]
-[FYPthread_mutex2 otherTest2]
複製代碼

從使用2把鎖是能夠解決這個問題的。 遞歸鎖是什麼鎖呢?容許同一個線程對一把鎖重複加鎖。

NSLock、NSRecursiveLosk

NSLock是對mutex普通鎖的封裝

使用(LLDB) si能夠跟蹤[myLock lock];的內部函數最終是pthread_mutex_lock

Foundation`-[NSLock lock]:
    0x1090dfb5a <+0>:  pushq  %rbp
    0x1090dfb5b <+1>:  movq   %rsp, %rbp
    0x1090dfb5e <+4>:  callq  0x1092ca3fe               ; symbol stub for: object_getIndexedIvars
    0x1090dfb63 <+9>:  movq   %rax, %rdi
    0x1090dfb66 <+12>: popq   %rbp
->  0x1090dfb67 <+13>: jmp    0x1092ca596   ;
//  symbol stub for: pthread_mutex_lock
複製代碼

NSLock API大全

//協議NSLocking
@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 API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end
複製代碼

用法也很簡單

@interface FYNSLock(){
	NSLock *_lock;
}
@end

@implementation FYNSLock
- (instancetype)init{
	if (self = [super init]) {
		//封裝了mutex的普通鎖
		_lock=[[NSLock alloc]init];
	}
	return self;
}

- (void)__saveMonery{
	[_lock lock];
	[super __saveMonery];
	[_lock unlock];
}
- (void)__saleTicket{
	[_lock lock];
	[super __saleTicket];
	[_lock unlock];
}
- (void)__getMonery{
	[_lock lock];
	[super __getMonery];
	[_lock unlock];
}
@end
//log

還剩 9 張票 - <NSThread: 0x600003d4dc40>{number = 3, name = (null)} 
還剩 8 張票 - <NSThread: 0x600003d4dc40>{number = 3, name = (null)} 
還剩 7 張票 - <NSThread: 0x600003d4dc40>{number = 3, name = (null)} 
還剩 6 張票 - <NSThread: 0x600003d7bfc0>{number = 4, name = (null)} 
還剩 5 張票 - <NSThread: 0x600003d7bfc0>{number = 4, name = (null)} 
還剩 4 張票 - <NSThread: 0x600003d7bfc0>{number = 4, name = (null)} 
還剩 3 張票 - <NSThread: 0x600003d66c00>{number = 5, name = (null)} 
還剩 2 張票 - <NSThread: 0x600003d66c00>{number = 5, name = (null)} 
還剩 1 張票 - <NSThread: 0x600003d66c00>{number = 5, name = (null)} 
複製代碼

NSRecursiveLock也是對mutex遞歸鎖的封裝,APINSLock基本一致

- (BOOL)tryLock;//嘗試加鎖
- (BOOL)lockBeforeDate:(NSDate *)limit;//日期前加鎖
複製代碼

遞歸鎖能夠對相同的線程進行反覆加鎖

@implementation FYRecursiveLockDemo
- (instancetype)init{
	if (self = [super init]) {
		//封裝了mutex的遞歸鎖
		_lock=[[NSRecursiveLock alloc]init];
	}
	return self;
}
- (void)otherTest{
	static int count = 10;
	[_lock lock];
	while (count > 0) {
		count -= 1;
		printf("循環% 2d次 - %s \n",count,[NSThread currentThread].description.UTF8String);
		[self otherTest];
	}
	[_lock unlock];
}
@end

//log
循環 9次 - <NSThread: 0x60000274e900>{number = 1, name = main} 
循環 8次 - <NSThread: 0x60000274e900>{number = 1, name = main} 
循環 7次 - <NSThread: 0x60000274e900>{number = 1, name = main} 
循環 6次 - <NSThread: 0x60000274e900>{number = 1, name = main} 
循環 5次 - <NSThread: 0x60000274e900>{number = 1, name = main} 
循環 4次 - <NSThread: 0x60000274e900>{number = 1, name = main} 
循環 3次 - <NSThread: 0x60000274e900>{number = 1, name = main} 
循環 2次 - <NSThread: 0x60000274e900>{number = 1, name = main} 
循環 1次 - <NSThread: 0x60000274e900>{number = 1, name = main} 
循環 0次 - <NSThread: 0x60000274e900>{number = 1, name = main}
複製代碼

NSCondition 條件

- (void)wait;//等待
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;//喚醒一個線程
- (void)broadcast;//喚醒多個線程
複製代碼

NSCondition是對mutexcond的封裝

- (instancetype)init{
	if (self = [super init]) {
		//遵照的 lock協議 的 條件🔐
		_lock=[[NSCondition alloc]init];
		self.array =[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];
}
- (void)__add{
	[_lock lock];
	[self.array addObject:@"Test"];
	NSLog(@"添加成功");
	sleep(1);
	[_lock signal];//喚醒一個線程
	[_lock unlock];
}
- (void)__remove{
	[_lock lock];
	if (self.array.count == 0) {
		[_lock wait];
	}
	[self.array removeLastObject];
	NSLog(@"刪除成功");

	[_lock unlock];
}
@end
//Log

2019-07-29 10:06:48.904648+0800 day16--線程安全[43603:4402260] 添加成功
2019-07-29 10:06:49.907641+0800 day16--線程安全[43603:4402259] 刪除成功
複製代碼

能夠看到時間上差了1秒,正好是咱們設定的sleep(1);。優勢是可讓線程之間造成依賴,缺點是沒有明確的條件。

NSConditionLock 能夠實現線程依賴的鎖

NSConditionLock是能夠實現多個子線程進行線程間的依賴,A依賴於B執行完成,B依賴於C執行完畢則可使用NSConditionLock來解決問題。 首先看下API

@property (readonly) NSInteger condition;//條件值
- (void)lockWhenCondition:(NSInteger)condition;//當con爲condition進行鎖住
//嘗試加鎖
- (BOOL)tryLock;
//當con爲condition進行嘗試鎖住
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
//當con爲condition進行解鎖
- (void)unlockWithCondition:(NSInteger)condition;
//NSDate 小余 limit進行 加鎖
- (BOOL)lockBeforeDate:(NSDate *)limit;
//條件爲condition 在limit以前進行加鎖
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
複製代碼

條件鎖的使用,在lockWhenCondition:(NSInteger)condition的條件到達的時候才能進行正常的加鎖和unlockWithCondition:(NSInteger)condition解鎖,不然會阻塞線程。

- (void)otherTest{
	[[[NSThread alloc]initWithTarget:self selector:@selector(__test2) object:nil] start];
	[[[NSThread alloc]initWithTarget:self selector:@selector(__test1) object:nil] start];
	[[[NSThread alloc]initWithTarget:self selector:@selector(__test3) object:nil] start];

}
- (void)__test1{
	[_lock lockWhenCondition:1];
	NSLog(@"%s",__func__);
	[_lock unlockWithCondition:2];//解鎖 並賦值2
}
- (void)__test2{
	[_lock lockWhenCondition:2];
	NSLog(@"%s",__func__);
	[_lock unlockWithCondition:3];//解鎖 並賦值3
}
- (void)__test3{
	[_lock lockWhenCondition:3];
	NSLog(@"%s",__func__);
	[_lock unlockWithCondition:4];//解鎖 並賦值4
}
@end
//log
-[FYCondLockDemo2 __test1]
-[FYCondLockDemo2 __test2]
-[FYCondLockDemo2 __test3]
複製代碼

con = 1進行test1加鎖和執行任務A,任務A執行完畢,進行解鎖,並把值2賦值給lock,這是當con = 2的鎖開始加鎖,進入任務B,開始執行任務B,當任務B執行完畢,進行解鎖並賦值爲3,而後con=3的鎖進行加鎖,解鎖並賦值4來進行線程之間的依賴。

dispatch_queue 特殊的鎖

其實直接使用GCD的串行隊列,也是能夠實現線程同步的。串行隊列其實就是線程的任務在隊列中按照順序執行,達到了鎖的目的。

@interface FYSerialQueueDemo(){
	dispatch_queue_t _queue;
}@end
@implementation FYSerialQueueDemo
- (instancetype)init{
	if (self =[super init]) {
		_queue = dispatch_queue_create("fyserial.queue", DISPATCH_QUEUE_SERIAL);
	}
	return self;
}
- (void)__saleTicket{
	dispatch_sync(_queue, ^{
		[super __saleTicket];
	});
}
- (void)__getMonery{
	dispatch_sync(_queue, ^{
		[super __getMonery];
	});
}
- (void)__saveMonery{
	dispatch_sync(_queue, ^{
		[super __saveMonery];
	});
}
@end
//log
還剩 9 張票 - <NSThread: 0x600001211b40>{number = 3, name = (null)} 
還剩 8 張票 - <NSThread: 0x600001243700>{number = 4, name = (null)} 
還剩 7 張票 - <NSThread: 0x60000121dd80>{number = 5, name = (null)} 
還剩 6 張票 - <NSThread: 0x600001211b40>{number = 3, name = (null)} 
還剩 5 張票 - <NSThread: 0x600001243700>{number = 4, name = (null)} 
還剩 4 張票 - <NSThread: 0x60000121dd80>{number = 5, name = (null)} 
還剩 3 張票 - <NSThread: 0x600001211b40>{number = 3, name = (null)} 
還剩 2 張票 - <NSThread: 0x600001243700>{number = 4, name = (null)} 
還剩 1 張票 - <NSThread: 0x60000121dd80>{number = 5, name = (null)}
複製代碼

dispatch_semaphore 信號量控制併發數量

當咱們有大量任務須要併發執行,並且同時最大併發量爲5個線程,這樣子又該如何控制呢?dispatch_semaphore信號量正好能夠知足咱們的需求。 dispatch_semaphore能夠控制併發線程的數量,當設置爲1時,能夠做爲同步鎖來用,設置多個的時候,就是異步併發隊列。

//初始化信號量 值爲2,就是最多容許同時2個線程執行
_semaphore = dispatch_semaphore_create(2);
//生成多個線程進行併發訪問test
- (void)otherTest{
	for (int i = 0; i < 10; i ++) {
		[[[NSThread alloc]initWithTarget:self selector:@selector(test) object:nil]start];
	}
}
- (void)test{
//若是信號量>0 ,讓信號量-1,繼續向下執行。
//若是信號量 <= 0;就會等待,等待時間是 DISPATCH_TIME_FOREVER
	dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
	sleep(2);//睡眠時間2s
	NSLog(@"%@",[NSThread currentThread]);
	//釋放一個信號量
	dispatch_semaphore_signal(_semaphore);
}
//log

2019-07-29 11:17:53.233318+0800 day16--線程安全[47907:4529610] <NSThread: 0x600002c45240>{number = 4, name = (null)}
2019-07-29 11:17:53.233329+0800 day16--線程安全[47907:4529609] <NSThread: 0x600002c45200>{number = 3, name = (null)}
2019-07-29 11:17:55.233879+0800 day16--線程安全[47907:4529616] <NSThread: 0x600002c45540>{number = 10, name = (null)}
2019-07-29 11:17:55.233879+0800 day16--線程安全[47907:4529612] <NSThread: 0x600002c45440>{number = 6, name = (null)}
2019-07-29 11:17:57.238860+0800 day16--線程安全[47907:4529613] <NSThread: 0x600002c45480>{number = 7, name = (null)}
2019-07-29 11:17:57.238867+0800 day16--線程安全[47907:4529614] <NSThread: 0x600002c454c0>{number = 8, name = (null)}
2019-07-29 11:17:59.241352+0800 day16--線程安全[47907:4529615] <NSThread: 0x600002c45500>{number = 9, name = (null)}
2019-07-29 11:17:59.241324+0800 day16--線程安全[47907:4529611] <NSThread: 0x600002c45400>{number = 5, name = (null)}
2019-07-29 11:18:01.245790+0800 day16--線程安全[47907:4529618] <NSThread: 0x600002c455c0>{number = 12, name = (null)}
2019-07-29 11:18:01.245790+0800 day16--線程安全[47907:4529617] <NSThread: 0x600002c45580>{number = 11, name = (null)}
複製代碼

一次最多2個線程同時執行任務,暫停時間是2s。 使用信號量實現線程最大併發鎖, 同時只有2個線程執行的。

- (instancetype)init{
	if (self =[super init]) {
		_semaphore = dispatch_semaphore_create(1);
	}
	return self;
}
- (void)__saleTicket{
	dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
	[super __saleTicket];
	dispatch_semaphore_signal(_semaphore);
}
//log
還剩 9 張票 - <NSThread: 0x6000022e0c00>{number = 3, name = (null)} 
還剩 8 張票 - <NSThread: 0x6000022e0dc0>{number = 4, name = (null)} 
還剩 7 張票 - <NSThread: 0x6000022ce880>{number = 5, name = (null)} 
還剩 6 張票 - <NSThread: 0x6000022e0c00>{number = 3, name = (null)} 
還剩 5 張票 - <NSThread: 0x6000022e0dc0>{number = 4, name = (null)} 
還剩 4 張票 - <NSThread: 0x6000022ce880>{number = 5, name = (null)} 
還剩 3 張票 - <NSThread: 0x6000022e0c00>{number = 3, name = (null)} 
還剩 2 張票 - <NSThread: 0x6000022e0dc0>{number = 4, name = (null)} 
還剩 1 張票 - <NSThread: 0x6000022ce880>{number = 5, name = (null)} 
複製代碼

@synchronized

@synchronized(id obj){}鎖的是對象obj,使用該鎖的時候,底層是對象計算出來的值做爲key,生成一把鎖,不一樣的資源的讀寫可使用不一樣obj做爲鎖對象。

- (void)__saleTicket{
	@synchronized (self) {
		[super __saleTicket];
	}
 }
 //log
還剩 9 張票 - <NSThread: 0x60000057d5c0>{number = 3, name = (null)} 
還剩 8 張票 - <NSThread: 0x60000056f340>{number = 4, name = (null)} 
還剩 7 張票 - <NSThread: 0x60000057d500>{number = 5, name = (null)} 
還剩 6 張票 - <NSThread: 0x60000057d5c0>{number = 3, name = (null)} 
還剩 5 張票 - <NSThread: 0x60000056f340>{number = 4, name = (null)} 
還剩 4 張票 - <NSThread: 0x60000057d500>{number = 5, name = (null)} 
還剩 3 張票 - <NSThread: 0x60000057d5c0>{number = 3, name = (null)} 
還剩 2 張票 - <NSThread: 0x60000056f340>{number = 4, name = (null)} 
還剩 1 張票 - <NSThread: 0x60000057d500>{number = 5, name = (null)} 
複製代碼

atmoic 原子操做

給屬性添加atmoic修飾,能夠保證屬性的settergetter都是原子性操做,也就保證了settergetter的內部是線程同步的。 原子操做是最終調用了static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) objc-accessors.mm 48行,咱們進入到函數內部

//設置屬性原子操做
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);
}

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{//偏移量等於0則是class指針
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }
//其餘的value
    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
    //若是是copy 用copyWithZone:
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        //mutableCopy則調用mutableCopyWithZone:
        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);
}


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

//以屬性的地址爲參數計算出key ,鎖爲value
StripedMap<spinlock_t> PropertyLocks;
複製代碼

從源碼瞭解到設置屬性讀取是self+屬性的偏移量,當copymutableCopy會調用到[newValue copyWithZone:nil][newValue mutableCopyWithZone:nil],若是新舊值相等則不進行操做,非原子操做直接賦值,原子操做則獲取spinlock_t& slotlock = PropertyLocks[slot]進行加鎖、賦值、解鎖操做。並且PropertyLocks是一個類,類有一個數組屬性,使用*p計算出來的值做爲key

咱們提取出來關鍵代碼

//原子操做 加鎖
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;//賦值
slotlock.unlock();//解鎖
複製代碼

使用自旋鎖對賦值操做進行加鎖,保證了setter()方法的安全性

//原子操做 加鎖 ->自旋鎖
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();//加鎖
id value = objc_retain(*slot);
slotlock.unlock();//解鎖
複製代碼

取值以前進行加鎖,取值以後進行解鎖,保證了getter()方法的安全。

由上面得知atmoic僅僅是對方法setter()getter()安全,對成員變量不保證安全,對於屬性的讀寫通常使用nonatomic,性能好,atomic讀取頻率高的時候會致使線程都在排隊,浪費CPU時間。

大概使用者幾種鎖分別對賣票功能進行了性能測試, 性能分別1萬次、100萬次、1000萬次鎖花費的時間對比,單位是秒。(僅供參考,不一樣環境時間略有差別)

鎖類型 1萬次 100萬次 1000萬次
pthread_mutex_t 0.000309 0.027238 0.284714
os_unfair_lock 0.000274 0.028266 0.285685
OSSpinLock 0.030688 0.410067 0.437702
NSCondition 0.005067 0.323492 1.078636
NSLock 0.038692 0.151601 1.322062
NSRecursiveLock 0.007973 0.151601 1.673409
@synchronized 0.008953 0.640234 2.790291
NSConditionLock 0.229148 5.325272 10.681123
semaphore 0.094267 0.415351 24.699100
SerialQueue 0.213386 9.058581 50.820202

建議

平時咱們簡單使用的話沒有很大的區別,仍是推薦使用NSLock和信號量,最簡單的是@synchronized,不用聲明和初始化,直接拿來就用。

自旋鎖、互斥鎖比較

自旋鎖和互斥鎖各有優劣,代碼執行頻率高,CPU充足,可使用互斥鎖,頻率低,代碼複雜則須要互斥鎖。

自旋鎖

  • 自旋鎖在等待時間比較短的時候比較合適
  • 臨界區代碼常常被調用,但競爭不多發生
  • CPU不緊張
  • 多核處理器

互斥鎖

  • 預計線程等待時間比較長
  • 單核處理器
  • 臨界區IO操做
  • 臨界區代碼比較多、複雜,或者循環量大
  • 臨界區競爭很是激烈

鎖的應用

簡單讀寫鎖

一個簡單的讀寫鎖,讀寫互斥便可,咱們使用信號量,值設定爲1.同時只能一個線程來操做文件,讀寫互斥。

- (void)viewDidLoad {
	[super viewDidLoad];
	// Do any additional setup after loading the view.
	self.semaphore = dispatch_semaphore_create(1);
	
	for (NSInteger i = 0; i < 10; i ++) {
		[[[NSThread alloc]initWithTarget:self selector:@selector(read) object:nil]start];
		[[[NSThread alloc]initWithTarget:self selector:@selector(write) object:nil]start];
	}
}

- (void)read{
	dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
	NSLog(@"%s",__func__);
	dispatch_semaphore_signal(self.semaphore);
}
- (void)write{
	dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
	NSLog(@"%s",__func__);
	dispatch_semaphore_signal(self.semaphore);
}
複製代碼

當讀寫都是一個線程來操做,會下降性能,當多個線程在讀資源的時候,其實不須要同步操做的,有讀沒寫,理論上說不用限制異步數量,寫入的時候不能讀,纔是真正限制線程性能的地方,讀寫鎖具有如下特色

  1. 同一時間,只能有1個線程進行寫操做
  2. 同一時間,容許有多個線程進行讀的操做
  3. 同一時間,不容許讀寫操做同時進行

典型的多讀單寫,常常用於文件等數據的讀寫操做,咱們實現2種

讀寫鎖 pthread_rwlock

這是有c語言封裝的讀寫鎖

//初始化讀寫鎖
int pthread_rwlock_init(pthread_rwlock_t * __restrict,
		const pthread_rwlockattr_t * _Nullable __restrict)
//讀上鎖
pthread_rwlock_rdlock(pthread_rwlock_t *)
//嘗試加鎖讀
pthread_rwlock_tryrdlock(pthread_rwlock_t *)
//嘗試加鎖寫
int pthread_rwlock_trywrlock(pthread_rwlock_t *)
//寫入加鎖
pthread_rwlock_wrlock(pthread_rwlock_t *)
//解鎖
pthread_rwlock_unlock(pthread_rwlock_t *)
//銷燬鎖屬性
pthread_rwlockattr_destroy(pthread_rwlockattr_t *)
//銷燬鎖
pthread_rwlock_destroy(pthread_rwlock_t * )
複製代碼

pthread_rwlock_t使用很簡單,只須要在讀以前使用pthread_rwlock_rdlock,讀完解鎖pthread_rwlock_unlock,寫入前須要加鎖pthread_rwlock_wrlock,寫入完成以後解鎖pthread_rwlock_unlock,任務都執行完了能夠選擇銷燬pthread_rwlock_destroy或者等待下次使用。

@property (nonatomic,assign) pthread_rwlock_t rwlock;


- (void)dealloc{
	pthread_rwlock_destroy(&_rwlock);//銷燬鎖
}
//初始化讀寫鎖
pthread_rwlock_init(&_rwlock, NULL);
	
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
	for (NSInteger i = 0; i < 5; i ++) {
		dispatch_async(queue, ^{
			[[[NSThread alloc]initWithTarget:self selector:@selector(readPthreadRWLock) object:nil]start];
			[[[NSThread alloc]initWithTarget:self selector:@selector(writePthreadRWLock) object:nil]start];
		});
	}
	
	
- (void)readPthreadRWLock{
    pthread_rwlock_rdlock(&_rwlock);
    NSLog(@"讀文件");
    sleep(1);
    pthread_rwlock_unlock(&_rwlock);
}
- (void)writePthreadRWLock{
    pthread_rwlock_wrlock(&_rwlock);
    NSLog(@" 寫入文件");
    sleep(1);
    pthread_rwlock_unlock(&_rwlock);
}

//log
2019-07-30 10:47:16 讀文件
2019-07-30 10:47:16 讀文件
2019-07-30 10:47:17 寫入文件
2019-07-30 10:47:18 寫入文件
2019-07-30 10:47:19 讀文件
2019-07-30 10:47:19 讀文件
2019-07-30 10:47:19 讀文件
2019-07-30 10:47:20 寫入文件
2019-07-30 10:47:21 寫入文件
2019-07-30 10:47:22 寫入文件
複製代碼

讀文件會出現同一秒讀屢次,寫文件同一秒只有一個。

異步柵欄調用 dispatch_barrier_async

柵欄你們都見過,爲了分開一個地區而使用的,線程的柵欄函數是分開任務的執行順序

操做 任務 任務 任務
A B
A B
C
C
A
A B

這個函數傳入的併發隊列必須是經過dispatch_queue_create建立,若是傳入的是一個串行的或者全局併發隊列,這個函數便等同於dispatch_async的效果。

//初始化 異步隊列
self.rwqueue = dispatch_queue_create("rw.thread", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (NSInteger i = 0; i < 5; i ++) {
	dispatch_async(queue, ^{
		[self readBarryier];
		[self readBarryier];
		[self readBarryier];
		[self writeBarrier];
	});
}

- (void)readBarryier{
//添加任務到rwqueue
	dispatch_async(self.rwqueue, ^{
		NSLog(@"讀文件 %@",[NSThread currentThread]);
		sleep(1);
	});
}
- (void)writeBarrier{
//barrier_async添加任務到self.rwqueue中
	dispatch_barrier_async(self.rwqueue, ^{
		NSLog(@"寫入文件 %@",[NSThread currentThread]);
		sleep(1);
	});
}

//log

2019-07-30 11:16:53 讀文件 <NSThread: 0x600001ae0740>{number = 9, name = (null)}
2019-07-30 11:16:53 讀文件 <NSThread: 0x600001ae8500>{number = 10, name = (null)}
2019-07-30 11:16:53 讀文件 <NSThread: 0x600001ae8040>{number = 8, name = (null)}
2019-07-30 11:16:53 讀文件 <NSThread: 0x600001ac3a80>{number = 11, name = (null)}
2019-07-30 11:16:54 寫入文件<NSThread: 0x600001ac3a80>{number = 11, name = (null)}
2019-07-30 11:16:55 寫入文件<NSThread: 0x600001ac3a80>{number = 11, name = (null)}
2019-07-30 11:16:56 寫入文件<NSThread: 0x600001ac3a80>{number = 11, name = (null)}
複製代碼

讀文件會出現同一秒讀多個,寫文件同一秒只有一個。

讀寫任務都添加到異步隊列rwqueue中,使用柵欄函數dispatch_barrier_async攔截一下,實現讀寫互斥,讀能夠異步無限讀,寫只能一個同步寫的功能。

總結

  • 普通線程鎖本質就是同步執行
  • atomic原子操做只限制settergetter方法,不限制成員變量
  • 讀寫鎖高性能可使用pthread_rwlock_tdispatch_barrier_async

參考資料

資料下載


最怕一輩子碌碌無爲,還安慰本身平凡難得。

廣告時間

相關文章
相關標籤/搜索