探索底層原理,積累從點滴做起。大家好,我是Mars。
往期回顧
iOS底層原理探索 — OC對象的本質
iOS底層原理探索 — class的本質
iOS底層原理探索 — KVO的本質
iOS底層原理探索 — KVC的本質
iOS底層原理探索 — Category的本質(一)
iOS底層原理探索 — Category的本質(二)
iOS底層原理探索 — 關聯對象的本質
iOS底層原理探索 — block的本質(一)
iOS底層原理探索 — block的本質(二)
iOS底層原理探索 — Runtime之isa的本質
iOS底層原理探索 — Runtime之class的本質
iOS底層原理探索 — Runtime之消息機制
iOS底層原理探索 — RunLoop的本質
iOS底層原理探索 — RunLoop的應用
iOS底層原理探索 — 多線程的本質
iOS底層原理探索 — 多線程的經典面試題
前言
多線程是iOS
開發中很重要的一個環節,無論是開發過程還是在面試環節中,多線程出現的頻率都非常高。我們會通過幾篇文章的探索,深入淺出的分析多線程技術。
我們通過之前兩篇文章的分析,大致對多線程技術有了一定的了解,認識到了多線程技術的強大。但是多線程在應用過程中存在一定的安全隱患,今天我們繼續分析,來看多線程中如何解決這些問題。
多線程的安全隱患
當多個線程訪問同一塊資源時,很容易引發數據錯亂和數據安全問題。
比如多個線程訪問同一個對象、同一個變量、同一個文件。
具體場景為:存錢取錢、購買車票等。
我們模擬一下賣票的場景。一共有15張票,開設三個窗口賣票,每個窗口賣掉5張,我們來看一下多個線程訪問的時候會出現什么問題:
/** 賣1張票 */
- (void)saleTicket {
int oldTicketsCount = self.ticketsCount;
sleep(.2);
oldTicketsCount--;
self.ticketsCount = oldTicketsCount;
NSLog(@"還剩%d張票 - %@", oldTicketsCount, [NSThread currentThread]);
}
/** 模擬賣票 */
- (void)ticketTest {
self.ticketsCount = 15;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 窗口一
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
// 窗口二
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
// 窗口三
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
}
我們打印一下執行結果:
我們發現,經過15次賣票后,正常來說15張票已經全部賣完才對,但是最后一次打印還剩5張票。這就出現了線程安全問題。
我們再來看下面一個例子:
例如上圖中,一個
integer
類型的對象值為17,當線程A和線程B同時訪問到的時候,線程A做+1操作,同時線程B也做+1操作。
由于兩個線程訪問到integer
類型對象時的值都為17,分別做+1操作后變量的值變為18。但是實際的結果應為做了兩次+1操作,值應該為19。這就出現了問題。
那么多線程同樣提供了解決方案:使用線程同步技術。
線程同步技術
常見的線程同步技術是加鎖
我們再講上面的例子優化:
如上圖,我們應用了加鎖技術后,當線程A訪問和修改變量時,會加鎖,進行+1操作,此時變量值變為18,然后對其解鎖。解鎖后,當線程B訪問和修改變量時,此時變量的值已經為18,再進行+1操作,完成修改。這樣就保證了線程安全。
iOS中的線程同步方案
iOS中為我們提供了一下幾種線程同步方案:
OSSpinLock
—— 自旋鎖os_unfair_lock
pthread_mutex
—— 互斥鎖NSLock
NSRecursiveLock
NSCondition
NSConditionLock
dispatch_queue(DISPATCH_QUEUE_SERIAL)
dispatch_semaphore
—— 信號量@synchronized
接下來我們逐一對以上方案進行分析:
1、OSSpinLock 自旋鎖
叫做自旋鎖,等待鎖的線程會處于忙等(busy-wait)狀態,一直占用著CPU資源。目前已經不再安全,可能會出現優先級反轉問題。在iOS10版本以后就不再支持這一技術。OSSpinLock
如果等待鎖的線程優先級較高,它會一直占用著CPU資源,優先級低的線程就無法釋放鎖
需要導入頭文件#import <libkern/OSAtomic.h>
//初始化鎖
OSSpinLock lock = OS_SPINLOCK_INIT;
//嘗試加鎖(如果需要等待,就不嘗試加鎖,直接返回false,如果不需要等待就加鎖,返回true)
bool result = OSSpinLockTry(&_lock);
// 加鎖
OSSpinLockLock(&_lock);
//解鎖
OSSpinLockUnlock(&_lock);
我們使用自旋鎖對上面賣票的例子進行優化:
#import <libkern/OSAtomic.h>
@property (assign, nonatomic) OSSpinLock lock;
// 初始化鎖
self.lock = OS_SPINLOCK_INIT;
/** 賣1張票 */
- (void)saleTicket {
// 加鎖
OSSpinLockLock(&_lock);
int oldTicketsCount = self.ticketsCount;
sleep(.2);
oldTicketsCount--;
self.ticketsCount = oldTicketsCount;
NSLog(@"還剩%d張票 - %@", oldTicketsCount, [NSThread currentThread]);
// 解鎖
OSSpinLockUnlock(&_lock);
}
通過打印可以看到,加鎖后線程安全,賣票結果也正確。
我們在使用OSSpinLock
自旋鎖時,系統會報出警告,告訴我們不推薦使用OSSpinLockLock
,在iOS 10.0中棄用。并推薦使用<os / lock.h>
中的os_unfair_lock_lock()
來代替。
2、os_unfair_lock
os_unfair_lock
用于取代不安全的OSSpinLock
,從iOS10開始才支持。從底層調用看,等待os_unfair_lock
鎖的線程會處于休眠狀態,并非忙等
需要導入頭文件#import <os/lock.h>
//初始化
os_unfair_lock moneyLock = OS_UNFAIR_LOCK_INIT;
// 嘗試加鎖
os_unfair_lock_trylock(&_ticketLock);
// 加鎖
os_unfair_lock_lock(&_ticketLock);
// 解鎖
os_unfair_lock_unlock(&_ticketLock);
3、pthread_mutex 互斥鎖
mutex叫做互斥鎖,等待鎖的線程會處于休眠狀態
需要導入頭文件#import <pthread.h>
// 初始化屬性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化鎖
pthread_mutex_init(mutex, &attr);
// 嘗試加鎖
pthread_mutex_trylock(&_ticketMutex);
// 加鎖
pthread_mutex_lock(&_ticketMutex);
// 解鎖
pthread_mutex_unlock(&_ticketMutex);
// 銷毀屬性
pthread_mutexattr_destroy(&attr);
互斥鎖的type
有以下幾種:
#define PTHREAD_MUTEX_NORMAL 0 普通鎖
#define PTHREAD_MUTEX_ERRORCHECK 1
#define PTHREAD_MUTEX_RECURSIVE 2 遞歸鎖
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL
pthread_mutex遞歸鎖
// 初始化鎖的屬性
pthread_mutexattr_t attr;
pthread_attr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化鎖
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);
pthread_mutex條件
// 初始化鎖
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
// 初始化條件
pthread_cond_t condition;
pthread_cond_init(&condition, NULL);
// 等待條件(進入休眠,放開mutex鎖;被喚醒后,會再次對mutex加鎖)
pthread_cond_wait(&condition, &mutex);
// 激活一個等待該條件的線程
pthread_cond_signal(&condition);
// 激活所有等待該條件的線程
pthread_cond_broadcast(&condition);
// 銷毀相關資源
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&condition);
4、NSLock
NSLock
是對mutex
普通鎖的封裝
// 初始化鎖
NSLock *lock = [[NSLock alloc] init];
// 嘗試加鎖
[lock tryLock];
// 指定Date之前嘗試加鎖
[lock lockBeforeDate:[NSDate date]];
// 加鎖
[lock lock];
// 解鎖
[lock unlock];
5、NSRecursiveLock
NSRecursiveLock
是對mutex 遞歸鎖
的封裝,API
跟NSLock
基本一致。
// 初始化鎖
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
// 嘗試加鎖
[lock tryLock];
// 指定Date之前嘗試加鎖
[lock lockBeforeDate:[NSDate date]];
// 加鎖
[lock lock];
// 解鎖
[lock unlock];
6、NSCondition
NSCondition
是對mutex
和條件的封裝
// 初始化鎖
NSCondition *condition = [[NSCondition alloc] init];
// 加鎖
[condition lock];
// 等待條件
[condition wait];
// Date之前等待條件
[condition waitUntilDate:[NSDate date]];
// 激活一個等待該條件的線程
[condition signal];
// 激活所有等待該條件的線程
[condition broadcast];
// 解鎖
[condition unlock];
7、NSConditionLock
NSConditionLock
是對NSCondition
的進一步封裝,可以設置具體的條件值。
// 初始化鎖
NSConditionLock *condition = [[NSConditionLock alloc] init];
NSConditionLock *condition = [[NSConditionLock alloc] initWithCondition:1];
// 加鎖 - 可設置條件值
[condition lockWhenCondition:1 beforeDate:[NSDate date]];
[condition lockBeforeDate:[NSDate date]];
[condition lockWhenCondition:1];
[condition lock];
[condition tryLock];
[condition tryLockWhenCondition:1];
// 解鎖
[condition unlockWithCondition:1];
[condition unlock];
8、dispatch_queue(DISPATCH_QUEUE_SERIAL)串行隊列
使用GCD的串行隊列,實現線程同步
dispatch_queue_t queue = dispatch_queue_create("lock_queue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
// 需要執行的任務
});
9、dispatch_semaphore信號量
semaphore
叫做信號量,信號量的初始值,可以用來控制線程并發訪問的最大數量。信號量的初始值為1,代表同時只允許1條線程訪問資源,保證線程同步。
// 信號量的初始值
int value = 1;
// 初始化信號量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(value);
// 如果信號量的值 >0,就 -1,然后往下執行代碼
// 如果信號量的值 <=0,就會休眠等待,直到信號量的值變成 >0
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 信號量的值 +1
dispatch_semaphore_signal(semaphore);
10、@synchronized
@synchronized
是對mutex 遞歸鎖
的封裝。可以查看源碼:objc4
中的objc-sync.mm
文件。
@synchronized(obj)
內部會生成obj
對應的遞歸鎖,然后進行加鎖、解鎖操作。obj
可以是同一個實例對象,類對象,靜態變量
@synchronized(obj) {
//需要執行的任務
}
iOS線程同步方案性能比較
以上幾種鎖的性能從高到低排序:
os_unfair_lock
OSSpinLock
dispatch_semaphore
pthread_mutex
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSCondition
pthread_mutex(recursive)
NSRecursiveLock
NSConditionLock
@synchronized
自旋鎖、互斥鎖比較
什么情況使用自旋鎖
比較劃算?
- 預計線程等待鎖的時間很短
- 加鎖的代碼(臨界區)經常被調用,但競爭情況很少發生
- CPU資源不緊張
- 多核處理器
什么情況使用互斥鎖
比較劃算?
- 預計線程等待鎖的時間較長
- 單核處理器
- 臨界區有IO操作
- 臨界區代碼復雜或者循環量大
- 臨界區競爭非常激烈
我們簡單的介紹了一下多線程中的線程安全問題,以及解決這些問題的線程同步方案,篇幅有限,實際案例會再后續文章中為大家解讀。
更多技術知識請關注公眾號
iOS進階
iOS進階.jpg