iOS底層原理 多執行緒之安全鎖以及常用的讀寫鎖 --(11)

fgyong發表於2019-07-30

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

執行緒安全

執行緒安全是什麼呢?摘抄一段百度百科的一段話

執行緒安全是多執行緒程式設計時的計算機程式程式碼中的一個概念。在擁有共享資料的多條執行緒並行執行的程式中,執行緒安全的程式碼會通過同步機制保證各個執行緒都可以正常且正確的執行,不會出現資料汙染等意外情況。

為什麼需要執行緒安全

ATM肯定用過,你要是邊取錢,邊存錢,會出問題嗎?當你取錢的時候,正在取,結果有人匯款正好到賬,本來1000塊取了100剩下900,結果到賬200,1000+200=1200,因為你取的時候,還沒取完,匯款到賬了結果數字又加上去了。你取的錢跑哪裡去了,這裡就需要取錢的時候不能寫入資料,就是匯款需要在你取錢完成之後再匯款,不能同時進行。

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

自旋鎖 OS_SPINLOCK

什麼是優先順序反轉

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

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

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

其次:A進入就緒狀態,因為優先順序比C高,所以獲得CPU,A轉為執行狀態;C進入就緒狀態;

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

第四:此時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

參考資料

資料下載


最怕一生碌碌無為,還安慰自己平凡可貴。

廣告時間

iOS底層原理 多執行緒之安全鎖以及常用的讀寫鎖 --(11)

相關文章