iOS 執行緒安全--鎖
一,前言
執行緒安全是iOS開發中避免了的話題,隨著多執行緒的使用,對於資源的競爭以及資料的操作都可能存在風險,所以有必要在操作時保證執行緒安全。
二,為什麼要使用鎖?
由於一個程序中不可避免的存在多執行緒,所以不可避免的存在多個執行緒訪問同一個資料的情況。但是為了資料的安全性,當一個執行緒訪問資料的時候,其它的執行緒不能對其訪問。簡單來講就是在同一時刻,對同一個資料操作的執行緒只有一個。只有確保了這樣,才能使資料不會被其他執行緒影響。而執行緒不安全,則是在同一時刻可以有多個執行緒對該資料進行訪問,從而得不到預期的結果。例如,一個記憶體單元儲存著一個可讀,可寫的變數資料10,我們想取到10時,另外一個執行緒把它改成11,就會造成我們取到的資料,並不是我們想要的。再比如,寫檔案和讀檔案,當一個執行緒在寫檔案的時候,理論上來說,如果這個時候另一個執行緒來直接讀取的話,那麼得到的結果可能是你無法預料的。
示例:我們定義一個person類,建立一個NSInterge age的屬性,開闢兩個執行緒去改變age的值。
- (void)withoutLock { __block Person *p = [[Person alloc]init]; [NSThread detachNewThreadWithBlock:^{ //開闢一個新執行緒 for (int i = 0; i<1000; i++) { p.age ++; } NSLog(@"%zd\n",p.age); }]; [NSThread detachNewThreadWithBlock:^{ //開闢一個新執行緒 for (int i = 0; i<1000; i++) { p.age ++; } NSLog(@"%zd\n",p.age); }]; }
列印結果:
2018-12-02 00:55:14.691872+0800 Masrony使用方式[19550:2339611] 1893 2018-12-02 00:55:14.691872+0800 Masrony使用方式[19550:2339610] 1012
分析結果:
按正常的理想情況,列印的結果應該為1000,2000; 造成這個問題的主要原因就是我們開闢的兩個執行緒都去訪問age的記憶體單元,造成資料混亂。
假如我們加上鎖以後:
- (void)useLock { __block Person *p = [[Person alloc]init]; NSLock *myLock = [[NSLock alloc]init]; NSLog(@"begin:"); [NSThread detachNewThreadWithBlock:^{ for (int i = 0; i<1000; i++) { [myLock lock]; //加鎖 p.age ++; [myLock unlock]; //解鎖 } NSLog(@"%zd\n",p.age); }]; [NSThread detachNewThreadWithBlock:^{ for (int i = 0; i<1000; i++) { [myLock lock]; //加鎖 p.age ++; [myLock unlock]; //解鎖 } NSLog(@"%zd\n",p.age); }]; }
列印結果:
2018-12-02 00:55:14.691872+0800 Masrony使用方式[19550:2339611] 1000 2018-12-02 00:55:14.691872+0800 Masrony使用方式[19550:2339610] 2000
三,怎麼保證執行緒安全
通常我們使用鎖的機制來保證執行緒安全,即確保同一時刻只有同一個執行緒來對同一個資料來源進行訪問。
四,常用的鎖有哪些
- NSLock
- Synchronized 同步鎖
- Atomic 自旋鎖
- Recursivelock 遞迴鎖
- Dispatch_semaphore 訊號量
- NSConditionLock和NSCondition 條件鎖
五,常用鎖的使用
- NSLock
* 系統API:
@protocol NSLocking lock 方法 - (void)lock //獲得鎖 unlock 方法 - (void)unlock //釋放鎖
@interface NSLock : NSObject <NSLocking> { @private void *_priv; } - (BOOL)tryLock; //試圖得到一個鎖。YES:成功得到鎖;NO:沒有得到鎖。 - (BOOL)lockBeforeDate:(NSDate *)limit; //在指定的時間以前得到鎖。YES:在指定時間之前獲得了鎖;NO:在指定時間之前沒有獲得鎖。該執行緒將被阻塞,直到獲得了鎖,或者指定時間過期。
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); //給鎖定義一個Name @end
* NSLock的執行原理:
某個執行緒A呼叫lock方法。這樣,NSLock將被上鎖。可以執行“關鍵部分”,完成後,呼叫unlock方法。如果,線上程A 呼叫unlock方法之前,另一個執行緒B呼叫了同一鎖物件的lock方法。那麼,執行緒B只有等待。直到執行緒A呼叫了unlock。
* 使用方法
//初始化資料鎖(主執行緒中) NSLock *lock =[NSLock alloc]init]; //資料加鎖 [lock lock];
//加鎖的內容
[object doSomeThine];
//資料解鎖 [lock Unlock];
* 使用示例
//主執行緒中 NSLock *lock = [[NSLock alloc] init]; //執行緒1 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [lock lock]; NSLog(@"執行緒1"); sleep(2); [lock unlock]; NSLog(@"執行緒1解鎖成功"); }); //執行緒2 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ sleep(1);//以保證讓執行緒2的程式碼後執行 [lock lock]; NSLog(@"執行緒2"); [lock unlock]; });
結果:
2018-12-02 14:23:09.659 ThreadLockControlDemo[1754:129663] 執行緒1 2018-12-02 14:23:11.663 ThreadLockControlDemo[1754:129663] 執行緒1解鎖成功 2018-12-02 14:23:11.665 ThreadLockControlDemo[1754:129659] 執行緒2
* 注意事項
Warning
* NSLock類使用POSIX(可移植性作業系統介面)執行緒來實現上鎖的特性。當NSLock類收到一個解鎖的訊息,你必須確定傳送源也是來自那個傳送上鎖的執行緒。在不同的執行緒上解鎖,會產生不定義行為。
* 你不應該把這個類實現遞迴鎖。如果在同一個執行緒上呼叫兩次lock方法,將會對這個執行緒永久上鎖。使用NSRecursiveLock類來才可以實現遞迴鎖。
* 解鎖一個沒有被鎖定的鎖是一個程式錯誤,這個地方需要注意。
- Synchronized 同步鎖
同步鎖是比較常用的,因為其使用方法是所有鎖中最簡單的,但效能卻是最差的,所以對效能要求不高的使用場景Synchronized是一種比較方便的鎖。
* 使用示例:
static Config * instance = nil; //方法A +(Config *) Instance { @synchronized(self) { if(nil == instance) { [self new]; } } return instance; } //方法B +(id)allocWithZone:(NSZone *)zone { @synchronized(self) { if(instance == nil){ instance = [super allocWithZone:zone]; return instance; } } return nil; }
* 使用介紹:
@synchronized,代表這個方法加鎖, 相當於不管哪一個執行緒(例如執行緒A),執行到這個方法時,都要檢查有沒有其它執行緒例如B正在用這個方法,有的話要等正在使用synchronized方法的執行緒B執行完這個方法後再執行此執行緒A,沒有的話,直接執行。它包括兩種用法:synchronized 方法和 synchronized 塊。
@synchronized 方法控制對類(一般在IOS中用在單例中)的訪問:每個類例項對應一把鎖,每個 synchronized 方法都必須獲得呼叫該方法鎖方能執行,否則所屬就會發生執行緒阻塞,方法一旦執行,就獨佔該鎖,直到從該方法返回時才將鎖釋放,此後被阻塞的執行緒方能獲得該鎖,重新進入可執行狀態。這種機制確保了同一時刻對於每一個類,至多隻有一個處於可執行狀態,從而有效避免了類成員變數的訪問衝突(只要所有可能訪問類的方法均被宣告為 synchronized)。
synchronized 塊:
@通過 synchronized關鍵字來宣告synchronized 塊。語法如下:
@synchronized(syncObject) {
}
synchronized 塊是這樣一個程式碼塊,其中的程式碼必須獲得物件 syncObject (如前所述,可以是類例項或類)的鎖方能執行,具體機制同前所述。由於可以針對任意程式碼塊,且可任意指定上鎖的物件,故靈活性較高。
* 使用總結:
- 從上可以看出不需要建立鎖,一種類似於swift中呼叫一個含有尾隨閉包的函式,就能實現功能。
- synchronized內部實現是對傳入的物件,為其分配一個遞迴鎖,儲存在雜湊表中。
* 使用注意:
@synchronized(){} 小括號裡面需要傳入一個物件型別,基本資料型別不能作為引數;
@synchronized(){}小括號內的這個物件不能為空,如果為nil,就不能保證其鎖的功能。
- Atomic 自旋鎖
自旋鎖在iOS系統中的實現是OSSpinLock。自旋鎖通過一直處於while盲等狀態,來實現只有一個執行緒訪問資料。由於一直處於while迴圈,所以對CPU的佔用也比較高的,用CPU的消耗換來的好處就是自旋鎖的效能高。
* 使用介紹:
當上一個執行緒的任務沒有執行完畢的時候(被鎖住),那麼下一個執行緒會一直等待(busy-waiting),當上一個執行緒的任務執行完畢,下一個執行緒會立即執行。
* 優缺點:
1. 由於自旋鎖不會引起呼叫者睡眠,所以自旋鎖的效率遠高於互斥鎖
2. 自旋鎖會一直佔用CPU,也可能會造成死鎖
3.自旋鎖有bug!不同優先順序執行緒排程演算法會有優先順序反轉問題,比如低優先順序獲鎖訪問資源,高優先順序嘗試訪問時會等待,這時低優先順序又沒法爭過高優先順序導致任務無法完成lock釋放不了
* 原子操作
nonatomic
:非原子屬性,非執行緒安全,適合小記憶體移動裝置atomic
:原子屬性,default,執行緒安全(內部使用自旋鎖),消耗大量資源-
單寫多讀,只為setter方法加鎖,不影響getter
-
相關程式碼如下:
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) { if (offset == 0) { object_setClass(self, newValue); return; } id oldValue; id *slot = (id*) ((char*)self + offset); if (copy) { newValue = [newValue copyWithZone:nil]; } else if (mutableCopy) { newValue = [newValue mutableCopyWithZone:nil]; } else { if (*slot == newValue) return; newValue = objc_retain(newValue); } if (!atomic) { oldValue = *slot; *slot = newValue; } else { spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); oldValue = *slot; *slot = newValue; slotlock.unlock(); } objc_release(oldValue); } void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) { bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY); bool mutableCopy = (shouldCopy == MUTABLE_COPY); reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy); }
-
總結:很容易理解的程式碼,可變拷貝和不可變拷貝會開闢新的空間,兩者皆不是則持有(引用計數+1),相比
nonatomic
只是多了一步鎖操作。
* 使用示例
#import "ViewController.h" #import "Person.h" #import <libkern/OSAtomic.h> @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; [self useLock]; } - (void)withoutLock { __block Person *p = [[Person alloc]init]; [NSThread detachNewThreadWithBlock:^{ for (int i = 0; i<1000; i++) { p.age ++; } NSLog(@"%zd\n",p.age); }]; [NSThread detachNewThreadWithBlock:^{ @synchronized(self){ } for (int i = 0; i<1000; i++) { p.age ++; } NSLog(@"%zd\n",p.age); }]; } - (void)useLock { __block OSSpinLock spinLock = OS_SPINLOCK_INIT; //建立鎖 __block Person *p = [[Person alloc]init]; NSLog(@"begin:"); [NSThread detachNewThreadWithBlock:^{ for (int i = 0; i<1000; i++) { OSSpinLockLock(&spinLock); //加鎖 p.age ++; OSSpinLockLock(&spinLock); //解鎖 } NSLog(@"%zd\n",p.age); }]; [NSThread detachNewThreadWithBlock:^{ for (int i = 0; i<1000; i++) { OSSpinLockLock(&spinLock); //加鎖 p.age ++; OSSpinLockLock(&spinLock); //解鎖 } NSLog(@"%zd\n",p.age); }]; }
* 使用總結:
1)首先需要#important<libkern/OSAtomic.h> ,因此關於自旋鎖的API是在這個檔案中宣告的。
2)建立自旋鎖也是通過一個靜態巨集,線上程內通過 OSSpinLockLock 和 OSSpinLockUnlock來上鎖,解鎖。如果不是因為現在的OSSpinLock出現了使用bug,在效能以及使用方面來說,都是很好的使用鎖的選擇。
* 自旋鎖的原理
就是while迴圈來佔用CPU,實際上,當A執行緒獲取到鎖時,CPU會處於while死迴圈,而這個死迴圈並不是A執行緒造成的,當A獲取到鎖,並且B執行緒也要申請鎖時,就會一直while迴圈詢問A執行緒是否釋放了該鎖,所以導致了CPU死迴圈,因此是B執行緒導致的,這個是“自旋”的由來,正是因為這個一直等待詢問,並不類似於互斥鎖,互斥鎖在申請時處於執行緒休眠狀態,所以才使自旋鎖的效能高。舉個列子:煮飯吃,你的電飯鍋(A執行緒)正在煮飯(資源),而你本人(B執行緒)也想煮飯,你有兩種方式,第一種,一直在電飯鍋前等待著,看著飯好了沒;第二種,去忙其它的,每15分鐘過來看一次飯好了沒。很顯然,按照第一種方式肯定是會先吃上飯。
- Recursivelock 遞迴鎖
* 需求場景:
一個鎖只是請求一份資源,而在一些開發實際中,往往需要在程式碼中巢狀鎖的使用,也就是在同一個執行緒中,一個鎖還沒有解鎖就再次加鎖。這個時候就用到了遞迴鎖。
* 實現原理:
遞迴鎖也是通過 pthread_mutex_lock 函式來實現,在函式內部會判斷鎖的型別。NSRecursiveLock 與 NSLock 的區別在於內部封裝的 pthread_mutex_t 物件的型別不同,前者的型別為 PTHREAD_MUTEX_RECURSIVE
* 運用場景:
迴圈(多張圖片迴圈上傳),遞迴
* 使用示例:
示例一:
//遞迴鎖例項化 NSRecursiveLock *lock = [[NSRecursiveLock alloc] init]; static void (^RecursiveMethod)(NSInteger); // 同一執行緒可多次加鎖,不會造成死鎖 RecursiveMethod = ^(NSInteger value){ [lock lock];//一進來就要開始加鎖 [NetWorkManager requestWithMethod:POST Url:url Parameters:paraDic success:^(id responseObject) { [self reuestForSuccess]; //一旦資料獲取成功就要解鎖 不然會造成死鎖 [lock unlock]; } requestRrror:^(id requestRrror) { //條件沒有達到,開始迴圈操作 if(value > 0){ RecursiveMethod(value-1);//必須-1 迴圈 } if(value == 0){ //條件 如果 == 0 代表迴圈的次數條件已經達到 可以做別的操作 } //失敗後也要解鎖 [lock unlock]; }]; //記得解鎖 [lock unlock]; }; //設定遞迴鎖迴圈次數 自定義 RecursiveMethod(5);
示例二:
- (void)recursiveLock { NSRecursiveLock *theLock = [[NSRecursiveLock alloc]init]; [self MyRecursiveFucntion:5 recursiveLock:theLock]; }
- (void) MyRecursiveFucntion:(NSInteger )value recursiveLock:(NSRecursiveLock *)theLock { [theLock lock]; if (value !=0) { --value; [self MyRecursiveFucntion:value recursiveLock:theLock]; } [theLock unlock]; }
- Dispatch_semaphore 訊號量
dispatch_semaphore是GCD用來同步的一種方式,與他相關的共有三個函式,分別是
dispatch_semaphore_create,dispatch_semaphore_signal,dispatch_semaphore_wait。
下面我們逐一介紹三個函式:
(1)dispatch_semaphore_create的宣告為:
dispatch_semaphore_t dispatch_semaphore_create(long value);
傳入的引數為long,輸出一個dispatch_semaphore_t型別且值為value的訊號量。
值得注意的是,這裡的傳入的引數value必須大於或等於0,否則dispatch_semaphore_create會返回NULL。
(關於訊號量,我就不在這裡累述了,網上很多介紹這個的。我們這裡主要講一下dispatch_semaphore這三個函式的用法)。
(2)dispatch_semaphore_signal的宣告為:
long dispatch_semaphore_signal(dispatch_semaphore_t dsema)
這個函式會使傳入的訊號量dsema的值加1;(至於返回值,待會兒再講)
(3) dispatch_semaphore_wait的宣告為:
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
這個函式會使傳入的訊號量dsema的值減1;
這個函式的作用是這樣的,如果dsema訊號量的值大於0,該函式所處執行緒就繼續執行下面的語句,並且將訊號量的值減1;
如果desema的值為0,那麼這個函式就阻塞當前執行緒等待timeout(注意timeout的型別為dispatch_time_t,
不能直接傳入整形或float型數),如果等待的期間desema的值被dispatch_semaphore_signal函式加1了,
且該函式(即dispatch_semaphore_wait)所處執行緒獲得了訊號量,那麼就繼續向下執行並將訊號量減1。
如果等待期間沒有獲取到訊號量或者訊號量的值一直為0,那麼等到timeout時,其所處執行緒自動執行其後語句。
(4)dispatch_semaphore_signal的返回值為long型別,當返回值為0時表示當前並沒有執行緒等待其處理的訊號量,其處理
的訊號量的值加1即可。當返回值不為0時,表示其當前有(一個或多個)執行緒等待其處理的訊號量,並且該函式喚醒了一
個等待的執行緒(當執行緒有優先順序時,喚醒優先順序最高的執行緒;否則隨機喚醒)。
dispatch_semaphore_wait的返回值也為long型。當其返回0時表示在timeout之前,該函式所處的執行緒被成功喚醒。
當其返回不為0時,表示timeout發生。
(5)在設定timeout時,比較有用的兩個巨集:DISPATCH_TIME_NOW 和 DISPATCH_TIME_FOREVER。
DISPATCH_TIME_NOW 表示當前;
DISPATCH_TIME_FOREVER 表示遙遠的未來;
一般可以直接設定timeout為這兩個巨集其中的一個,或者自己建立一個dispatch_time_t型別的變數。
建立dispatch_time_t型別的變數有兩種方法,dispatch_time和dispatch_walltime。
利用建立dispatch_time建立dispatch_time_t型別變數的時候一般也會用到這兩個變數。
dispatch_time的宣告如下:
dispatch_time_t dispatch_time(dispatch_time_t when, int64_t delta);
其引數when需傳入一個dispatch_time_t型別的變數,和一個delta值。表示when加delta時間就是timeout的時間。
例如:dispatch_time_t t = dispatch_time(DISPATCH_TIME_NOW, 1*1000*1000*1000);
表示當前時間向後延時一秒為timeout的時間。
(6)關於訊號量,一般可以用停車來比喻。
停車場剩餘4個車位,那麼即使同時來了四輛車也能停的下。如果此時來了五輛車,那麼就有一輛需要等待。
訊號量的值就相當於剩餘車位的數目,dispatch_semaphore_wait函式就相當於來了一輛車,dispatch_semaphore_signal
就相當於走了一輛車。停車位的剩餘數目在初始化的時候就已經指明瞭(dispatch_semaphore_create(long value)),
呼叫一次dispatch_semaphore_signal,剩餘的車位就增加一個;呼叫一次dispatch_semaphore_wait剩餘車位就減少一個;
當剩餘車位為0時,再來車(即呼叫dispatch_semaphore_wait)就只能等待。有可能同時有幾輛車等待一個停車位。有些車主
沒有耐心,給自己設定了一段等待時間,這段時間內等不到停車位就走了,如果等到了就開進去停車。而有些車主就像把車停在這,
所以就一直等下去。
(7)程式碼舉簡單示例如下:
dispatch_semaphore_t signal;
signal = dispatch_semaphore_create(1);
__block
long
x = 0;
NSLog
(@
"0_x:%ld"
,x);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
NSLog
(@
"waiting"
);
x = dispatch_semaphore_signal(signal);
NSLog
(@
"1_x:%ld"
,x);
sleep(2);
NSLog
(@
"waking"
);
x = dispatch_semaphore_signal(signal);
NSLog
(@
"2_x:%ld"
,x);
});
// dispatch_time_t duration = dispatch_time(DISPATCH_TIME_NOW, 1*1000*1000*1000); //超時1秒
// dispatch_semaphore_wait(signal, duration);
x = dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
NSLog
(@
"3_x:%ld"
,x);
x = dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
NSLog
(@
"wait 2"
);
NSLog
(@
"4_x:%ld"
,x);
x = dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
NSLog
(@
"wait 3"
);
NSLog
(@
"5_x:%ld"
,x);
|
最終列印的結果為:
2018-12-02 22:51:54.734 LHTest[15700:70b] 0_x:0
2018-12-02 22:51:54.737 LHTest[15700:70b] 3_x:0
2018-12-02 22:51:55.738 LHTest[15700:f03] waiting
2018-12-02 22:51:55.739 LHTest[15700:70b] wait 2
2018-12-02 22:51:55.739 LHTest[15700:f03] 1_x:1
2018-12-02 22:51:55.739 LHTest[15700:70b] 4_x:0
2018-12-02 22:51:57.741 LHTest[15700:f03] waking
2018-12-02 22:51:57.742 LHTest[15700:f03] 2_x:1
2018-12-02 22:51:57.742 LHTest[15700:70b] wait 3
2018-12-02 22:51:57.742 LHTest[15700:70b] 5_x:0
|
- NSConditionLock和NSCondition 條件鎖
* 使用介紹:
NSConditionLock好處是可以設定條件,條件符合時獲得鎖。設定時間,指定時間之前獲取鎖。缺點是加鎖和解鎖需要在同一執行緒中執行,否則控制檯會報錯,雖然不影響程式執行。(but好像會影響程序釋放,因為多次執行後進程到了80多,程式卡了還是崩潰了,忘了。只是猜測。)
* 使用舉例:
NSConditionLock * conditionLock = [[NSConditionLockalloc] init]; //當條件符合時獲得鎖 [conditionLock lockWhenCondition:1]; //在指定時間前嘗試獲取鎖,若成功則返回YES 否則返回NO BOOL isLock = [conditionLock lockBeforeDate:date1]; //在指定時間前嘗試獲取鎖,且條件必須符合 BOOL isLock = [conditionLock lockWhenCondition:1 beforeDate:date1]; //解鎖並設定條件為2 [conditionLock unlockWithCondition:2];