B站C++ 一面:如何選擇互斥鎖與自旋鎖,使用場(chǎng)景分別是什么?
做并發(fā)開發(fā)時(shí),你是不是總在互斥鎖和自旋鎖之間糾結(jié)?選對(duì)了鎖,程序性能能提一截;選錯(cuò)了,反而可能埋下 CPU 占用高、響應(yīng)慢的坑 —— 其實(shí)兩者的核心差異,就藏在 “等待方式” 里?;コ怄i是 “躺平等”:線程拿不到鎖時(shí)會(huì)主動(dòng)讓出 CPU,進(jìn)入阻塞狀態(tài),直到鎖釋放再被喚醒。這種 “睡等” 能避免 CPU 空轉(zhuǎn),但切換線程上下文會(huì)有開銷;而自旋鎖是 “站著等”:線程會(huì)循環(huán)檢查鎖是否釋放,全程占用 CPU 不放手,沒有上下文切換成本,可一旦等久了就會(huì)浪費(fèi)算力。
比如處理數(shù)據(jù)庫(kù)查詢、文件讀寫這類耗時(shí)久的臨界區(qū),用互斥鎖更劃算,畢竟線程阻塞時(shí) CPU 能去干別的;但要是只改個(gè)全局計(jì)數(shù)器、更新個(gè)簡(jiǎn)單數(shù)據(jù)結(jié)構(gòu),臨界區(qū)耗時(shí)極短,自旋鎖的 “忙等” 反而比切換線程更快。接下來我們就拆解兩者的底層邏輯、性能差異,再結(jié)合多核 / 單核環(huán)境、臨界區(qū)耗時(shí)等場(chǎng)景,幫你搞懂什么時(shí)候該 “躺平” 用互斥鎖,什么時(shí)候該 “堅(jiān)持” 用自旋鎖。
一、引言:選對(duì)鎖能讓性能起飛?
在多線程編程的世界里,鎖是守護(hù)共享資源的關(guān)鍵衛(wèi)士?;コ怄i(Mutex)和自旋鎖(Spinlock)作為其中的兩大 “護(hù)法”,各有神通,也常常讓開發(fā)者們?cè)谶x擇時(shí)犯難。咱們不妨先來幾個(gè)直擊靈魂的問題:
- 為啥有的鎖在等待時(shí) CPU 使用率居高不下,像個(gè)不知疲倦的 “卷王”;而有的鎖卻能優(yōu)雅地讓出資源,安靜等待?
 - 都說臨界區(qū)代碼短就用自旋鎖,這是絕對(duì)真理嗎?在多核環(huán)境和單核環(huán)境下,情況又有啥不同?
 - 數(shù)據(jù)庫(kù)連接池和計(jì)數(shù)器,看似都是共享資源場(chǎng)景,為啥適合的鎖卻大相徑庭?
 
其實(shí),互斥鎖和自旋鎖的核心差異,就在于線程等待鎖時(shí)的不同策略。自旋鎖采用 “忙等” 策略,線程在獲取不到鎖時(shí),不會(huì)進(jìn)入睡眠狀態(tài),而是在原地不停循環(huán)檢查鎖是否可用,像個(gè)倔強(qiáng)的孩子,不拿到鎖誓不罷休 ,這種方式雖然避免了線程上下文切換的開銷,但會(huì)持續(xù)占用 CPU 資源;互斥鎖則是 “睡等”,當(dāng)線程嘗試獲取鎖失敗時(shí),會(huì)被操作系統(tǒng)掛起,進(jìn)入睡眠狀態(tài),放入等待隊(duì)列,直到鎖被釋放才會(huì)被喚醒,它雖然節(jié)省了 CPU 資源,但線程上下文切換的開銷較大。理解了這一本質(zhì)區(qū)別,就如同掌握了一把萬能鑰匙,能在不同場(chǎng)景下精準(zhǔn)選擇合適的鎖,讓程序性能實(shí)現(xiàn)質(zhì)的飛躍。
二、底層原理:兩種鎖如何 “等待” 鎖釋放?
要深入理解互斥鎖和自旋鎖的差異,就得從它們的底層原理入手。這就好比了解兩個(gè)武林高手的武功根基,只有知曉了這些,才能在不同的 “戰(zhàn)場(chǎng)” 上讓它們發(fā)揮出最大威力。
2.1自旋鎖:CPU 空轉(zhuǎn)的 “執(zhí)著者”
(1)自旋鎖是什么
為了更好地理解自旋鎖,我們不妨先從一個(gè)生活中的場(chǎng)景說起。假設(shè)你在辦公室,大家需要輪流使用一臺(tái)打印機(jī)。當(dāng)你需要打印文件時(shí),卻發(fā)現(xiàn)同事 A 正在使用打印機(jī),這時(shí)你有兩種選擇:
- 阻塞等待:你可以選擇去休息區(qū)喝杯咖啡,等同事 A 使用完打印機(jī)并通知你后,你再去使用。在計(jì)算機(jī)領(lǐng)域,這就類似于線程獲取不到鎖時(shí),進(jìn)入阻塞狀態(tài),讓出 CPU 資源,等待被喚醒。
 - 自旋等待:你也可以選擇站在打印機(jī)旁邊,每隔一會(huì)兒就問一下同事 A 是否使用完畢。一旦同事 A 用完,你立刻就可以使用打印機(jī)。這就是自旋鎖的思想 —— 線程在獲取不到鎖時(shí),并不進(jìn)入阻塞狀態(tài),而是不斷地嘗試獲取鎖 ,就像在原地 “自旋” 一樣。
 
