多線程在日常開發(fā)中會(huì)時(shí)不時(shí)遇到少欺。首先APP會(huì)有一個(gè)主線程(UI線程),處理一些UI相關(guān)的邏輯欣福。但是牽扯到網(wǎng)絡(luò)、數(shù)據(jù)庫等耗時(shí)的操作需要新開辟線程處理焦履,避免“卡住”主線程拓劝,給用戶留下不好的印象。多線程的好處不言而喻:幕后做事嘉裤,不影響明面上的事兒郑临。但是也有一些需要注意的地方,其中“資源搶奪”就是需要特別注意的一點(diǎn)价脾。
資源搶奪
所謂資源搶奪就是多個(gè)線程同時(shí)操作一個(gè)數(shù)據(jù)牧抵。
下面這段代碼很簡單,就是往Preferences文件中存一個(gè)值侨把,并讀取出來輸出
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))")
}
}
通常會(huì)認(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 這時(shí)候也運(yùn)行了获枝, 它在 queue1 的寫入操作沒有完成之前就做了讀取操作。 這時(shí)候他們兩個(gè)讀到值都是0骇笔, 就會(huì)造成兩個(gè)都輸出1省店。線程的調(diào)度是由操作系統(tǒng)來控制的,如果 queue2 調(diào)用的時(shí)笨触, queue1 正好寫入完成懦傍,這時(shí)就能得到正確的輸出結(jié)果。 可如果 queue2 調(diào)起的時(shí)候 queue1 還沒寫入完成芦劣,那么就會(huì)出現(xiàn)輸出同樣結(jié)果的現(xiàn)象粗俱。 這一切都是由操作系統(tǒng)來控制。
解決
1虚吟、NSLock
NSLock 是 iOS 提供給我們的一個(gè) API 封裝寸认, 可以很好的解決資源搶奪問題。 NSLock 就是對(duì)線程加鎖機(jī)制的一個(gè)封裝
使用示例:
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類型的鎖偏塞。例如在一個(gè)多核的機(jī)器上有兩個(gè)線程p1和p2,分別運(yùn)行在Core1和 Core2上邦鲫。假設(shè)線程p1想要通過pthread_mutex_lock操作去得到一個(gè)臨界區(qū)(Critical Section)的鎖灸叼,而此時(shí)這個(gè)鎖正被線程p2所持有,那么線程p1就會(huì)被阻塞 (blocking),Core1 會(huì)在此時(shí)進(jìn)行上下文切換(Context Switch)將線程p1置于等待隊(duì)列中怜姿,此時(shí)Core1就可以運(yùn)行其他的任務(wù)(例如另一個(gè)線程p3)慎冤,而不必進(jìn)行忙等待。
自旋鎖(Spin lock)
先插個(gè)話題:在OC中定義屬性時(shí)沧卢,很多人會(huì)認(rèn)為如果屬性具備 nonatomic 特質(zhì)蚁堤,則不使用 “同步鎖”。其實(shí)在屬性設(shè)置方法中使用的是自旋鎖但狭。
旋鎖與互斥鎖有點(diǎn)類似披诗,只是自旋鎖不會(huì)引起調(diào)用者睡眠,如果自旋鎖已經(jīng)被別的執(zhí)行單元保持立磁,調(diào)用者就一直循環(huán)在那里看是 否該自旋鎖的保持者已經(jīng)釋放了鎖呈队,"自旋"一詞就是因此而得名。其作用是為了解決某項(xiàng)資源的互斥使用唱歧。因?yàn)樽孕i不會(huì)引起調(diào)用者睡眠宪摧,所以自旋鎖的效率遠(yuǎn) 高于互斥鎖。
雖然它的效率比互斥鎖高颅崩,但是它也有些不足之處:
1几于、自旋鎖一直占用CPU,他在未獲得鎖的情況下沿后,一直運(yùn)行--自旋沿彭,所以占用著CPU,如果不能在很短的時(shí) 間內(nèi)獲得鎖尖滚,這無疑會(huì)使CPU效率降低喉刘。
2、在用自旋鎖時(shí)有可能造成死鎖漆弄,當(dāng)遞歸調(diào)用時(shí)有可能造成死鎖睦裳,調(diào)用有些其他函數(shù)也可能造成死鎖,如 copy_to_user()撼唾、copy_from_user()廉邑、kmalloc()等。
因此我們要慎重使用自旋鎖券坞,自旋鎖只有在內(nèi)核可搶占式或SMP的情況下才真正需要,在單CPU且不可搶占式的內(nèi)核下肺素,自旋鎖的操作為空操作恨锚。自旋鎖適用于鎖使用者保持鎖時(shí)間比較短的情況下。
總結(jié)
這里貼一張ibireme做的測(cè)試圖倍靡,介紹了一些iOS 中的鎖的API猴伶,及其效率
挑幾個(gè)我們常用且熟悉的啰嗦幾句
@synchronized (屬:互斥鎖)
顯然,這是我們最熟悉的加鎖方式,因?yàn)檫@是OC層面的為我們封裝的他挎,使用起來簡單粗暴筝尾。使用時(shí) @synchronized 后面需要緊跟一個(gè) OC 對(duì)象,它實(shí)際上是把這個(gè)對(duì)象當(dāng)做鎖來使用办桨。這是通過一個(gè)哈希表來實(shí)現(xiàn)的筹淫,OC 在底層使用了一個(gè)互斥鎖的數(shù)組(也就是鎖池),通過對(duì)象的哈希值來得到對(duì)應(yīng)的互斥鎖呢撞。
-(void)criticalMethod
{
@synchronized(self)
{
//關(guān)鍵代碼;
}
}
NSLock(屬:互斥鎖)
NSLock 是OC 以對(duì)象的形式暴露給開發(fā)者的一種鎖损姜,它的實(shí)現(xiàn)非常簡單,通過宏殊霞,定義了 lock 方法:
#define MLOCK - (void) lock{\ int err = pthread_mutex_lock(&_mutex);\ // 錯(cuò)誤處理 ……}
NSLock只是在內(nèi)部封裝了一個(gè)pthread_mutex摧阅,屬性為PTHREAD_MUTEX_ERRORCHECK,它會(huì)損失一定性能換來錯(cuò)誤提示绷蹲。這里使用宏定義的原因是棒卷,OC 內(nèi)部還有其他幾種鎖,他們的 lock 方法都是一模一樣祝钢,僅僅是內(nèi)部pthread_mutex互斥鎖的類型不同比规。通過宏定義,可以簡化方法的定義太颤。NSLock比pthread_mutex略慢的原因在于它需要經(jīng)過方法調(diào)用苞俘,同時(shí)由于緩存的存在涝开,多次方法調(diào)用不會(huì)對(duì)性能產(chǎn)生太大的影響港柜。
atomic原子操作(屬:自旋鎖)
即不可分割開的操作;該操作一定是在同一個(gè)cpu時(shí)間片中完成鲫惶,這樣即使線程被切換做裙,多個(gè)線程也不會(huì)看到同一塊內(nèi)存中不完整的數(shù)據(jù)岗憋。如果屬性具備 atomic 特質(zhì),則在屬性設(shè)置方法中使用的是“自旋鎖”锚贱。
什么情況下用什么鎖仔戈?
1、總的來看拧廊,推薦pthread_mutex作為實(shí)際項(xiàng)目的首選方案监徘;
2、對(duì)于耗時(shí)較大又易沖突的讀操作吧碾,可以使用讀寫鎖代替pthread_mutex凰盔;
3、如果確認(rèn)僅有set/get的訪問操作倦春,可以選用原子操作屬性户敬;
4落剪、對(duì)于性能要求苛刻,可以考慮使用OSSpinLock尿庐,需要確保加鎖片段的耗時(shí)足夠兄也馈;
5抄瑟、條件鎖基本上使用面向?qū)ο蟮腘SCondition和NSConditionLock即可凡泣;
6、@synchronized則適用于低頻場(chǎng)景如初始化或者緊急修復(fù)使用锐借;