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