多線程在日常開發(fā)中會時不時遇到。首先APP會有一個主線程(UI線程),處理一些UI相關(guān)的邏輯。但是牽扯到網(wǎng)絡(luò)、數(shù)據(jù)庫等耗時的操作需要新開辟線程處理,避免“卡住”主線程,給用戶留下不好的印象。多線程的好處不言而喻:幕后做事,不影響明面上的事兒。但是也有一些需要注意的地方,其中“資源搶奪”就是需要特別注意的一點(diǎn)。
資源搶奪
所謂資源搶奪就是多個線程同時操作一個數(shù)據(jù)。
下面這段代碼很簡單,就是往Preferences文件中存一個值,并讀取出來輸出
override func viewDidLoad() {
super.viewDidLoad()
// 寫
saveData(key: identifier1, value: 1)
// 讀
let result1 = readData(key: identifier1)
print(" result1: \(String(describing: result1))")
// 寫
saveData(key: identifier2, value: 2)
// 讀
print("result2: \(String(describing: result1))")
}
輸出結(jié)果毫無疑問是
result1: 1
result2: 2
如果這么寫
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// 線程一操作
let queue1 = DispatchQueue(label: "queue1");
queue1.async {[weak self] in
// 寫
self?.saveData(key: identifier, value: 1)
// 讀
let result = self?.readData(key: identifier) ?? ""
print("queue1 result: \(String(describing: result))")
}
// 線程二操作
let queue2 = DispatchQueue(label: "queue2");
queue2.async {[weak self] in
// 寫
self?.saveData(key: identifier, value: 2)
// 讀
let result = self?.readData(key: identifier) ?? ""
print("queue2 result: \(String(describing: result))")
}
}
通常會認(rèn)為 queue1 先輸出 1, 然后 queue2 再輸出 2。 但實(shí)際上...
循環(huán)打印的結(jié)果
queue1 result: 1
queue2 result: 2
queue2 result: 1
queue2 result: 2
queue1 result: 2
queue2 result: 2
queue2 result: 2
queue1 result: 1
剛才代碼中的 queue1要讀取并寫入, 但很有可能 queue2 這時候也運(yùn)行了, 它在 queue1 的寫入操作沒有完成之前就做了讀取操作。 這時候他們兩個讀到值都是0, 就會造成兩個都輸出1。線程的調(diào)度是由操作系統(tǒng)來控制的,如果 queue2 調(diào)用的時, queue1 正好寫入完成,這時就能得到正確的輸出結(jié)果。 可如果 queue2 調(diào)起的時候 queue1 還沒寫入完成,那么就會出現(xiàn)輸出同樣結(jié)果的現(xiàn)象。 這一切都是由操作系統(tǒng)來控制。
解決
1、NSLock
NSLock 是 iOS 提供給我們的一個 API 封裝, 可以很好的解決資源搶奪問題。 NSLock 就是對線程加鎖機(jī)制的一個封裝
使用示例:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let lock = NSLock()
for _ in 0..<100 {
// 線程一操作
let queue1 = DispatchQueue(label: "queue1");
queue1.async {[weak self] in
lock.lock() // 鎖起來
// 寫
self?.saveData(key: identifier, value: 1)
// 讀
let result = self?.readData(key: identifier) ?? ""
lock.unlock() // 解鎖
print("queue1 result: \(String(describing: result))")
}
// 線程二操作
let queue2 = DispatchQueue(label: "queue2");
queue2.async {[weak self] in
lock.lock() // 鎖起來
// 寫
self?.saveData(key: identifier, value: 2)
// 讀
let result = self?.readData(key: identifier) ?? ""
lock.unlock() // 解鎖
print("queue2 result: \(String(describing: result))")
}
}
}
循環(huán)打印的結(jié)果
queue1 result: 1
queue2 result: 2
queue1 result: 1
queue2 result: 2
queue1 result: 2
queue2 result: 2
queue1 result: 1
queue2 result: 2
互斥鎖(pthread_mutex_lock)
從實(shí)現(xiàn)原理上來講,Mutex(互斥鎖)屬于sleep-waiting類型的鎖。例如在一個多核的機(jī)器上有兩個線程p1和p2,分別運(yùn)行在Core1和 Core2上。假設(shè)線程p1想要通過pthread_mutex_lock操作去得到一個臨界區(qū)(Critical Section)的鎖,而此時這個鎖正被線程p2所持有,那么線程p1就會被阻塞 (blocking),Core1 會在此時進(jìn)行上下文切換(Context Switch)將線程p1置于等待隊(duì)列中,此時Core1就可以運(yùn)行其他的任務(wù)(例如另一個線程p3),而不必進(jìn)行忙等待。
自旋鎖(Spin lock)
先插個話題:在OC中定義屬性時,很多人會認(rèn)為如果屬性具備 nonatomic 特質(zhì),則不使用 “同步鎖”。其實(shí)在屬性設(shè)置方法中使用的是自旋鎖。
旋鎖與互斥鎖有點(diǎn)類似,只是自旋鎖不會引起調(diào)用者睡眠,如果自旋鎖已經(jīng)被別的執(zhí)行單元保持,調(diào)用者就一直循環(huán)在那里看是 否該自旋鎖的保持者已經(jīng)釋放了鎖,"自旋"一詞就是因此而得名。其作用是為了解決某項(xiàng)資源的互斥使用。因?yàn)樽孕i不會引起調(diào)用者睡眠,所以自旋鎖的效率遠(yuǎn) 高于互斥鎖。
雖然它的效率比互斥鎖高,但是它也有些不足之處:
1、自旋鎖一直占用CPU,他在未獲得鎖的情況下,一直運(yùn)行--自旋,所以占用著CPU,如果不能在很短的時 間內(nèi)獲得鎖,這無疑會使CPU效率降低。
2、在用自旋鎖時有可能造成死鎖,當(dāng)遞歸調(diào)用時有可能造成死鎖,調(diào)用有些其他函數(shù)也可能造成死鎖,如 copy_to_user()、copy_from_user()、kmalloc()等。
因此我們要慎重使用自旋鎖,自旋鎖只有在內(nèi)核可搶占式或SMP的情況下才真正需要,在單CPU且不可搶占式的內(nèi)核下,自旋鎖的操作為空操作。自旋鎖適用于鎖使用者保持鎖時間比較短的情況下。
總結(jié)
這里貼一張ibireme做的測試圖,介紹了一些iOS 中的鎖的API,及其效率