在多線程編程中,當(dāng)多個(gè)線程同時(shí)訪問共享資源時(shí),為了保證數(shù)據(jù)的一致性和完整性,我們需要引入同步機(jī)制。自旋鎖就是其中一種常用的同步機(jī)制,它通過讓線程在等待鎖的過程中 “忙等待”(busy - waiting),即不斷地循環(huán)檢查鎖的狀態(tài),而不是立即進(jìn)入阻塞狀態(tài),來實(shí)現(xiàn)多線程對(duì)共享資源的安全訪問。
(2)自旋鎖工作機(jī)制
自旋鎖的設(shè)計(jì)非常獨(dú)特,當(dāng)線程嘗試獲取一個(gè)已經(jīng)被其他線程持有的自旋鎖時(shí),它不會(huì)乖乖地進(jìn)入睡眠狀態(tài)等待,而是進(jìn)入一種 “忙等待”(Busy-Waiting)的循環(huán) 。在這個(gè)循環(huán)里,線程會(huì)持續(xù)不斷地檢查鎖的狀態(tài),就像一個(gè)執(zhí)著的守望者,始終盯著鎖是否被釋放,一旦發(fā)現(xiàn)鎖被釋放,它便能立即獲取鎖并繼續(xù)執(zhí)行任務(wù)。這種 “不放棄、不等待” 的策略,避免了線程上下文切換的開銷,因?yàn)榫€程無需從運(yùn)行狀態(tài)切換到睡眠狀態(tài),再?gòu)乃郀顟B(tài)被喚醒回到運(yùn)行狀態(tài)。
①獲取鎖:搶占先機(jī)的第一步
當(dāng)一個(gè)線程嘗試獲取自旋鎖時(shí),它首先會(huì)檢查鎖的狀態(tài)。這就好比你去圖書館借一本熱門書籍,你得先看看這本書是否在書架上(鎖是否空閑) 。如果鎖當(dāng)前處于 “空閑” 狀態(tài),也就是說沒有其他線程持有這把鎖,那么該線程就可以幸運(yùn)地立即占有這把鎖,然后就可以放心地去訪問共享資源,繼續(xù)執(zhí)行后續(xù)的任務(wù)了。這個(gè)過程就像是你發(fā)現(xiàn)那本熱門書籍剛好在書架上,你直接拿起來就可以閱讀了。
在實(shí)際的代碼實(shí)現(xiàn)中,通常會(huì)使用一個(gè)原子變量來表示鎖的狀態(tài)。例如在 C++ 中,可以使用std::atomic_flag來實(shí)現(xiàn)自旋鎖:
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
class SpinLock {
private:
    std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
    void lock() {
        while (flag.test_and_set(std::memory_order_acquire)) {
            // 自旋等待鎖釋放
        }
    }
    void unlock() {
        flag.clear(std::memory_order_release);
    }
};在上述代碼中,lock方法通過test_and_set方法來嘗試獲取鎖,如果鎖空閑(flag初始為false),則test_and_set會(huì)將flag設(shè)置為true并返回false,線程成功獲取鎖;如果鎖已被占用(flag為true),test_and_set返回true,線程進(jìn)入循環(huán)等待。
②自旋等待:執(zhí)著的等待策略
要是鎖已經(jīng)被其他線程占用了,當(dāng)前線程并不會(huì)像使用普通鎖那樣乖乖地進(jìn)入阻塞狀態(tài),把 CPU 資源讓給其他線程。相反,它會(huì)進(jìn)入一個(gè)循環(huán),在這個(gè)循環(huán)里不斷地檢查鎖的狀態(tài),這個(gè)過程就是 “自旋”。線程就像一個(gè)執(zhí)著的守望者,死死地盯著鎖的狀態(tài),一直等待著鎖被釋放的那一刻。就好像你去圖書館借那本熱門書籍,發(fā)現(xiàn)已經(jīng)被別人借走了,你不離開圖書館,而是每隔一會(huì)兒就去服務(wù)臺(tái)問一下書是否被還回來了,一旦書被還回來,你就能第一時(shí)間借到。
在自旋等待過程中,線程會(huì)持續(xù)占用 CPU 資源,不斷地執(zhí)行循環(huán)中的指令,這也就是為什么自旋鎖會(huì)浪費(fèi) CPU 資源的原因。不過,如果鎖被占用的時(shí)間很短,那么這種自旋等待的方式就比線程阻塞再喚醒的方式更高效,因?yàn)榫€程阻塞和喚醒需要操作系統(tǒng)內(nèi)核的參與,會(huì)帶來一定的開銷 。
③釋放鎖:開啟新的競(jìng)爭(zhēng)
當(dāng)持有鎖的線程完成了對(duì)共享資源的操作后,就會(huì)釋放這把鎖。這就好比你在圖書館看完那本熱門書籍后,把它放回了書架。此時(shí),那些正在自旋等待的線程就像聞到血腥味的鯊魚,會(huì)立即檢測(cè)到鎖狀態(tài)的變化,其中一個(gè)線程會(huì)迅速獲取到這把鎖,開始執(zhí)行自己的任務(wù)。在這個(gè)過程中,多個(gè)自旋等待的線程會(huì)競(jìng)爭(zhēng)獲取鎖,就像有很多人都在等著借那本熱門書籍,誰先發(fā)現(xiàn)書被還回來,誰就能先借到。
在代碼實(shí)現(xiàn)中,釋放鎖的操作相對(duì)簡(jiǎn)單。還是以上面的 C++ 代碼為例,unlock方法通過clear方法將flag設(shè)置為false,表示鎖已被釋放,其他線程可以嘗試獲?。?/span>
void unlock() {
    flag.clear(std::memory_order_release);
}通過獲取鎖、自旋等待和釋放鎖這三個(gè)步驟,自旋鎖實(shí)現(xiàn)了多線程對(duì)共享資源的安全訪問 。
自旋鎖的實(shí)現(xiàn)高度依賴原子操作 ,以確保線程安全。像test_and_set這樣的原子操作,能保證鎖狀態(tài)的原子性讀取和修改。下面這段 C++ 代碼展示了自旋鎖的基本實(shí)現(xiàn):
#include <atomic>
class Spinlock {
private:
    std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
    void lock() {
        // test_and_set:測(cè)試并設(shè)置標(biāo)志位
        // 如果標(biāo)志位是 false,則將其設(shè)置為 true 并返回 false
        // 如果標(biāo)志位是 true,則不做任何事并返回 true
        // 當(dāng)循環(huán)退出時(shí),表示成功獲取了鎖
        while (flag.test_and_set(std::memory_order_acquire)) {
            // 忙等待,不斷檢查鎖狀態(tài)
        }
    }
    void unlock() {
        // 清除標(biāo)志位,表示鎖已釋放
        flag.clear(std::memory_order_release);
    }
};在這段代碼中,std::atomic_flag類型的flag用于表示鎖的狀態(tài)。lock函數(shù)通過test_and_set方法嘗試獲取鎖,如果flag為false,則設(shè)置為true并返回false,表示獲取鎖成功,循環(huán)結(jié)束;如果flag為true,則返回true,表示鎖已被占用,線程繼續(xù)在循環(huán)中自旋等待。unlock函數(shù)則通過clear方法將flag設(shè)置為false,釋放鎖。
自旋鎖的優(yōu)點(diǎn)顯而易見,它的響應(yīng)速度極快。由于線程在等待鎖的過程中不會(huì)被阻塞,一旦鎖被釋放,就能立刻被獲取,不存在線程喚醒的延遲。這使得它在鎖持有時(shí)間極短的場(chǎng)景中表現(xiàn)出色,比如對(duì)一個(gè)計(jì)數(shù)器進(jìn)行原子增減操作,因?yàn)榕R界區(qū)代碼執(zhí)行速度極快,自旋等待的時(shí)間可能比線程上下文切換的開銷還要小,所以能顯著提高性能。
然而,自旋鎖也有明顯的缺點(diǎn)。它會(huì)持續(xù)消耗 CPU 資源 ,因?yàn)榫€程在自旋等待時(shí),CPU 一直在執(zhí)行循環(huán)檢查鎖狀態(tài)的指令,相當(dāng)于在 “空轉(zhuǎn)”,這就如同發(fā)動(dòng)機(jī)一直運(yùn)轉(zhuǎn)卻沒有實(shí)際做功,白白浪費(fèi)了能源。如果鎖被持有的時(shí)間較長(zhǎng),自旋的線程會(huì)一直占用 CPU,導(dǎo)致其他線程無法獲得足夠的 CPU 時(shí)間來執(zhí)行任務(wù),甚至可能出現(xiàn) “餓死” 的情況,即某些線程長(zhǎng)時(shí)間無法獲取到 CPU 資源,從而無法執(zhí)行。
2.2互斥鎖:讓出 CPU 的 “佛系等待者”
互斥鎖則是另一種風(fēng)格,當(dāng)一個(gè)線程嘗試獲取一個(gè)已經(jīng)被其他線程持有的互斥鎖時(shí),它會(huì)主動(dòng) “示弱”,選擇讓出 CPU 資源,進(jìn)入睡眠狀態(tài) 。操作系統(tǒng)會(huì)將這個(gè)線程掛起,并把它放入等待隊(duì)列中,此時(shí)該線程不再參與 CPU 的調(diào)度,也就不會(huì)消耗 CPU 時(shí)間。只有當(dāng)持有鎖的線程完成對(duì)共享資源的訪問,釋放互斥鎖后,操作系統(tǒng)才會(huì)從等待隊(duì)列中選擇一個(gè)線程喚醒,將其狀態(tài)設(shè)置為就緒狀態(tài),等待 CPU 調(diào)度執(zhí)行。這個(gè)過程涉及到線程上下文的切換,從用戶態(tài)切換到內(nèi)核態(tài),再?gòu)膬?nèi)核態(tài)切換回用戶態(tài),開銷相對(duì)較大。
(1)互斥鎖是什么
互斥鎖,即 Mutex,是英文 Mutual Exclusion 的縮寫,直譯為 “相互排斥” ,它是一種在多線程編程中至關(guān)重要的同步原語。在多線程環(huán)境下,當(dāng)多個(gè)線程同時(shí)訪問和修改共享資源時(shí),就可能會(huì)出現(xiàn)數(shù)據(jù)競(jìng)爭(zhēng)問題,導(dǎo)致程序出現(xiàn)不可預(yù)測(cè)的行為。例如,在一個(gè)銀行賬戶轉(zhuǎn)賬的場(chǎng)景中,如果有多個(gè)線程同時(shí)對(duì)賬戶余額進(jìn)行操作,可能會(huì)導(dǎo)致余額計(jì)算錯(cuò)誤,出現(xiàn)重復(fù)扣款或多扣款的情況。
而互斥鎖的作用,就是為了避免這種數(shù)據(jù)競(jìng)爭(zhēng),確保在同一時(shí)刻,只有一個(gè)線程能夠訪問被保護(hù)的共享資源,就像給共享資源加上了一把鎖,當(dāng)一個(gè)線程拿到這把鎖并進(jìn)入臨界區(qū)(訪問共享資源的代碼區(qū)域)時(shí),其他線程必須等待,直到該線程釋放鎖,其他線程才有機(jī)會(huì)獲取鎖并進(jìn)入臨界區(qū)。 它就像是一個(gè)交通警察,在多線程的 “道路” 上指揮著對(duì)共享資源的訪問,保證秩序井然,避免混亂和沖突。
(2)互斥鎖的工作原理
互斥鎖的工作原理基于操作系統(tǒng)提供的原子操作和線程調(diào)度機(jī)制。當(dāng)一個(gè)線程執(zhí)行到需要訪問共享資源的代碼段時(shí),它會(huì)調(diào)用互斥鎖的加鎖函數(shù)(如std::mutex的lock方法)。此時(shí),互斥鎖會(huì)檢查自身的狀態(tài),如果當(dāng)前處于未鎖定狀態(tài),它會(huì)將自己標(biāo)記為已鎖定,并允許該線程進(jìn)入臨界區(qū)訪問共享資源。這個(gè)標(biāo)記過程是通過原子操作實(shí)現(xiàn)的,確保在多線程環(huán)境下不會(huì)出現(xiàn)競(jìng)爭(zhēng)條件。例如,在一個(gè)多線程的文件讀寫操作中,當(dāng)一個(gè)線程獲取到互斥鎖后,就可以安全地對(duì)文件進(jìn)行寫入,避免其他線程同時(shí)寫入導(dǎo)致文件內(nèi)容混亂。
如果互斥鎖已經(jīng)被其他線程鎖定,那么調(diào)用加鎖函數(shù)的線程會(huì)被操作系統(tǒng)掛起,放入等待隊(duì)列中,進(jìn)入阻塞狀態(tài)。此時(shí),該線程會(huì)讓出 CPU 資源,以便其他線程能夠繼續(xù)執(zhí)行,避免了無效的 CPU 占用。就像在一條單行道上,當(dāng)一輛車已經(jīng)在行駛時(shí),其他車輛只能在路口等待,直到前面的車通過。
當(dāng)持有鎖的線程完成對(duì)共享資源的訪問后,它會(huì)調(diào)用互斥鎖的解鎖函數(shù)(如std::mutex的unlock方法) 。解鎖操作會(huì)將互斥鎖的狀態(tài)標(biāo)記為未鎖定,并從等待隊(duì)列中喚醒一個(gè)等待的線程(如果有線程在等待)。被喚醒的線程會(huì)重新競(jìng)爭(zhēng) CPU 資源,當(dāng)它獲得 CPU 時(shí)間片后,會(huì)再次嘗試獲取互斥鎖。一旦獲取成功,就可以進(jìn)入臨界區(qū)訪問共享資源。例如,在一個(gè)多線程的數(shù)據(jù)庫(kù)操作中,當(dāng)一個(gè)線程完成對(duì)數(shù)據(jù)庫(kù)的更新操作并釋放互斥鎖后,等待隊(duì)列中的另一個(gè)線程就有機(jī)會(huì)獲取鎖,進(jìn)行查詢或其他操作。
互斥鎖的實(shí)現(xiàn)依賴于操作系統(tǒng)的線程調(diào)度機(jī)制 ,通常會(huì)涉及內(nèi)核態(tài)上下文切換。在 C++ 中,使用標(biāo)準(zhǔn)庫(kù)的std::mutex可以很方便地實(shí)現(xiàn)互斥鎖,示例代碼如下:
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
std::mutex mtx;
long long counter = 0;
void task() {
    for (int i = 0; i < 100000; ++i) {
        // std::lock_guard 在構(gòu)造時(shí)加鎖,析構(gòu)時(shí)自動(dòng)解鎖,避免忘記解鎖
        std::lock_guard<std::mutex> lock(mtx);
        counter++;
    }
}
int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(task);
    }
    for (auto& t : threads) {
        t.join();
    }
    std::cout << "Counter: " << counter << std::endl;
    return 0;
}在這段代碼中,std::mutex類型的mtx就是互斥鎖。std::lock_guard是一個(gè) RAII(Resource Acquisition Is Initialization)類,在其構(gòu)造函數(shù)中會(huì)自動(dòng)調(diào)用mtx.lock()加鎖,在析構(gòu)函數(shù)中會(huì)自動(dòng)調(diào)用mtx.unlock()解鎖,這樣可以確保在離開作用域時(shí)鎖一定會(huì)被釋放,避免了手動(dòng)解鎖可能出現(xiàn)的遺漏。
互斥鎖的優(yōu)點(diǎn)在于它不會(huì)浪費(fèi) CPU 資源 ,因?yàn)榈却i的線程會(huì)進(jìn)入睡眠狀態(tài),不會(huì)占用 CPU 進(jìn)行無效的操作。這使得它非常適合鎖持有時(shí)間較長(zhǎng)、臨界區(qū)代碼邏輯復(fù)雜或線程競(jìng)爭(zhēng)激烈的場(chǎng)景。比如在數(shù)據(jù)庫(kù)連接池的實(shí)現(xiàn)中,線程獲取數(shù)據(jù)庫(kù)連接可能需要進(jìn)行一系列復(fù)雜的操作,包括從連接池中查找可用連接、驗(yàn)證連接狀態(tài)等,這個(gè)過程可能需要較長(zhǎng)時(shí)間,使用互斥鎖可以讓其他線程在等待時(shí)釋放 CPU 資源,提高系統(tǒng)整體的資源利用率。
但是,互斥鎖也存在性能開銷大的問題 。線程的掛起和喚醒涉及到兩次上下文切換(用戶態(tài) -> 內(nèi)核態(tài) -> 用戶態(tài)),這個(gè)過程需要保存和恢復(fù)線程的寄存器狀態(tài)、程序計(jì)數(shù)器等信息,會(huì)帶來一定的時(shí)間開銷。如果鎖持有時(shí)間很短,上下文切換的開銷可能會(huì)超過臨界區(qū)代碼的執(zhí)行時(shí)間,反而降低了程序的性能。此外,鎖釋放后,被喚醒的線程需要經(jīng)過操作系統(tǒng)的調(diào)度才能重新執(zhí)行,這中間存在一定的延遲,對(duì)于一些對(duì)響應(yīng)時(shí)間要求極高的場(chǎng)景來說,可能無法滿足需求。
三、適用場(chǎng)景深度拆解:5 個(gè)維度決定選誰
在實(shí)際的編程世界里,選擇互斥鎖還是自旋鎖,就像一場(chǎng)精密的棋局,需要綜合考慮多個(gè)維度的因素。接下來,我們就從五個(gè)關(guān)鍵維度,深度剖析它們的適用場(chǎng)景。
3.1臨界區(qū)耗時(shí):鎖的持有時(shí)間是核心指標(biāo)
自旋鎖優(yōu)先:當(dāng)臨界區(qū)的執(zhí)行時(shí)間極短,比如只是簡(jiǎn)單地修改一個(gè)全局計(jì)數(shù)器,或者進(jìn)行一些簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu)操作,這種情況下,自旋鎖是更好的選擇。因?yàn)樽孕却牡?CPU 時(shí)間,可能比線程上下文切換的開銷還要小。以高性能網(wǎng)絡(luò)服務(wù)器為例,它需要對(duì)處理的請(qǐng)求數(shù)進(jìn)行計(jì)數(shù)統(tǒng)計(jì),每次加鎖操作僅僅是遞增一個(gè)atomic_int,這個(gè)過程非常迅速,使用自旋鎖進(jìn)行忙等待,幾乎不會(huì)被感知到,卻能避免線程上下文切換帶來的開銷,大大提高了系統(tǒng)的性能。
互斥鎖優(yōu)先:然而,如果臨界區(qū)包含了 I/O 操作,像數(shù)據(jù)庫(kù)查詢、文件讀寫這類耗時(shí)較長(zhǎng)的任務(wù),或者包含復(fù)雜的計(jì)算邏輯、大規(guī)模的循環(huán)操作,此時(shí)互斥鎖則更為合適。因?yàn)樵谶@些情況下,線程如果自旋等待,會(huì)白白消耗 CPU 資源,而互斥鎖可以讓線程阻塞,釋放 CPU 資源給其他線程使用,從而提高整個(gè)系統(tǒng)的資源利用率。例如,在數(shù)據(jù)庫(kù)連接池的連接分配場(chǎng)景中,獲取連接時(shí)可能涉及到磁盤 I/O 或網(wǎng)絡(luò)通信,這些操作的耗時(shí)是不確定的,且通常較長(zhǎng),使用互斥鎖讓線程阻塞等待,能避免 CPU 資源的浪費(fèi),使得系統(tǒng)資源得到更合理的分配。
3.2系統(tǒng)架構(gòu):多核 vs 單核的不同選擇
多核環(huán)境(推薦自旋鎖):在多核環(huán)境下,不同的線程可以在不同的 CPU 核心上并行執(zhí)行。這就為自旋鎖提供了施展拳腳的舞臺(tái),當(dāng)一個(gè)線程持有鎖在 A 核心上運(yùn)行時(shí),等待鎖的線程可以在 B 核心上自旋,它們之間互不干擾,能夠充分利用多核的并行性。這種情況下,自旋鎖的忙等待不會(huì)影響其他線程的執(zhí)行,反而因?yàn)楸苊饬松舷挛那袚Q的開銷,提高了系統(tǒng)的整體性能。比如在多線程的圖像渲染任務(wù)中,每個(gè)線程負(fù)責(zé)處理圖像的一部分,當(dāng)線程需要訪問共享的顏色表等資源時(shí),使用自旋鎖可以讓線程在不同核心上高效地進(jìn)行自旋等待,充分發(fā)揮多核處理器的優(yōu)勢(shì)。
單核環(huán)境(必選互斥鎖):但在單核環(huán)境中,情況就截然不同了。自旋鎖的忙等策略會(huì)導(dǎo)致 CPU 被單一線程獨(dú)占,其他線程無法獲得 CPU 時(shí)間片來執(zhí)行任務(wù),這樣不僅無法提高效率,反而會(huì)降低系統(tǒng)的整體性能。相比之下,互斥鎖的阻塞調(diào)度機(jī)制能夠讓線程在等待鎖時(shí)釋放 CPU 資源,使得操作系統(tǒng)可以調(diào)度其他線程執(zhí)行,從而更有效地利用單核 CPU 的資源。就像在一個(gè)簡(jiǎn)單的桌面應(yīng)用程序中,如果使用自旋鎖,可能會(huì)導(dǎo)致界面卡頓,響應(yīng)遲緩,而使用互斥鎖則能保證程序的流暢運(yùn)行。
3.3鎖競(jìng)爭(zhēng)激烈程度
低競(jìng)爭(zhēng)場(chǎng)景(自旋鎖更優(yōu)):在低競(jìng)爭(zhēng)場(chǎng)景下,多數(shù)情況下線程能夠快速獲取鎖,偶爾出現(xiàn)的自旋等待時(shí)間也很短。這種場(chǎng)景非常適合自旋鎖,因?yàn)樗母哳l次、短時(shí)間的鎖操作特性能夠得到充分發(fā)揮。例如,在一個(gè)多線程的緩存管理系統(tǒng)中,各個(gè)線程對(duì)緩存的訪問頻率很高,但由于緩存的設(shè)計(jì)合理,競(jìng)爭(zhēng)沖突很少發(fā)生,此時(shí)使用自旋鎖,線程在極短的時(shí)間內(nèi)就能獲取到鎖,避免了線程上下文切換的開銷,大大提高了緩存的訪問效率。
高競(jìng)爭(zhēng)場(chǎng)景(互斥鎖更優(yōu)):然而,當(dāng)進(jìn)入高競(jìng)爭(zhēng)場(chǎng)景,大量線程頻繁地競(jìng)爭(zhēng)同一把鎖時(shí),自旋鎖就顯得力不從心了。多個(gè)線程同時(shí)進(jìn)行自旋空轉(zhuǎn),會(huì)導(dǎo)致 CPU 使用率急劇飆升,系統(tǒng)資源被大量浪費(fèi)。而互斥鎖的隊(duì)列調(diào)度機(jī)制,能夠?qū)⒌却i的線程有序地放入等待隊(duì)列中,當(dāng)鎖被釋放時(shí),再按照一定的規(guī)則喚醒線程,這種方式更加穩(wěn)定,能夠有效避免 CPU 資源的過度消耗。以電商系統(tǒng)的秒殺場(chǎng)景為例,在短時(shí)間內(nèi)會(huì)有大量用戶請(qǐng)求搶購(gòu)商品,對(duì)庫(kù)存鎖的競(jìng)爭(zhēng)非常激烈,此時(shí)使用互斥鎖可以保證系統(tǒng)的穩(wěn)定性,避免因?yàn)樽孕i導(dǎo)致的 CPU 過熱等問題。
3.4資源管理策略
優(yōu)先節(jié)省 CPU 資源(選互斥鎖):對(duì)于那些對(duì) CPU 占用敏感的場(chǎng)景,如服務(wù)器后臺(tái)任務(wù)、桌面應(yīng)用程序等,節(jié)省 CPU 資源是首要考慮的因素。在這些場(chǎng)景中,互斥鎖是更好的選擇,因?yàn)樗茏尩却i的線程進(jìn)入睡眠狀態(tài),避免了自旋鎖導(dǎo)致的 CPU 長(zhǎng)時(shí)間空轉(zhuǎn),從而降低了 CPU 的負(fù)載,減少了因 CPU 過熱或耗電增加帶來的問題。比如在一個(gè)運(yùn)行多個(gè)后臺(tái)服務(wù)的服務(wù)器上,如果使用自旋鎖,可能會(huì)導(dǎo)致 CPU 使用率過高,影響其他服務(wù)的正常運(yùn)行,而互斥鎖則能有效地避免這種情況。
優(yōu)先減少延遲(選自旋鎖):而在實(shí)時(shí)系統(tǒng)、高頻交易引擎等對(duì)響應(yīng)速度有極致要求的場(chǎng)景中,延遲是關(guān)鍵因素。自旋鎖的即時(shí)響應(yīng)特性,使得線程在等待鎖時(shí)無需經(jīng)歷上下文切換的延遲,一旦鎖被釋放,就能立即獲取并繼續(xù)執(zhí)行任務(wù),滿足了這些場(chǎng)景對(duì)低延遲的嚴(yán)格要求。例如,在高頻交易系統(tǒng)中,每毫秒的延遲都可能導(dǎo)致巨大的經(jīng)濟(jì)損失,使用自旋鎖可以確保交易指令能夠快速執(zhí)行,提高交易的成功率和效率。
3.5是否支持遞歸與阻塞語義
遞歸鎖需求:在編程中,有時(shí)候一個(gè)線程可能需要多次獲取同一把鎖,這就涉及到遞歸鎖的需求。互斥鎖通常支持可重入特性,以 POSIX 的pthread_mutex為例,它默認(rèn)就是可重入的,這意味著同一線程可以多次調(diào)用lock方法加鎖,而不會(huì)導(dǎo)致死鎖,每次加鎖對(duì)應(yīng)一次解鎖,只有所有的鎖都被釋放,其他線程才能獲取到鎖。而自旋鎖一般不具備可重入性,如果一個(gè)線程在持有自旋鎖的情況下再次嘗試獲取該鎖,就會(huì)陷入死鎖狀態(tài),因?yàn)樽孕i在被占用時(shí),其他線程只能自旋等待,無法進(jìn)行其他操作。
非阻塞獲取需求:另外,在某些場(chǎng)景下,我們希望能夠快速判斷是否能夠獲取鎖,而不是無條件地等待,這就需要鎖具備非阻塞獲取的能力。自旋鎖可以通過try_lock方法實(shí)現(xiàn)非阻塞嘗試獲取鎖,當(dāng)嘗試獲取失敗時(shí),線程可以立即返回,進(jìn)行其他操作,這種特性非常適合那些需要快速失敗的場(chǎng)景,比如在復(fù)雜的多線程程序中,為了避免死鎖的發(fā)生,我們可以先使用try_lock嘗試獲取鎖,如果獲取失敗,則可以采取其他措施,如釋放已經(jīng)持有的資源,重新調(diào)整操作流程。而互斥鎖的阻塞語義則更適合那些需要無條件等待鎖的場(chǎng)景,它會(huì)將線程阻塞,直到成功獲取鎖為止 。
四、實(shí)戰(zhàn)避坑:從代碼示例看最佳實(shí)踐
理論分析得再透徹,也不如實(shí)際代碼來得直觀。接下來,我們通過具體的代碼示例,深入探討自旋鎖和互斥鎖在不同場(chǎng)景下的應(yīng)用,看看如何在實(shí)戰(zhàn)中避坑,讓代碼性能更上一層樓。
4.1自旋鎖實(shí)戰(zhàn):高頻短操作場(chǎng)景
在多核處理器的環(huán)境下,當(dāng)遇到高頻次、短耗時(shí)的操作場(chǎng)景時(shí),自旋鎖能夠充分發(fā)揮其優(yōu)勢(shì),避免線程上下文切換帶來的開銷。比如,在一個(gè)多線程的網(wǎng)絡(luò)服務(wù)器中,需要對(duì)處理的請(qǐng)求數(shù)進(jìn)行高效統(tǒng)計(jì)。下面是一個(gè)用 C++ 實(shí)現(xiàn)的多線程安全計(jì)數(shù)器示例:
#include <iostream>
#include <thread>
#include <vector>
#include <atomic>
// 定義自旋鎖,使用std::atomic_flag來實(shí)現(xiàn),初始狀態(tài)為未設(shè)置(即未加鎖)
std::atomic_flag spinlock = ATOMIC_FLAG_INIT;
// 共享的全局計(jì)數(shù)器,將被多個(gè)線程更新
long long counter = 0;
// 模擬處理請(qǐng)求的函數(shù),每個(gè)線程執(zhí)行此函數(shù)來遞增計(jì)數(shù)器
void processRequest() {
    // 每個(gè)線程將執(zhí)行100,000次計(jì)數(shù)器遞增操作
    for (int i = 0; i < 100000; ++i) {
        // 自旋鎖:在鎖被占用時(shí),自旋等待,直到鎖被釋放
        while (spinlock.test_and_set(std::memory_order_acquire)) {
            // 忙等待,不斷檢查鎖狀態(tài),直到其他線程釋放鎖
        }
        // 獲得鎖后進(jìn)入臨界區(qū),對(duì)共享計(jì)數(shù)器進(jìn)行遞增操作
        ++counter;
        // 離開臨界區(qū),釋放鎖,允許其他線程進(jìn)入
        spinlock.clear(std::memory_order_release);
    }
}
int main() {
    const int numThreads = 4; // 定義線程數(shù)量
    std::vector<std::thread> threads; // 用于存儲(chǔ)線程對(duì)象的向量
    // 創(chuàng)建并啟動(dòng)多個(gè)線程,每個(gè)線程都執(zhí)行processRequest函數(shù)
    for (int i = 0; i < numThreads; ++i) {
        threads.emplace_back(processRequest);
    }
    // 等待所有線程執(zhí)行完畢
    for (auto& t : threads) {
        t.join();
    }
    // 輸出最終的計(jì)數(shù)器值
    std::cout << "Total requests processed: " << counter << std::endl;
    return 0;
}在這個(gè)示例中,processRequest函數(shù)模擬了處理請(qǐng)求的操作,每次處理請(qǐng)求時(shí)會(huì)對(duì)全局計(jì)數(shù)器counter進(jìn)行遞增操作。由于計(jì)數(shù)器的遞增操作非常簡(jiǎn)單,耗時(shí)極短,使用自旋鎖spinlock來保護(hù)對(duì)計(jì)數(shù)器的訪問是非常合適的。當(dāng)一個(gè)線程嘗試獲取自旋鎖時(shí),如果鎖已經(jīng)被其他線程持有,它會(huì)在while (spinlock.test_and_set(std::memory_order_acquire))循環(huán)中自旋等待,不斷檢查鎖的狀態(tài),直到鎖被釋放。一旦獲取到鎖,線程就可以安全地對(duì)計(jì)數(shù)器進(jìn)行遞增操作,操作完成后再釋放鎖,允許其他線程獲取。
這種自旋鎖的實(shí)現(xiàn)方式,避免了線程上下文切換的開銷,因?yàn)榫€程在等待鎖的過程中不會(huì)被阻塞,而是在原地循環(huán)檢查鎖狀態(tài),一旦鎖可用就能立即獲取并繼續(xù)執(zhí)行。在多核環(huán)境下,不同的線程可以在不同的核心上并行執(zhí)行,自旋等待的線程不會(huì)影響其他線程的執(zhí)行,從而大大提高了系統(tǒng)的性能。
4.2互斥鎖實(shí)戰(zhàn):長(zhǎng)耗時(shí)臨界區(qū)場(chǎng)景
當(dāng)臨界區(qū)包含長(zhǎng)耗時(shí)操作時(shí),互斥鎖則是更好的選擇。以數(shù)據(jù)庫(kù)連接池的連接分配為例,這是一個(gè)典型的需要長(zhǎng)時(shí)間占用鎖的場(chǎng)景,因?yàn)楂@取數(shù)據(jù)庫(kù)連接可能涉及到復(fù)雜的邏輯,如從連接池中查找可用連接、驗(yàn)證連接狀態(tài)、進(jìn)行必要的初始化等,還可能包含網(wǎng)絡(luò) I/O 操作,這些操作的耗時(shí)是不確定的,且通常較長(zhǎng)。下面是一個(gè)簡(jiǎn)化的 C++ 實(shí)現(xiàn)的數(shù)據(jù)庫(kù)連接池示例,展示了互斥鎖的應(yīng)用:
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <queue>
#include <condition_variable>
#include <chrono>
#include <random>
// 模擬數(shù)據(jù)庫(kù)連接類
class DatabaseConnection {
public:
    DatabaseConnection() {
        // 這里可以添加實(shí)際的連接初始化邏輯,如創(chuàng)建Socket連接、驗(yàn)證數(shù)據(jù)庫(kù)憑證等
        std::cout << "Database connection created" << std::endl;
    }
    ~DatabaseConnection() {
        // 這里可以添加實(shí)際的連接釋放邏輯,如關(guān)閉Socket連接等
        std::cout << "Database connection destroyed" << std::endl;
    }
};
class ConnectionPool {
public:
    ConnectionPool(int initialSize) : poolSize(initialSize) {
        // 初始化連接池,創(chuàng)建initialSize個(gè)數(shù)據(jù)庫(kù)連接
        for (int i = 0; i < initialSize; ++i) {
            connections.push(std::make_shared<DatabaseConnection>());
        }
    }
    // 獲取數(shù)據(jù)庫(kù)連接
    std::shared_ptr<DatabaseConnection> getConnection() {
        std::unique_lock<std::mutex> lock(mutex_);
        // 如果連接池為空,等待有連接被歸還
        while (connections.empty()) {
            // 模擬在等待過程中線程被操作系統(tǒng)調(diào)度出去,執(zhí)行其他任務(wù)
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            // 等待條件變量,當(dāng)有連接歸還時(shí)會(huì)被喚醒
            cv.wait(lock);
        }
        // 從連接池中取出一個(gè)連接
        auto conn = connections.front();
        connections.pop();
        return conn;
    }
    // 歸還數(shù)據(jù)庫(kù)連接
    void releaseConnection(std::shared_ptr<DatabaseConnection> conn) {
        std::unique_lock<std::mutex> lock(mutex_);
        // 將連接放回連接池
        connections.push(conn);
        // 通知等待的線程,有新的連接可用了
        cv.notify_one();
    }
private:
    std::queue<std::shared_ptr<DatabaseConnection>> connections; // 連接池,使用隊(duì)列存儲(chǔ)連接
    std::mutex mutex_; // 互斥鎖,保護(hù)連接池的訪問
    std::condition_variable cv; // 條件變量,用于通知等待連接的線程
    int poolSize; // 連接池的大小
};
// 模擬線程任務(wù),獲取連接并執(zhí)行數(shù)據(jù)庫(kù)操作
void databaseTask(ConnectionPool& pool) {
    auto conn = pool.getConnection();
    // 模擬執(zhí)行數(shù)據(jù)庫(kù)操作,如查詢、插入等,這里通過隨機(jī)睡眠來模擬耗時(shí)操作
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dis(100, 500);
    std::this_thread::sleep_for(std::chrono::milliseconds(dis(gen)));
    // 模擬操作完成后歸還連接
    pool.releaseConnection(conn);
}
int main() {
    ConnectionPool pool(5); // 創(chuàng)建連接池,初始大小為5
    const int numThreads = 10; // 定義線程數(shù)量
    std::vector<std::thread> threads; // 用于存儲(chǔ)線程對(duì)象的向量
    // 創(chuàng)建并啟動(dòng)多個(gè)線程,每個(gè)線程都執(zhí)行databaseTask函數(shù)
    for (int i = 0; i < numThreads; ++i) {
        threads.emplace_back(databaseTask, std::ref(pool));
    }
    // 等待所有線程執(zhí)行完畢
    for (auto& t : threads) {
        t.join();
    }
    return 0;
}在這個(gè)示例中,ConnectionPool類表示數(shù)據(jù)庫(kù)連接池,connections隊(duì)列用于存儲(chǔ)數(shù)據(jù)庫(kù)連接。getConnection方法用于從連接池中獲取一個(gè)數(shù)據(jù)庫(kù)連接,在獲取連接時(shí),首先會(huì)加鎖std::unique_lock<std::mutex> lock(mutex_),如果連接池為空,線程會(huì)在while (connections.empty())循環(huán)中等待,通過cv.wait(lock)釋放鎖并進(jìn)入睡眠狀態(tài),直到有連接被歸還,條件變量cv被觸發(fā)通知。
當(dāng)線程獲取到連接后,會(huì)模擬執(zhí)行數(shù)據(jù)庫(kù)操作,這里通過隨機(jī)睡眠std::this_thread::sleep_for(std::chrono::milliseconds(dis(gen)));來模擬實(shí)際的數(shù)據(jù)庫(kù)操作耗時(shí),操作完成后再通過releaseConnection方法歸還連接,歸還時(shí)同樣會(huì)加鎖,將連接放回連接池,并通知等待的線程。
在這個(gè)場(chǎng)景中,使用互斥鎖可以有效地管理對(duì)連接池的訪問,避免多個(gè)線程同時(shí)操作連接池導(dǎo)致的數(shù)據(jù)不一致問題。由于獲取連接和執(zhí)行數(shù)據(jù)庫(kù)操作的過程可能會(huì)比較耗時(shí),如果使用自旋鎖,等待的線程會(huì)一直占用 CPU 進(jìn)行自旋,導(dǎo)致 CPU 資源的浪費(fèi),而互斥鎖的阻塞機(jī)制可以讓等待的線程釋放 CPU 資源,讓其他線程有機(jī)會(huì)執(zhí)行,從而提高了系統(tǒng)的整體資源利用率和穩(wěn)定性。
4.3選擇策略總結(jié):5 步?jīng)Q策法
經(jīng)過前面的分析,我們可以總結(jié)出一個(gè) 5 步?jīng)Q策法,幫助大家在實(shí)際應(yīng)用中快速、準(zhǔn)確地選擇互斥鎖和自旋鎖:
- 測(cè)臨界區(qū)耗時(shí):首先要評(píng)估臨界區(qū)代碼的執(zhí)行時(shí)間。如果臨界區(qū)的執(zhí)行時(shí)間極短,在微秒級(jí)以下,如簡(jiǎn)單的變量讀寫、計(jì)數(shù)器增減等操作,自旋鎖是優(yōu)先選擇,因?yàn)槠渥孕却拈_銷可能比線程上下文切換的開銷還?。环粗?,如果臨界區(qū)執(zhí)行時(shí)間較長(zhǎng),達(dá)到毫秒級(jí)以上,涉及復(fù)雜計(jì)算、I/O 操作等,互斥鎖更為合適,它能避免自旋鎖帶來的 CPU 資源浪費(fèi)。
 - 看系統(tǒng)核數(shù):了解運(yùn)行環(huán)境是多核還是單核系統(tǒng)。在多核系統(tǒng)中,自旋鎖有更大的發(fā)揮空間,不同線程可以在不同核心上并行執(zhí)行,自旋等待的線程不會(huì)影響其他核心上的線程運(yùn)行,充分利用多核的并行性;而在單核系統(tǒng)中,自旋鎖的忙等策略會(huì)導(dǎo)致 CPU 被單一線程獨(dú)占,降低系統(tǒng)整體性能,此時(shí)必須選擇互斥鎖。
 - 評(píng)估競(jìng)爭(zhēng)頻率:分析線程對(duì)鎖的競(jìng)爭(zhēng)激烈程度。在低競(jìng)爭(zhēng)場(chǎng)景下,多數(shù)線程能快速獲取鎖,偶爾的自旋等待時(shí)間也很短,自旋鎖可以減少線程上下文切換的開銷,提高效率;但在高競(jìng)爭(zhēng)場(chǎng)景中,大量線程頻繁競(jìng)爭(zhēng)同一把鎖,自旋鎖會(huì)使 CPU 使用率飆升,資源浪費(fèi)嚴(yán)重,互斥鎖的隊(duì)列調(diào)度機(jī)制能更穩(wěn)定地管理線程,避免 CPU 過度消耗。
 - 查資源敏感型:判斷應(yīng)用場(chǎng)景對(duì) CPU 資源和延遲的敏感程度。對(duì)于 CPU 敏感的場(chǎng)景,如移動(dòng)設(shè)備、多任務(wù)服務(wù)器等,節(jié)省 CPU 資源至關(guān)重要,互斥鎖是更好的選擇,它能讓等待的線程釋放 CPU 資源;而對(duì)于延遲敏感的場(chǎng)景,如實(shí)時(shí)系統(tǒng)、高頻交易引擎等,要求響應(yīng)速度極快,自旋鎖的即時(shí)響應(yīng)特性可以滿足需求,減少延遲。
 - 驗(yàn)特殊需求:檢查是否有特殊的鎖需求。如果需要支持可重入或遞歸操作,互斥鎖是必備選項(xiàng),因?yàn)樽孕i一般不具備可重入性;如果希望實(shí)現(xiàn)非阻塞獲取鎖的功能,自旋鎖則更靈活,它可以通過try_lock方法快速判斷是否能獲取鎖,而互斥鎖的阻塞語義更適合無條件等待鎖的場(chǎng)景。
 
通過這 5 個(gè)步驟,我們可以全面、系統(tǒng)地分析具體場(chǎng)景的需求,從而做出最適合的鎖選擇,讓我們的多線程程序在性能和資源利用率上達(dá)到最佳平衡。















 
 
 
















 
 
 
 