一文搞懂Linux線程同步原理
大家好,今天和大家聊一聊Linux線程同步相關(guān)的知識(shí),線程同步相關(guān)的知識(shí)值得花時(shí)間好好研究,要設(shè)計(jì)出高性能軟件架構(gòu),必須學(xué)好Linux線程同步,對(duì)Linux線程同步原理有深刻的認(rèn)知。
1.背景知識(shí)
1.1 原子變量和原子操作
原子變量和原子操作是多線程編程中的重要概念,用于保證多線程環(huán)境下的數(shù)據(jù)同步和互斥。原子操作是指不會(huì)被線程調(diào)度機(jī)制打斷的操作,一旦開始就會(huì)一直運(yùn)行到結(jié)束,中間不會(huì)切換到其他進(jìn)程。原子變量是原子操作的基本單位。
C11標(biāo)準(zhǔn)引入了原子類型和原子操作,用于在多線程環(huán)境下保證數(shù)據(jù)的同步和一致性。
常見原子變量類型:
圖片
常見原子操作:
圖片
1.2 futex系統(tǒng)調(diào)用
futex是Linux內(nèi)核提供的一種系統(tǒng)調(diào)用,用于實(shí)現(xiàn)用戶空間線程之間的同步和互斥。它是fast userspace mutex的縮寫,意為快速用戶空間互斥鎖。futex的主要作用是在用戶空間實(shí)現(xiàn)鎖和條件變量,避免了用戶空間和內(nèi)核空間之間的頻繁切換,從而提高了多線程程序的性能。
futex系統(tǒng)調(diào)用的基本用法是:
一個(gè)線程在需要鎖或等待條件變量時(shí),調(diào)用futex系統(tǒng)調(diào)用,將自己掛起。
另一個(gè)線程在釋放鎖或改變條件變量時(shí),調(diào)用futex系統(tǒng)調(diào)用,喚醒等待的線程。
1.2.1 futex函數(shù)原型
int futex(int *uaddr, int futex_op, int val, const struct timespec *timeout, int *uaddr2, int val3);
功能:futex函數(shù)是Linux內(nèi)核提供的一種輕量級(jí)的鎖機(jī)制,它可以用于用戶空間進(jìn)程間的同步。
參數(shù):
uaddr:指向等待的變量的指針。
futex_op:表示要執(zhí)行的操作,可以是以下值之一:
- FUTEX_WAIT:等待變量的值變?yōu)橹付ㄖ怠?/li>
- FUTEX_WAKE:喚醒等待變量的線程。
val:與操作相關(guān)的值。
timeout:超時(shí)時(shí)間。
uaddr2:第二個(gè)等待變量的指針。
val3:與第二個(gè)等待變量相關(guān)的值。
1.2.2 futex實(shí)現(xiàn)原理
圖片
通過futex系統(tǒng)調(diào)用執(zhí)行FUTEX_WAIT命令,可以將線程掛起,futex傳入的uaddr參數(shù)會(huì)通過hash函數(shù)轉(zhuǎn)換成hash值,通過hash值能索引到futex_hash_bucket,此時(shí)會(huì)創(chuàng)建futex_q節(jié)點(diǎn),futex_q節(jié)點(diǎn)會(huì)存儲(chǔ)哈希key,線程相關(guān)信息,futex_q節(jié)點(diǎn)會(huì)插入chain鏈表。
通過futex系統(tǒng)調(diào)用執(zhí)行FUTEX_WAKE命令可喚醒掛起線程,futex系統(tǒng)調(diào)用通過uaddr參數(shù)找到對(duì)應(yīng)的futex_q節(jié)點(diǎn),然后喚醒futex_q節(jié)點(diǎn)指向的掛起線程。
2.線程為什么需要同步?
Linux線程是在Linux操作系統(tǒng)中實(shí)現(xiàn)的一種輕量級(jí)進(jìn)程,也稱為輕量級(jí)進(jìn)程或者LWP。同一線程組的線程共享主線程(進(jìn)程)的地址空間、文件描述符、信號(hào)處理等資源。
在Linux中,CPU的調(diào)度是以線程為單位進(jìn)行調(diào)度的,因此線程的調(diào)度也是以線程為單位進(jìn)行調(diào)度的。
圖片
由于線程之間共享地址空間,文件描述,信號(hào)相關(guān)資源,所以線程之間必然會(huì)存在同時(shí)訪問同一資源的問題,如果不進(jìn)行線程同步,就會(huì)導(dǎo)致數(shù)據(jù)的不一致性和安全性問題。同步可以保證在同一時(shí)刻只有一個(gè)線程訪問共享資源,從而避免了數(shù)據(jù)的沖突和錯(cuò)誤。
3. 互斥鎖實(shí)現(xiàn)原理
互斥鎖的實(shí)現(xiàn)視基于原子操作和futex系統(tǒng)調(diào)用實(shí)現(xiàn)。
3.1 互斥鎖常見操作
- 創(chuàng)建互斥鎖
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- 加鎖
pthread_mutex_lock(&mutex);
- 解鎖
pthread_mutex_unlock(&mutex);
- 嘗試加鎖
pthread_mutex_trylock(&mutex);
- 銷毀互斥鎖
pthread_mutex_destroy(&mutex);
3.2 互斥鎖實(shí)現(xiàn)原理
圖片
互斥鎖本質(zhì)是一個(gè)原子變量,原子變量同樣是一個(gè)共享變量,不同的線程都能訪問,只不過原子變量采用的是原子操作,互斥鎖的操作不可被中斷。
1)互斥鎖初始化
將原子變量設(shè)置成0,原子變量不同的值代表鎖不同的狀態(tài):
- 原子變量等于0:互斥鎖空閑,未加鎖。
- 原子變量等于1:互斥鎖加鎖成功。
- 原子變量等于2:互斥鎖加鎖失敗,線程通過futex(FUTEX_WAIT)系統(tǒng)調(diào)用被掛起。
2)互斥鎖加鎖
- 通過atomic_compare_exchange_strong(value, 0, 1)原子操作,判斷當(dāng)前互斥鎖是否已經(jīng)被加鎖,如果原子變量等于0,說明互斥鎖空閑,此時(shí)可以對(duì)互斥鎖進(jìn)行加鎖操作,將原子變量設(shè)置為1,返回true。
- 如果原子變量不等于0,則說明互斥鎖已經(jīng)加鎖,此時(shí)互斥鎖加鎖線程需要通過futex(FUTEX_WAIT)系統(tǒng)調(diào)用將線程掛起,掛起之前需要通過atomic_exchange(value, 2)設(shè)置原子變量的值為2,并返回舊原子變量值,通過舊原子變量值可以判斷原子變量是否被其他線程操作。
3)互斥鎖解鎖
- 線程通過atomic_exchange(value, 0)原子操作,將原子變量的值設(shè)置成0,返回舊原子變量值。
- 如果舊原子變量的值等于2,說明有一個(gè)線程被掛起,此時(shí)需要通過futex(FUTEX_WAKE)系統(tǒng)調(diào)用喚醒掛起線程,解鎖成功。
- 如果舊原子變量小于等于1,則直接解鎖成功。
總結(jié):
互斥鎖雖然有很多優(yōu)點(diǎn),能夠很方便的進(jìn)行線程同步,但是互斥鎖是通過futex系統(tǒng)調(diào)用實(shí)現(xiàn),采用系統(tǒng)調(diào)用必然存在用戶態(tài)和內(nèi)核態(tài)的切換問題,如果這種切換很頻繁的話,必然會(huì)影響系統(tǒng)性能和降低系統(tǒng)效率,后續(xù)我們將繼續(xù)探索更為高效的線程同步方式。