挑幾個我們常用且熟悉的啰嗦幾句
@synchronized (屬:互斥鎖)
顯然,這是我們最熟悉的加鎖方式,因?yàn)檫@是OC層面的為我們封裝的,使用起來簡單粗暴。使用時 @synchronized 后面需要緊跟一個 OC 對象,它實(shí)際上是把這個對象當(dāng)做鎖來使用。這是通過一個哈希表來實(shí)現(xiàn)的,OC 在底層使用了一個互斥鎖的數(shù)組(也就是鎖池),通過對象的哈希值來得到對應(yīng)的互斥鎖。
-(void)criticalMethod
{
@synchronized(self)
{
//關(guān)鍵代碼;
}
}
NSLock(屬:互斥鎖)
NSLock 是OC 以對象的形式暴露給開發(fā)者的一種鎖,它的實(shí)現(xiàn)非常簡單,通過宏,定義了 lock 方法:
#define MLOCK - (void) lock{\ int err = pthread_mutex_lock(&_mutex);\ // 錯誤處理 ……}
NSLock只是在內(nèi)部封裝了一個pthread_mutex,屬性為PTHREAD_MUTEX_ERRORCHECK,它會損失一定性能換來錯誤提示。這里使用宏定義的原因是,OC 內(nèi)部還有其他幾種鎖,他們的 lock 方法都是一模一樣,僅僅是內(nèi)部pthread_mutex互斥鎖的類型不同。通過宏定義,可以簡化方法的定義。NSLock比pthread_mutex略慢的原因在于它需要經(jīng)過方法調(diào)用,同時由于緩存的存在,多次方法調(diào)用不會對性能產(chǎn)生太大的影響。
atomic原子操作(屬:自旋鎖)
即不可分割開的操作;該操作一定是在同一個cpu時間片中完成,這樣即使線程被切換,多個線程也不會看到同一塊內(nèi)存中不完整的數(shù)據(jù)。如果屬性具備 atomic 特質(zhì),則在屬性設(shè)置方法中使用的是“自旋鎖”。
什么情況下用什么鎖?
1、總的來看,推薦pthread_mutex作為實(shí)際項(xiàng)目的首選方案;
2、對于耗時較大又易沖突的讀操作,可以使用讀寫鎖代替pthread_mutex;
3、如果確認(rèn)僅有set/get的訪問操作,可以選用原子操作屬性;
4、對于性能要求苛刻,可以考慮使用OSSpinLock,需要確保加鎖片段的耗時足夠小;
5、條件鎖基本上使用面向?qū)ο蟮腘SCondition和NSConditionLock即可;
6、@synchronized則適用于低頻場景如初始化或者緊急修復(fù)使用;