多線程在日常開發中會時不時遇到。首先APP會有一個主線程(UI線程),處理一些UI相關的邏輯。但是牽扯到網絡、數據庫等耗時的操作需要新開辟線程處理,避免“卡住”主線程,給用戶留下不好的印象。多線程的好處不言而喻:幕后做事,不影響明面上的事兒。但是也有一些需要注意的地方,其中“資源搶奪”就是需要特別注意的一點。
資源搶奪
所謂資源搶奪就是多個線程同時操作一個數據。
下面這段代碼很簡單,就是往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))")
}
輸出結果毫無疑問是
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))")
}
}
通常會認為 queue1 先輸出 1, 然后 queue2 再輸出 2。 但實際上...
循環打印的結果
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 這時候也運行了, 它在 queue1 的寫入操作沒有完成之前就做了讀取操作。 這時候他們兩個讀到值都是0, 就會造成兩個都輸出1。線程的調度是由操作系統來控制的,如果 queue2 調用的時, queue1 正好寫入完成,這時就能得到正確的輸出結果。 可如果 queue2 調起的時候 queue1 還沒寫入完成,那么就會出現輸出同樣結果的現象。 這一切都是由操作系統來控制。
解決
1、NSLock
NSLock 是 iOS 提供給我們的一個 API 封裝, 可以很好的解決資源搶奪問題。 NSLock 就是對線程加鎖機制的一個封裝
使用示例:
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))")
}
}
}
循環打印的結果
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)
從實現原理上來講,Mutex(互斥鎖)屬于sleep-waiting類型的鎖。例如在一個多核的機器上有兩個線程p1和p2,分別運行在Core1和 Core2上。假設線程p1想要通過pthread_mutex_lock操作去得到一個臨界區(Critical Section)的鎖,而此時這個鎖正被線程p2所持有,那么線程p1就會被阻塞 (blocking),Core1 會在此時進行上下文切換(Context Switch)將線程p1置于等待隊列中,此時Core1就可以運行其他的任務(例如另一個線程p3),而不必進行忙等待。
自旋鎖(Spin lock)
先插個話題:在OC中定義屬性時,很多人會認為如果屬性具備 nonatomic 特質,則不使用 “同步鎖”。其實在屬性設置方法中使用的是自旋鎖。
旋鎖與互斥鎖有點類似,只是自旋鎖不會引起調用者睡眠,如果自旋鎖已經被別的執行單元保持,調用者就一直循環在那里看是 否該自旋鎖的保持者已經釋放了鎖,"自旋"一詞就是因此而得名。其作用是為了解決某項資源的互斥使用。因為自旋鎖不會引起調用者睡眠,所以自旋鎖的效率遠 高于互斥鎖。
雖然它的效率比互斥鎖高,但是它也有些不足之處:
1、自旋鎖一直占用CPU,他在未獲得鎖的情況下,一直運行--自旋,所以占用著CPU,如果不能在很短的時 間內獲得鎖,這無疑會使CPU效率降低。
2、在用自旋鎖時有可能造成死鎖,當遞歸調用時有可能造成死鎖,調用有些其他函數也可能造成死鎖,如 copy_to_user()、copy_from_user()、kmalloc()等。
因此我們要慎重使用自旋鎖,自旋鎖只有在內核可搶占式或SMP的情況下才真正需要,在單CPU且不可搶占式的內核下,自旋鎖的操作為空操作。自旋鎖適用于鎖使用者保持鎖時間比較短的情況下。
總結
這里貼一張ibireme做的測試圖,介紹了一些iOS 中的鎖的API,及其效率
挑幾個我們常用且熟悉的啰嗦幾句
@synchronized (屬:互斥鎖)
顯然,這是我們最熟悉的加鎖方式,因為這是OC層面的為我們封裝的,使用起來簡單粗暴。使用時 @synchronized 后面需要緊跟一個 OC 對象,它實際上是把這個對象當做鎖來使用。這是通過一個哈希表來實現的,OC 在底層使用了一個互斥鎖的數組(也就是鎖池),通過對象的哈希值來得到對應的互斥鎖。
-(void)criticalMethod
{
@synchronized(self)
{
//關鍵代碼;
}
}
NSLock(屬:互斥鎖)
NSLock 是OC 以對象的形式暴露給開發者的一種鎖,它的實現非常簡單,通過宏,定義了 lock 方法:
#define MLOCK - (void) lock{\ int err = pthread_mutex_lock(&_mutex);\ // 錯誤處理 ……}
NSLock只是在內部封裝了一個pthread_mutex,屬性為PTHREAD_MUTEX_ERRORCHECK,它會損失一定性能換來錯誤提示。這里使用宏定義的原因是,OC 內部還有其他幾種鎖,他們的 lock 方法都是一模一樣,僅僅是內部pthread_mutex互斥鎖的類型不同。通過宏定義,可以簡化方法的定義。NSLock比pthread_mutex略慢的原因在于它需要經過方法調用,同時由于緩存的存在,多次方法調用不會對性能產生太大的影響。
atomic原子操作(屬:自旋鎖)
即不可分割開的操作;該操作一定是在同一個cpu時間片中完成,這樣即使線程被切換,多個線程也不會看到同一塊內存中不完整的數據。如果屬性具備 atomic 特質,則在屬性設置方法中使用的是“自旋鎖”。
什么情況下用什么鎖?
1、總的來看,推薦pthread_mutex作為實際項目的首選方案;
2、對于耗時較大又易沖突的讀操作,可以使用讀寫鎖代替pthread_mutex;
3、如果確認僅有set/get的訪問操作,可以選用原子操作屬性;
4、對于性能要求苛刻,可以考慮使用OSSpinLock,需要確保加鎖片段的耗時足夠小;
5、條件鎖基本上使用面向對象的NSCondition和NSConditionLock即可;
6、@synchronized則適用于低頻場景如初始化或者緊急修復使用;