1. 程式人生 > 程式設計 >linux與執行緒

linux與執行緒

1.1.1 程式

​ 在早期,人們都是為特定機器編寫程式,並在其上執行計算任務(task)。漸漸的人們發現CPU與IO裝置之間速度差太多了,往往CPU都在空轉,是不是可以在CPU空閒的時候做些其他事呢?於是,就有了多工(每個任務就是一個程式),有了資源排程,有了作業系統...

​ 程式是資源管理的最小單位,作業系統在分配資源(記憶體,檔案等)時是以程式為單位劃分的。在單CPU(單核)的時代,通過對多個程式的排程(分配CPU時間片),我們已經可以邊聽音樂,邊打遊戲了!

在以前程式被描述為資源分配和執行排程的最小單位,但現在都不這麼說了,因為引入了執行緒的概念。不管怎樣,這些都只是一個名稱而已,本質上還是要看資源是怎麼分配和管理的。

​ 本文不會討論程式的排程演演算法,如FCFS,SJF,時間片輪轉等。在這裡,具體看一下Linux程式的資料結構,以及狀態轉換:

  • 程式的資料結構

linux程式是一個雙端連結串列結構,其主要內容包括程式碼段,資料段,堆,棧,記憶體對映表等

  • 程式的狀態轉換

程式狀態主要在就緒,執行,等待,每一次切換伴隨著一次上下文切換。

  • 上下文切換

    程式的上下文包括程式在執行時,CPU所有暫存器中的值、程式的狀態以及堆疊中的內容。所謂程式切換即一個程式獲得或者丟失CPU時間片,這個過程由核心負責儲存程式的狀態快照(上下文),由此發生了上下文切換。可以看到這個過程本身就需要消耗很多CPU時間片。

總結: 雖然通過多程式排程,可以併發的處理任務,但可以看到程式的切換很頻繁。一個程式剛得到CPU資源就又可能因為發生了IO阻塞而轉入等待狀態。一個程式在得到CPU時間片後如何充分利用它呢?於是又引入了執行緒的概念。

1.1.2 執行緒

​ 為了最大效率的利用CPU,防止IO操作阻塞整個程式執行,降低程式上下文切換的開銷,於是又引入了執行緒的概念,將執行緒作為CPU排程執行的基本單位。如果一個程式包含多個執行緒,則這多個執行緒可以併發或並行執行,並且執行緒不會導致程式的阻塞(理想情況或者理論層面來講)。

​ 為什麼執行緒可以降低開銷呢?對照上面程式記憶體結構圖,程式的所有執行緒共享程式的資料結構,除了執行緒私有的像程式計數器,棧空間,暫存器之外。一個程式的執行緒之間切換不會發生系統呼叫,還有采用多執行緒可以更好的利用多處理器平行計算,執行緒直接通訊更方便等等。

​ 儘管執行緒有很多優點,但這都只是概念性的。並不是所有的作業系統都支援執行緒。windows原生支援了執行緒的實現,但linux中並沒有執行緒的概念,所以只能通過在核心外實現多執行緒,根據執行緒的支援是在核心還是核心外,把執行緒劃分為核心執行緒和使用者級執行緒。

1.1.3 Posix執行緒標準

​ 在講Linux下執行緒實現之前,有必要先介紹一下posix執行緒標準。POSIX(Portable Operating System Interface)是一套介面API規範,有C語言描述,使用posix API編寫的程式碼在遵循posix規範的平臺間是可以移植的,JVM在linux系統上使用的就是pthread執行緒庫作為底層實現。其中關於執行緒的API被稱作pthread,該標準定義了從執行緒建立,通訊,退出全部相關API(以pthread_開頭)及其行為約束。

主要API:

函式字首 功能
pthread_ 執行緒本身及相關函式
pthread_attr_ 執行緒屬性物件
pthread_mutex_ 互斥鎖
pthread_mutexattr_ 互斥鎖屬性物件
pthread_cond_ 條件變數
pthread_condattr_ 條件變數屬性物件
pthread_key_ 執行緒私有資料
pthread_rwlock_ 讀寫鎖
pthread_rwlockattr_ 讀寫鎖屬性物件
pthread_barrier_ 屏障
pthread_barrierattr_ 屏障屬性物件
pthread_spin_ 自旋鎖

使用時引入標頭檔案#include <pthread.h>:

//--------------------執行緒相關API--------------------//

/** 建立執行緒 */
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine)(void*),void *arg);
/** 等待執行緒結束 */
int pthread_join (pthread_t thread,void**value_ptr);
/** 退出執行緒 */
void pthread_exit(void *value_ptr);
/** 脫離執行緒: 將執行緒屬性的分離狀態設定為 detached,等待結束時回收資源 */
int pthread_detach (pthread_t thread);
/** 結束執行緒: 給執行緒傳送中止訊號 */
int pthread_kill(pthread_t thread,int sig);
/** 獲取執行緒ID */
pthread_t pthread_self(void);

//----------------執行緒屬性相關API-------------------//

/** 設定執行緒屬性,pthread_create會用到 */
int pthread_attr_init(pthread_attr_t *attr);
/** 銷燬執行緒屬性 */
int pthread_attr_destroy(pthread_attr_t *attr);

//----------------互斥鎖相關API--------------------//

