多線程技術你們都很瞭解,並且在項目中也比較經常使用。好比開啓一個子線程來處理一些耗時的計算,而後返回主線程刷新UI等。首先咱們先簡單的梳理一下經常使用到的多線程方案。具體的用法這裏我就不說了,每一種方案你們能夠去查一下,網上教程不少。面試
咱們比較經常使用的是GCD和NSOperation,固然還有NSThread,pthread。他們的具體區別咱們不詳細說,給出下面這一個表格,你們自行對比一下。數組
提到多線程,有一個術語是常常能聽到的,同步,異步,串行,併發。安全
同步和異步的區別,就是是否有開啓新的線程的能力。異步具有開啓線程的能力,同步不具有開啓線程的能力。注意,異步只是具有開始新線程的能力,具體開啓與否還要跟隊列的屬性有關係。多線程
串行和併發,是指的任務的執行方式。併發是任務能夠多個同時執行,串行之能是一個執行完成後在執行下一個。併發
在面試的過程當中可能被問到什麼網狀況下會出現死鎖的問題,總結一下就是使用sync函數(同步)往當前的串行對列中添加任務時,會出現死鎖。異步
多線程和安全問題是分不開的,由於在使用多個線程訪問同一塊數據的時候,若是同時有讀寫操做,就可能產生數據安全問題。async
因此這時候咱們就用到了鎖這個東西。函數
其實使用鎖也是爲了在使用多線程的過程當中保障數據安全,除了鎖,而後一些其餘的實現線程同步來保證數據安全的方案,咱們一塊兒來了解一下。atom
下面這些是咱們經常使用來實現線程同步方案的。spa
OSSpinLock os_unfair_lock pthread_mutex NSLock NSRecursiveLock NSCondition NSConditinLock dispatch_semaphore dispatch_queue(DISPATCH_QUEUE_SERIAL) @synchronized
能夠看出來,實現線程同步的方案包括各類鎖,還有信號量,串行隊列。
咱們只挑其中不經常使用的來講一下使用方法。
下面是咱們模擬了存錢取錢的場景,下面是加鎖以前的代碼,運行以後確定是有數據問題的。
/** 存錢、取錢演示 */ - (void)moneyTest { self.money = 100; dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_async(queue, ^{ for (int i = 0; i < 10; i++) { [self __saveMoney]; } }); dispatch_async(queue, ^{ for (int i = 0; i < 10; i++) { [self __drawMoney]; } }); } /** 存錢 */ - (void)__saveMoney { int oldMoney = self.money; sleep(.2); oldMoney += 50; self.money = oldMoney; NSLog(@"存50,還剩%d元 - %@", oldMoney, [NSThread currentThread]); } /** 取錢 */ - (void)__drawMoney { int oldMoney = self.money; sleep(.2); oldMoney -= 20; self.money = oldMoney; NSLog(@"取20,還剩%d元 - %@", oldMoney, [NSThread currentThread]); }
加鎖的代碼,涉及到鎖的初始化、加鎖、解鎖這麼三部分。咱們從OSSpinLock
開始說。
OSSpinLock
叫作自旋鎖。那什麼叫自旋鎖呢?其實咱們能夠從大類上面把鎖分爲兩類,一類是自旋鎖,一類是互斥鎖。咱們經過一個例子來區分這兩類鎖。
若是線程A率先到達加鎖的部分,併成功加鎖,線程B到達的時候會由於已經被A加鎖而等待。若是是自旋鎖,線程B會經過執行一個循環來實現等待,咱們不用管它循環執行了什麼,只要知道他在那"轉圈圈"等着就行。若是是互斥鎖,那線程B在等待的時候會休眠。
使用OSSpinLock
須要導入頭文件#import <libkern/OSAtomic.h>
//聲明一個鎖 @property (nonatomic, assign) OSSpinLock lock; // 鎖的初始化 self.lock = OS_SPINLOCK_INIT;
在咱們這個例子中,存錢取錢都是訪問了money,因此咱們要在存和取的操做中使用同一個鎖。
/** 存錢 */ - (void)__saveMoney { OSSpinLockLock(&_lock); //....省去中間的邏輯代碼 OSSpinLockUnlock(&_lock); } /** 取錢 */ - (void)__drawMoney { OSSpinLockLock(&_lock); //....省去中間的邏輯代碼 OSSpinLockUnlock(&_lock); }
這就是簡單的自旋鎖的使用,咱們發如今使用的過程當中,Xcode一直提醒咱們這個OSSpinLock
被廢棄了,讓咱們使用os_unfair_lock
代替。OSSpinLock
之因此會被廢棄是由於它可能會產生一個優先級反轉的問題。
具體來講,若是一個低優先級的線程得到了鎖並訪問共享資源,那高優先級的線程只能忙等,從而佔用大量的CPU。低優先級的線程沒法和高優先級的線程競爭(CPU會給高優先級的線程分配更多的時間片),因此會致使低優先級的線程的任務一直完不成,從而沒法釋放鎖。
os_unfair_lock
的用法跟OSSpinLock
很像,就不單獨說了。
一看到這個pthread咱們應該就能知道這是一種跨平臺的方案了。首先仍是來看用法。
//聲明一個鎖 @property (nonatomic, assign) pthread_mutex_t lock; //初始化 pthread_mutex_init(pthread_mutex_t *restrict _Nonnull, const pthread_mutexattr_t *restrict _Nullable)
咱們能夠看到在初始化鎖的時候,第一個參數是鎖的地址,第二個參數是一個pthread_mutexattr_t
類型的地址,若是咱們不傳pthread_mutexattr_t
,直接傳一個NULL,至關於建立一個默認的互斥鎖。
//方式一 pthread_mutex_init(mutex, NULL); //方式二 // - 建立attr pthread_mutexattr_t attr; // - 初始化attr pthread_mutexattr_init(&attr); // - 設置attr類型 pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_DEFAULT); // - 使用attr初始化鎖 pthread_mutex_init(&_lock, &attr); // - 銷燬attr pthread_mutexattr_destroy(&attr);
上面兩個方式是一個效果,那爲何使用attr,那就說明除了default類型的還有其餘類型,咱們後面再說。
在使用的時候用pthread_mutex_lock(&_lock);
和 pthread_mutex_unlock(&_lock);
加鎖解鎖。
NSLock就是對這種普通互斥鎖的OC層面的封裝。
調用pthread_mutexattr_settype的時候若是類型傳入PTHREAD_MUTEX_RECURSIVE
,會建立一個遞歸鎖。舉個例子吧。
// 僞代碼 -(void)test { lock; [self test]; unlock; }
若是是普通的鎖,當咱們在test方法中,遞歸調用test,應該會出現死鎖,由於被lock,在遞歸調用時沒法調用,一直等待。可是若是鎖是遞歸鎖,他會容許同一個線程屢次加鎖和解鎖,就能夠解決這個問題了。
NSRecursiveLock是對遞歸鎖的封裝。
咱們直接上這種鎖的使用方法,
- (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_cond_broadcast(&_cond); pthread_mutex_unlock(&_mutex); }
咱們建立了兩個線程,一個往數組中添加數據,一個刪除數據,咱們經過這個條件鎖實現的效果就是在數組中尚未數據的時候等待,數組中添加了一個數據以後在進行刪除。
條件鎖就是互斥鎖+條件。咱們聲明一個條件並初始化。
@property (assign, nonatomic) pthread_cond_t cond; //使用完後也要pthread_cond_destroy(&_cond); pthread_cond_init(&_cond, NULL);
在__remove
方法中
if (self.data.count == 0) { // 等待 pthread_cond_wait(&_cond, &_mutex); }
若是線程1率先拿到所並加鎖,執行到上面代碼這裏發現數組中尚未數據,就執行pthread_cond_wait,此時線程1會暫時放開_mutex這個鎖,並在這休眠等待。
線程2在__add
方法中最開始由於拿不到鎖,因此等待,在線程1休眠放開鎖以後拿到鎖,加鎖,並執行爲數組添加數據的代碼。添加完了以後會發個信號通知等待條件的線程,並解鎖。
pthread_cond_signal(&_cond); pthread_mutex_unlock(&_mutex);
線程2執行了pthread_cond_signal
以後,線程1就收到了通知,退出休眠狀態,繼續執行下面的代碼。
這個地方可能有人會有疑問,是否是線程2應該先unlock再cond_dingnal,其實這個地方順序沒有太大差異,由於線程2執行了pthread_cond_signal
以後,會繼續執行unlock代碼,線程1收到signal通知後會推出休眠狀態,同時線程1須要再一次持有這個鎖,就算此時線程2尚未unlock,線程1等到線程2 unlock 的時間間隔很短,等到線程2 unlock 後線程1會再去持有這個鎖,並加鎖。
NSCondition就是OC層面的條件鎖,內部把mutex互斥鎖和條件封裝到了一塊兒。NSConditionLock其實也差很少,NSConditionLock能夠指定具體的條件,這兩個OC層面的類的用法你們能夠自行上網搜索。
@property (strong, nonatomic) dispatch_semaphore_t semaphore; //初始化 self.semaphore = dispatch_semaphore_create(5);
在初始化一個信號的的過程當中傳入dispatch_semaphore_create
的值,其實就表明了容許幾個線程同時訪問。
再回到以前咱們存錢取錢這個例子。
self.moneySemaphore = dispatch_semaphore_create(1);
咱們一次只容許一個線程訪問,因此在初始化的時候傳1。下面就是使用方法。
- (void)__drawMoney { dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER); // ... 省略代碼 dispatch_semaphore_signal(self.moneySemaphore); } - (void)__saveMoney { dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER); // ... 省略代碼 dispatch_semaphore_signal(self.moneySemaphore); }
dispatch_semaphore_wait
是怎麼上鎖的呢?
若是信號量>0的時候,讓信號量-1,並繼續往下執行。
若是信號量<=0的時候,休眠等待。
就這麼簡單。
dispatch_semaphore_signal
讓信號量+1。
小提示
在咱們平時使用這種方法的時候,能夠把信號量的代碼提取出來定義一個宏。
#define SemaphoreBegin \ static dispatch_semaphore_t semaphore; \ static dispatch_once_t onceToken; \ dispatch_once(&onceToken, ^{ \ semaphore = dispatch_semaphore_create(1); \ }); \ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); #define SemaphoreEnd \ dispatch_semaphore_signal(semaphore);
上面咱們講到的線程同步方案都是每次只容許一個線程訪問,在實際的狀況中,讀寫的同步方案應該下面這樣:
這就是多讀單寫,用於文件讀寫的操做。在咱們的iOS中能夠用下面這兩種解決方案。
這個讀寫鎖的用法很簡單,跟以前的普通互斥鎖都差很少,你們隨便搜一下應該就能搜到,我就不拿出來寫了,這裏主要是提一下這種鎖,你們之後有須要的時候能夠用。
首先在使用這個函數的時候,咱們要用本身建立的併發隊列。
若是傳入的是一個串行隊列或者全局的併發隊列,那dispatch_barrier_async
等同於dispatch_async
的效果。
self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(self.queue, ^{ [self read]; }); dispatch_barrier_async(self.queue, ^{ [self write]; });
在讀取數據的時候,使用dispatch_async
往對列中添加任務,在寫數據時,用dispatch_barrier_async
添加任務。
dispatch_barrier_async
添加的任務會等前面全部的任務都執行完,他再執行,並且他執行的時候,不容許有別的任務同時執行。
咱們都知道這個atomic是原子性的意思。他保證了屬性setter和getter的原子性操做,至關於在set和get方法內部加鎖。
atomic修飾的屬性是讀/寫安全的,但不是線程安全。
假設有一個 atomic 的屬性 "name",若是線程 A 調用 [self setName:@"A"],線程 B 調用 [self setName:@"B"],線程 C 調用 [self name],那麼全部這些不一樣線程上的操做都將依次順序執行——也就是說,若是一個線程正在執行 getter/setter,其餘線程就得等待。所以,屬性 name 是讀/寫安全的。
可是,若是有另外一個線程 D 同時在調[name release],那可能就會crash,由於 release 不受 getter/setter 操做的限制。也就是說,這個屬性只能說是讀/寫安全的,但並非線程安全的,由於別的線程還能進行讀寫以外的其餘操做。線程安全須要開發者本身來保證。