鎖
多線程中涩惑,鎖大部分可以分成兩種,互斥鎖與自旋鎖桑驱。
互斥鎖 Mutex
互斥鎖也稱互斥量 竭恬,屬于sleep-waiting類型的鎖,當(dāng)線程訪問被鎖資源時(shí)熬的,調(diào)用者線程會(huì)休眠痊硕,此時(shí)cpu可以調(diào)度其他線程工作。直到被鎖資源釋釋放鎖押框。此時(shí)會(huì)喚醒休眠線程岔绸。互斥鎖的加鎖與解鎖操作回設(shè)計(jì)系統(tǒng)線程的調(diào)度與上下文切換橡伞。自旋鎖 Spinlock
屬于busy-wait 類型的鎖,調(diào)用者線程反復(fù)檢查鎖變量是否可用盒揉。由于線程在這一過程中保持執(zhí)行,因此是一種忙等待兑徘。一旦獲取了自旋鎖刚盈,線程會(huì)一直保持該鎖,直至顯式釋放自旋鎖挂脑。自旋鎖會(huì)比互斥鎖更加消耗CPU藕漱,自旋鎖的效率也會(huì)比互斥鎖更高欲侮。死鎖
兩個(gè)運(yùn)行單元都在等待對(duì)方停止運(yùn)行,以獲取系統(tǒng)資源肋联,但是沒有一方提前退出時(shí)威蕉,就稱為死鎖加鎖與解鎖
加鎖其實(shí)就是獲得鎖,獲得這個(gè)這個(gè)資源的訪問權(quán)限橄仍,解鎖就是釋放鎖韧涨,其他線程就可以訪問。
iOS的鎖
在iOS中沙兰,實(shí)現(xiàn)鎖有多種方式氓奈,一般有
- @synchronized 同步代碼塊
@synchronized(self) {//入?yún)elf為所要保護(hù)的對(duì)象,內(nèi)部其實(shí)是一個(gè)遞歸鎖
// task
}
@synchronized可以很方便就給對(duì)象加鎖鼎天,不用再額外聲明鎖舀奶,手動(dòng)加鎖和釋放鎖。需要保護(hù)的代碼寫在block即可斋射,但是效率較慢育勺。
- NSLock 對(duì)象鎖
NSLock *mutexLock = [[NSLock alloc] init];
[mutexLock lock];
//task
[mutexLock unlock];
NSRecursiveLock 遞歸鎖
NSLock在遞歸中容易產(chǎn)生死鎖,使用NSRecursiveLock可以避免這個(gè)問題罗岖,NSRecursiveLock可以在被同一線程重復(fù)獲取時(shí)不會(huì)產(chǎn)生死鎖涧至。NSConditionLock 條件鎖
滿足預(yù)設(shè)的條件,就可以獲取鎖OSSpinLock 自旋鎖
一般的互斥鎖沒有拿到鎖之前桑包,線程都會(huì)休眠南蓬,而自旋鎖不會(huì)引起調(diào)用者線程休眠,調(diào)用者就一直循環(huán)在那里看是否該自旋鎖的保持者已經(jīng)釋放了鎖哑了。pthread_mutex C語言實(shí)現(xiàn)的互斥鎖
從效率上看赘方,自然是OSSpinLock自旋鎖的效率最高,執(zhí)行耗時(shí)最小弱左,但是OSSpinLock會(huì)出現(xiàn)優(yōu)先級(jí)反轉(zhuǎn)的問題窄陡,目前OSSpinLock的內(nèi)部實(shí)現(xiàn)已經(jīng)被os_unfair_lock代替。等待os_unfair_lock鎖的其他線程會(huì)處于休眠狀態(tài)拆火,而并非忙等跳夭。
需要注意的是,iOS中的鎖目前按照功能來分基本是兩種们镜,自旋鎖和互斥鎖币叹,其他的鎖基本本質(zhì)是互斥鎖,大體都是封裝pthread_mutex而來憎账。
typedef int32_t OSSpinLock OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock);
優(yōu)先級(jí)反轉(zhuǎn)
在 iOS 中,高優(yōu)先級(jí)high priority線程始終會(huì)在低優(yōu)先級(jí)(low priority )線程前執(zhí)行套硼,一個(gè)線程不會(huì)受到比它更低優(yōu)先級(jí)low priority線程的干擾。具體來說胞皱,如果一個(gè)低優(yōu)先級(jí)的線程獲得鎖并訪問共享資源邪意,這時(shí)一個(gè)高優(yōu)先級(jí)high priority的線程也嘗試獲得這個(gè)鎖九妈,它會(huì)處于 spin lock 的忙等狀態(tài)從而占用大量 CPU。此時(shí)低優(yōu)先級(jí)線程無法與高優(yōu)先級(jí)線程爭奪 CPU 時(shí)間片雾鬼,從而導(dǎo)致任務(wù)遲遲完不成萌朱、無法釋放 lock。除非開發(fā)者能保證訪問鎖的線程全部都處于同一優(yōu)先級(jí)策菜,否則 iOS 系統(tǒng)中所有類型的自旋鎖都不能再使用了晶疼。
CPU調(diào)度
一般來說線程是程序執(zhí)行的最小單元,一個(gè)線程包括:獨(dú)有ID又憨,程序計(jì)數(shù)器翠霍,寄存器集合,堆棧蠢莺。同一進(jìn)程可以有多個(gè)線程寒匙,它們共享進(jìn)程的全局變量和堆數(shù)據(jù)。CPU的核心在同一個(gè)時(shí)刻只能執(zhí)行一條線程躏将,如果要執(zhí)行的線程超過CPU的核心數(shù)時(shí)锄弱,就需要線程調(diào)度,簡單來說就是:一個(gè) CPU 核心輪流讓各個(gè)線程分別執(zhí)行一段時(shí)間祸憋。上一點(diǎn)所述会宪,在線程調(diào)度里面,高優(yōu)先級(jí)的線程會(huì)優(yōu)先獲得CPU時(shí)間片蚯窥。線程調(diào)度還有其他算法掸鹅,如FIFO,先排隊(duì)的線程獲得運(yùn)行的CPU時(shí)間片拦赠。CPU調(diào)度使得等待的線程可以運(yùn)行河劝,這樣的切換同時(shí)也會(huì)伴隨著上下文的切換(寄存器數(shù)據(jù)、棧等)矛紫,過多的上下文切換會(huì)帶來資源開銷。
公平鎖與非公平鎖
上述所說替換自旋鎖OSSpinLock
的os_unfair_lock
牌里,其實(shí)是非公平鎖颊咬,平時(shí)使用的鎖基本都是公平鎖,這一類鎖有著FIFO的特性,
多個(gè)線程情況下排隊(duì)牡辽,先到先獲得鎖喳篇。如果進(jìn)入等待的順序?yàn)?2345,則最后等待結(jié)束被執(zhí)行的順序也是12345态辛。但是如果使用的是非公平鎖麸澜,前面的任務(wù)馬上要執(zhí)行完畢,若釋放鎖的時(shí)候奏黑,正好一個(gè)新的線程6來訪問資源炊邦,而此時(shí)位于隊(duì)列頭的線程1還沒有被喚醒(因?yàn)榫€程上下文切換是需要不少開銷的)编矾,此時(shí)后來的線程6則優(yōu)先獲得鎖,成功打破公平馁害,成為非公平鎖窄俏。但是如果線程6來訪問時(shí),鎖不是剛好釋放碘菜,或者線程1已經(jīng)被喚醒凹蜈,線程6還是得進(jìn)入線程隊(duì)列中休眠等待CPU的喚醒執(zhí)行。
信號(hào)量(Semaphore)
信號(hào)量是一個(gè)同步對(duì)象忍啸,用于保持在0至指定最大值之間的一個(gè)計(jì)數(shù)值仰坦。當(dāng)線程完成一次對(duì)該semaphore對(duì)象的等待(wait)時(shí),該計(jì)數(shù)值(- 1)加鎖计雌;當(dāng)線程完成一次對(duì)semaphore對(duì)象的釋放(release)時(shí)悄晃,計(jì)數(shù)值(+ 1)釋放鎖。簡單來說白粉,信號(hào)量為0的時(shí)候传泊,線程會(huì)阻塞,一直等待該信號(hào)量對(duì)象的計(jì)數(shù)值變成大于0的狀態(tài)鸭巴。
如在OC中眷细,
關(guān)于信號(hào)量主要有三個(gè)函數(shù):
-
dispatch_semaphore_create(long value);
創(chuàng)建信號(hào)量,參數(shù)為設(shè)置信號(hào)量的初始值 -
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
發(fā)送當(dāng)前信號(hào)量鹃祖,參數(shù)為當(dāng)前創(chuàng)建的信號(hào)量 -
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
等待信號(hào)量溪椎,第一個(gè)為當(dāng)前等待的信號(hào)量,第二個(gè)參數(shù)為超時(shí)時(shí)間恬口。當(dāng)?shù)却龝r(shí)間超過超時(shí)時(shí)間就不會(huì)繼續(xù)等待了校读。
信號(hào)量是一個(gè)整型值,在創(chuàng)建的時(shí)候會(huì)有一個(gè)初始值祖能。當(dāng)執(zhí)行dispatch_semaphore_signal
發(fā)送信號(hào)的時(shí)候信號(hào)量會(huì)加1歉秫,dispatch_semaphore_wait
在信號(hào)量小于或等于0的時(shí)候會(huì)一直等待,直到超時(shí)养铸,并且會(huì)阻塞該線程雁芙,當(dāng)信號(hào)量大于0時(shí)會(huì)繼續(xù)執(zhí)行并對(duì)信號(hào)量執(zhí)行減1操作。
dispatch_queue_t queue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t lock = dispatch_semaphore_create(0);//信號(hào)量初始為0
dispatch_async(queue, ^{
// task1
dispatch_semaphore_signal(lock);// 使信號(hào)量+1并返回
});
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);//堵塞當(dāng)前線程钞螟,等待task1結(jié)束兔甘,信號(hào)量加1,釋放鎖鳞滨。
// task2
//只有當(dāng)task1執(zhí)行完,信號(hào)量+1之后,才會(huì)執(zhí)行這里
數(shù)值為N的信號(hào)量允許N個(gè)線程并發(fā)訪問洞焙。
如果信號(hào)量是一個(gè)任意的整數(shù),通常被稱為計(jì)數(shù)信號(hào)量(Counting semaphore),或一般信號(hào)量(general semaphore)澡匪;如果信號(hào)量只有二進(jìn)制的0或1熔任,稱為二進(jìn)制信號(hào)量(binary semaphore)。在linux系統(tǒng)中仙蛉,二進(jìn)制信號(hào)量(binary semaphore)又稱互斥量(Mutex)笋敞。
所以說,互斥量和信號(hào)量本質(zhì)上一樣荠瘪,都是用來表示對(duì)資源的訪問權(quán)夯巷,但是互斥量表示資源某個(gè)時(shí)刻最多只能被一個(gè)線程占用,也就是資源計(jì)數(shù)最多是1哀墓,而信號(hào)量的資源計(jì)數(shù)可以超過1趁餐,即同時(shí)被多個(gè)線程占用。
兩者對(duì)比的話篮绰,簡單來說后雷,
- 互斥量就是N個(gè)線程,爭奪一把鎖吠各,
- 信號(hào)量就是N個(gè)線程臀突,爭奪M把鎖,當(dāng)信號(hào)量的計(jì)數(shù)值只有0贾漏、1(二進(jìn)制信號(hào)量)候学,那么M = 1,這時(shí)候纵散,和互斥量的性質(zhì)是一樣的梳码。
信號(hào)量死鎖問題
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self testSemaphore];
}
/// 主線程運(yùn)行
- (void)testSemaphore
{
NSLog(@"1");
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_main_queue(), ^{//主線程
sleep(1);
NSLog(@"2");
dispatch_semaphore_signal(semaphore);
});
NSLog(@"3");
//堵塞當(dāng)前線程
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"4");
}
這時(shí)候輸出
2021-05-15 23:28:12.156395+0800 TestProject[10564:1787212] 1
2021-05-15 23:28:12.156641+0800 TestProject[10564:1787212] 3
就死鎖了,兩個(gè)運(yùn)行單元出現(xiàn)互相等待的情況伍掀。因?yàn)閐ispatch_semaphore_wait卡主了主線程掰茶,而dispatch_async(dispatch_get_main_queue()
又是在主線程中運(yùn)行,需要等里面的block運(yùn)行結(jié)束蜜笤,信號(hào)量+1濒蒋,釋放鎖后,wait才會(huì)結(jié)束把兔。解決這個(gè)問題啊胶,可以把避免在主線程中執(zhí)行Block,或者wait的時(shí)間可以手動(dòng)設(shè)置短一些垛贤。
參考文章
https://zh.wikipedia.org/wiki/%E4%BF%A1%E8%99%9F%E6%A8%99
https://blog.ibireme.com/2016/01/16/spinlock_is_unsafe_in_ios/