鎖的實(shí)現(xiàn)
操作系統(tǒng)會(huì)給用戶程序提供開鎖,閉鎖的原語操作,那么鎖在操作系統(tǒng)中是怎么實(shí)現(xiàn)的呢?
1.使用中斷啟用與禁止來實(shí)現(xiàn)鎖
中斷禁止:就是禁止打斷,可以將一系列操作變?yōu)樵硬僮?br> 中斷啟用:就是從這里開始,可以被打斷,允許操作系統(tǒng)進(jìn)行調(diào)度
上鎖操作
lock() {
/**中斷禁止*/
while(value != FREE) {
/**中斷啟用*/
//之所以在這里允許被調(diào)度,是因?yàn)樵趩魏藱C(jī)器上運(yùn)行時(shí),
//如果調(diào)用lock()的線程因?yàn)橹袛嘟苟恢鲍@得CPU時(shí)間的話,
//那么獲得鎖的線程根本沒有機(jī)會(huì)運(yùn)行并釋放鎖,所以必須給其他線程插入的機(jī)會(huì)
/**中斷禁止*/
}
value = BUSY;
/**中斷啟用*/
}
解鎖操作
unlock() {
/**中斷禁止*/
value = FREE;//因?yàn)檫@并不是原子操作,所以需要中斷禁止的保護(hù)
/**中斷啟用*/
}
缺點(diǎn):繁忙等待,不可重入(指該實(shí)現(xiàn))
2.使用測試與設(shè)置指令來實(shí)現(xiàn)鎖
測試與設(shè)置(test & set)指令:以不可分割的方式執(zhí)行如下兩個(gè)步驟:
1)設(shè)置操作:將1寫入指定內(nèi)存單元。
2)讀取操作:返回指定內(nèi)存單元里原來的值(寫入1之前的值)。
指令若以程序來實(shí)現(xiàn),則是這樣的:
test_and_set(value) {
tmp = value;
value = 1;
return(tmp);
}
上鎖操作
lock() {
while(test_and_set(value) != 0) {}
}
如果返回值不為0(即不能將鎖從0變?yōu)?),則會(huì)一直循環(huán),繁忙等待鎖的釋放(0)再獲取鎖(1)。當(dāng)能夠?qū)alue從0設(shè)置為1時(shí),跳出循環(huán),調(diào)用lock()的線程進(jìn)入臨界區(qū)。
如果同一個(gè)線程調(diào)用兩次lock(),因?yàn)関alue已經(jīng)為1,所以返回值會(huì)一直返回1,會(huì)無法退出循環(huán),導(dǎo)致死鎖。
解鎖操作
unlock() {
value = 0;
}
缺點(diǎn):繁忙等待,不可重入(指該實(shí)現(xiàn))
3.以非繁忙等待、中斷啟用與禁止來實(shí)現(xiàn)鎖
鎖的實(shí)現(xiàn)思路如下:
1)使用中斷禁止,但不進(jìn)行繁忙等待。
2)如果拿不到鎖,進(jìn)程放棄CPU并進(jìn)入睡眠狀態(tài),以便持有鎖的進(jìn)程可以更好地運(yùn)行。
3)當(dāng)鎖釋放的時(shí)候?qū)⑺X進(jìn)程叫醒。
錯(cuò)誤示例 上鎖操作
lock() {
/**中斷禁止*/
if(value == FREE) {
value = BUSY;
} else {
/**將該線程放進(jìn)等待隊(duì)列,準(zhǔn)備進(jìn)入睡眠*/
/**調(diào)用yield放棄CPU切換到其他線程*/
}
/**中斷啟用*/
}
錯(cuò)誤示例 解鎖操作
unlock() {
/**中斷禁止*/
value = FREE;
if(等待隊(duì)列不為空) {
/**將等待隊(duì)列的其中一個(gè)線程移至就緒隊(duì)列*/
value = BUSY;
}
/**中斷啟用*/
}
上述鎖的實(shí)現(xiàn)程序乍一看似乎很有道理,可仔細(xì)一想?yún)s發(fā)現(xiàn)有問題。
問題是,在上鎖操作中,我們調(diào)用yield切換到別的線程的語句在啟用中斷指令前執(zhí)行,由于切換到另一個(gè)線程后,該線程就無法再執(zhí)行,那么后面的中斷啟用指令自然就不能執(zhí)行了。
而因?yàn)槲覀兪窃谥袛嗵幱诮範(fàn)顟B(tài)下切換到別的線程的,如果別的線程沒有執(zhí)行中斷啟用或者自動(dòng)放棄CPU給另一個(gè)線程,那么該線程將一直占用CPU,其他的線程(包括調(diào)用lock()的線程)將無法得到執(zhí)行。
解決方法:
分析后可以發(fā)現(xiàn),“將自己放在等待隊(duì)列”和“切換到別的線程”這兩個(gè)操作應(yīng)該是一組原子操作,不能在中間中斷。
這也就是說,啟用中斷操作只能在執(zhí)行這兩個(gè)操作之后,而不能在中間。
那么剩下的唯一可能就是閉鎖操作不啟用中斷,而是留給別的線程去啟用中斷。
也就是說,我們要求所有線程遵守下列約定:
?所有線程承諾在調(diào)用線程切換時(shí)將中斷留在禁止?fàn)顟B(tài)。
?所有線程承諾在從切換返回時(shí)(回到自己線程時(shí))將中斷重新啟用。
實(shí)現(xiàn)過程
因此整個(gè)過程如下:
線程A(沒有鎖,調(diào)用lock()) | 線程B(初始獲得鎖) |
---|---|
CPU時(shí)間片結(jié)束 | |
禁止中斷 | |
切換到A | |
啟用中斷 | |
(用戶代碼) | |
請(qǐng)求資源,調(diào)用lock() | |
禁止中斷 | |
檢查鎖的情況(鎖被B持有) | |
進(jìn)入等待隊(duì)列 | |
切換至線程B | |
啟用中斷 | |
(用戶代碼) | |
釋放資源,調(diào)用unlock() | |
從等待隊(duì)列中拿出A | |
將A移到就緒隊(duì)列 | |
CPU時(shí)間片結(jié)束 | |
禁止中斷 | |
切換到A | |
啟用中斷 | |
lock()方法返回 | |
A獲得鎖 | |
進(jìn)入臨界區(qū) |
缺點(diǎn):依賴操作系統(tǒng)。
4.以最少繁忙等待、測試與設(shè)置來實(shí)現(xiàn)鎖
思路:只用繁忙等待來執(zhí)行閉鎖的操作。如果不能獲得就放棄CPU。
上鎖操作
lock() {
while(test_and_set(guard) == 1) {}
if(value == FREE) {
value = BUSY;
guard = 0;
} else {
guard = 0;
/**將自己加入該鎖的等待隊(duì)列,準(zhǔn)備進(jìn)入睡眠*/
/**切換到其他線程*/
}
解鎖操作
unlock() {
while(test_and_set(guard) == 1){}
value = FREE;
if(鎖的等待隊(duì)列不為空) {
/**將等待隊(duì)列中一個(gè)線程移至就緒隊(duì)列*/
value = BUSY;
}
guard = 0;
}
為什么繁忙等待時(shí)間縮短了?
這是因?yàn)槲覀儗⒌却龑?duì)象從value改成了guard。
guard要防止的只是多個(gè)線程同時(shí)拿鎖而已,一旦拿鎖的動(dòng)作完成(不管是否拿到鎖),guard都將被設(shè)置為0。
而這個(gè)范圍很小。在拿到鎖的情況下, guard保護(hù)的只有if(value == FREE)的條件判斷和value=BUSY的賦值語句。在沒有拿到鎖的情況下,guard保護(hù)的語句只有將自己這個(gè)線程加到等待隊(duì)列和切換線程的一段代碼。
該實(shí)現(xiàn)中存在一個(gè)問題。假如在將自己放在等待隊(duì)列后,執(zhí)行切換操作前突然發(fā)生線程切換,那么本線程將同時(shí)處于等待和就緒兩個(gè)隊(duì)列。這個(gè)問題是怎么解決的呢?
解決辦法也很簡單,就是將執(zhí)行l(wèi)ock這個(gè)系統(tǒng)調(diào)用的進(jìn)程的優(yōu)先權(quán)提高,以使這種情況發(fā)生的概率降低。當(dāng)然,完全避免是不可能的。
參考資料:
《操作系統(tǒng)之哲學(xué)原理》 鄒恒明著