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];
iOS 線程安全--鎖