/** 初始化互斥鎖物件 */
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
/** 銷燬互斥鎖物件 */
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
/** 獲取互斥鎖(阻塞方式) */
int pthread_mutex_lock(pthread_mutex_t *mutex);
/** 獲取互斥鎖(非阻塞方式) */
int pthread_mutex_trylock(pthread_mutex_t *mutex);
/** 釋放互斥鎖 */
int pthread_mutex_unlock(pthread_mutex_t *mutex);

//----------------條件變數相關API------------------//

/** 初始化條件變數 */
int pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t *attr);
/** 銷燬條件變數 */
int pthread_cond_destroy(pthread_cond_t *cond);
/** 在條件變數上阻塞等待 */
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
/** 在條件變數上有時限等待 */
int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex,const struct timespec *abstime);
/** 喚醒一個在條件變數上等待的執行緒 */
int pthread_cond_signal(pthread_cond_t *cond);
/** 喚醒全部在條件變數上等待的執行緒 */
int pthread_cond_broadcast(pthread_cond_t *cond);

//----------------執行緒私有資料相關API------------------//

/** 設定執行緒私有資料 */
int pthread_key_create(pthread_key_t *key,void (*destructor)(void*));
/** 刪除執行緒私有資料 */
int pthread_key_delete(pthread_key_t key);

//----------------讀寫鎖相關API------------------//

/** 初始化一個讀寫鎖 */
int pthread_rwlock_init(pthread_rwlock_t *rwlock,const pthread_rwlockattr_t *attr);
/** 銷燬讀寫鎖 */
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
/** 讀鎖定(阻塞) */
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
/** 讀鎖定(非阻塞) */
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
/** 寫鎖定(阻塞) */
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
/** 寫鎖定(非阻塞) */
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
/** 釋放讀寫鎖 */
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

//----------------屏障相關API------------------//

/** 初始化一個屏障(柵欄) */
int pthread_barrier_init(pthread_barrier_t *barrier,const pthread_barrierattr_t *attr,unsigned count);
/** 銷燬屏障 */
int pthread_barrier_destroy(pthread_barrier_t *barrier);
/** 在屏障上等待 */
int pthread_barrier_wait(pthread_barrier_t *barrier);
複製程式碼

這裡只列出了部分API,更多API可以使用man命令檢視手冊,熟悉pthreads api對理解java執行緒機制也很有幫助。

1.1.4 Linux執行緒支援

​ Linux中沒有核心級執行緒的實現,所以只能在使用者級別實現執行緒功能。比較著名的有早期的LinuxThreads,後來的NGPT以及NPTL。這些實現都利用了Linux提供的輕量級程式功能。

​ 輕量級程式(LWP)就是對一個程式的拷貝(clone()系統呼叫),不過在進行拷貝時,可以有選擇的只拷貝部分,clone底層呼叫核心do_fork方法:

int do_fork(unsigned long clone_flags,unsigned long stack_start,struct pt_regs *regs,unsigned long stack_size)
複製程式碼

clone_flags就是要拷貝的內容,如LinuxThreads建立執行緒時,用CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND指定拷貝程式記憶體空間,檔案目錄,開啟的檔案表,訊號處理器,注意,這裡的拷貝表示在新的程式task_struct中將相關部分地址設定為與原程式相應地址相同,其實是共享相同記憶體,這個消耗相對普通程式會低一些。可以看到輕量級程式與執行緒非常相似,共享程式記憶體空間,開啟的檔案列表,訊號等,也有自己私有的暫存器,棧空間等,但不能就此將輕量級程式與執行緒等同。

  • LinuxThreads

    LinuxThreads通過建立一個輕量級程式來建立一個執行緒,即一對一的執行緒模型,這樣,執行緒的排程有os負責,而LinuxThreads通過一個管理執行緒(使用者級)來管理像執行緒取消、執行緒間的同步的工作。通過這種方式模擬了一個程式包含一組執行緒的定義,但畢竟是模擬的,必然存在很多問題,如管理執行緒增加了執行緒建立的開銷,執行緒數受到os程式數限制(後來linux版本有改進),無法利用SMP,執行緒間通訊需要通過訊號量的方式等等,以及與posix嚴重不相容問題,正是由於種種問題,出現了一些其他新的執行緒庫實現。

  • NGPT

    NGPT(Next-Generation POSIX Threads)是由IBM開發的一套新的用於取代LinuxThreads的執行緒庫,不過並沒有被廣泛使用,現在已經不在維護了。

  • NPTL

    NPTL(Native POSIX Thread Library)是由Red Hat開發的另一套用於取代LinuxThreads的執行緒庫。NPTL 不在使用管理執行緒,使用核心支援的程式共享訊號及訊號處理器,通過共享記憶體上實現futex功能來做執行緒同步,以及可以利用SMP特性等,理論上提高了多執行緒的效能,還有一個重要的點,基本支援posix標準。

    現在大部分平臺執行緒庫都是NPTL,可以通過getconf GNU_LIBPTHREAD_VERSION命令檢視。

總結: 因為Linux沒有原生語義的執行緒支援,所以在linux平臺的執行緒都是使用輕量級程式來實現執行緒,這種方式是即有核外也有核內參與,在核內通過輕量級程式模擬,在核外實現執行緒語義(執行緒組,執行緒通訊等)在一些地方稱之為“混合式執行緒實現”。總而言之,可以知道,在linux平臺一個執行緒就是一個輕量級程式。

此篇為java多執行緒系列文章第一篇,更多內容關注以後更新