一、線程模型:
線程是程序中完成一個獨立任務的完整執行序列,即一個可調度的實體。根據運行環境和調度者的身份,線程可分為
內核線程和用戶線程。
內核線程:運行在內核空間,由內核來調度;
用戶線程:運行在用戶空間,由線程庫來調用。
當進程的一個內核線程獲得CPU的使用權時,它就加載並運行一個用戶線程。可見,內核程序相當於用戶線程運行的容器。一個進程可以擁有M個內核線程和N個用戶線程,其中M≤N。並且在一個系統的所有進程中,M和N的比值都是固定的。按照M:N的取值,線程的實現方式可分為三種模式:
完全在用戶空間實現、完全由內核調度和雙層調度。
1、完全在用戶空間實現的線程無須內核的支持,內核甚至根本不知道這些現成的存在。線程庫負責管理所有執行線程,比如線程的優先級、時間片等。線程庫利用longjmp來切換線程的執行,使它們看起來像是“並發”執行的。但實際上內核仍然是把整個進程作為最小單位來調度的。換句話說,一個進程的所有執行線程共享該進程的時間片,它們對外表現出相同的優先級。因此,對於這種實現方式而言,M=1,即N個用戶空間線程對應1個內核線程,而該內核線程實際上就是進程本身。
完全在用戶空間實現的線程的優點是:創建和調度線程都無需內核的干預,因此速度相當快。並且由於它不占用額外的內核資源,所以即使一個進程創建了很多線程,也不會對系統性能造成明顯的影響。其缺點是:對於多處理器系統,一個進程的多線程無法運行在不同的CPU上,因為內核是按照其最小調度單位來分配CPU的。此外,線程的優先級只對同一個進程中的線程有效,比較不同進程中的線程的優先級沒有意義。
2、完全由內核調度的模式將創建、調度線程的任務都交給了內核,運行在用戶空間的線程庫無須執行管理任務,這與完全在用戶空間實現的線程恰恰相反。二者的優缺點也正好互換。完全由內核調度的這種線程實現方式滿足M:N=1:1,即1個用戶空間線程被映射為1個內核線程。
3、雙層調度模式是前兩種實現模式的混合體:內核調度M個內核線程,線程庫調用N個用戶線程。這種線程實現方式結合了前兩種方式的優點:不但不會消耗過多的內核資源,而且線程切換速度也較快,同時它可以充分利用多處理器的優勢。
二、Linux線程庫
Linux上最有名的線程庫是LinuxTreads和NPTL,它們都是采用1:1的方式實現的。
用戶可以使用如下命令來查看當前系統上所使用的線程庫:
$ getconf GNU_LIBPTHREAD_VERSION
NPTL 2.14.90
三、創建線程和結束線程
1、pthread_create:創建一個線程
#include <pthread.h>
int pthread_create( pthread_t* thread, const pthread_att_t* attr,
void* (*start_routine)(void*),void* arg);
thread參數是新線程的標識符,后續pthread_t*函數通過它來引用新的線程。其類型pthread_t的定義如下:
#include <bits/pthreadtypes.h>
typedef unsigned long int pthread_t;
attr參數用於設置新線程的屬性。給它傳遞NULL表示使用默認的線程屬性。start_routine和arg參數分別指定新線程將運行的函數及其參數。
pthread_create成功時返回0,失敗時返回錯誤碼。一個用戶可以打開的線程數量不能超過RLIMIT)NPROC軟資源限制。此外,系統上所有用戶能創建的線程總數也不能超過/pro/sys/kernel.threads-max內核參數所定義的值。
2、pthread_exit
線程一旦被創建好,內核就可以調度內核線程來執行start_routine函數指針所指向的函數了。線程函數在結束時最好調用如下函數,以確保安全、干凈的退出。
#include <pthread.h>
void pthread_exit(void* retval);
pthread_exit函數通過retval參數向線程的回收者傳遞其退出信息。它執行完之后不會返回到調用者,而且永遠不會失敗。
3、pthread_join
一個進程中的所有線程都可以調用
pthread_join函數來回收其他線程,即等待其他線程結束,這類似於回收進程的wait和waitpid系統調用。
#include <pthread.h>
int pthread_join(pthread_t thread,void** retval);
thread參數是目標線程的標識符,retval參數則是目標線程返回的退出消息。該函數會一直阻塞,直到被回收的線程結束為止。該函數成功時返回0,失敗則返回錯誤碼。
錯誤碼 | 描述 |
EDEADLK | 可能引起死鎖。比如兩個線程互相針對對方調用pthread_join,或者線程對自身調用pthread_join。 |
EINVAL | 目標線程是不可回收的,或者已有其他線程在回收該目標線程 |
ESRCH | 目標線程不存在 |
4、pthread_cancel
有時候我們希望異常終止一個線程,即取消線程。
#include <pthread.h>
int pthread_cancel(pthread_t thread);
threat參數是目標線程的標識符。該函數返回成功時返回0,失敗返回錯誤碼。不過,接收到取消請求的目標線程可以決定是否允許被取消以及如何取消。
#include <pthread.h>
int pthread_setcancelstate(int state,int* oldstate);
int pthread_setcanceltype(int type,int* oldtype);
這兩個函數的第一個參數分別用於設置線程的取消狀態(是否取消)和取消類型(如何取消),第二個參數分別記錄線程原來的取消狀態和取消類型。
四、POSIX信號量
專門用於線程同步的機制:POSIX信號量、互斥量和條件變量。
常用的POSIX信號量函數是下面5個:
#include <semaphore.h>
int sem_init(sem_t* sem,int pshared,unsigned int value);
int sem_destroy(sem_t* sem);
int sem_wait(sem_t* sem);
int sem_trywait(sem_t* sem);
int sem_post(sem_t* sem);
這些函數的第一個參數sem指向被操作的信號量。
sem_init函數用於初始化一個未命名的信號量。pshared參數指定信號量的類型。如果其值為0,就表示這個信號量是當前進程的局部信號量,否則該信號量就可以在多個進程之間共享。value參數指定信號量的初始值。此外,初始化一個已經被初始化的信號量將導致不可預期的結果。
sem_destroy函數用於銷毀信號量,以釋放其占用的內核資源。如果銷毀一個正被其他線程等待的信號量,則將導致不可預期的結果。
sem_wait函數以原子操作的方式將信號量減1。如果信號量的值為0,則sem_wait將被阻塞,直到這個信號量具有非0值。
sem_trywait與sem_wait函數相似,不過它始終立即返回,而不論被操作的信號量是否具有非0值,相當於sem_wait的非阻塞版本。當信號量的值非0時,sem_trywait對信號量執行減1操作。當信號量的值為0時,它將返回-1並設置errno為EAGAIN。
sem_post函數以原子操作的方式將信號量的值加1。當信號量的值大於0時,其他正在調用sem_wait等待信號的線程將被喚醒。
上面這些函數,成功時返回0,失敗則返回-1並設置errno。
五、互斥鎖
POSIX互斥鎖的相關函數主要有5個:
#include <pthread.h>
int pthread_mutex_init( pthread_mutex_t *mutex,
const pthread_mutexattr_t *mutexattr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
這些函數的第一個參數mutex指向要操作的目標互斥鎖,互斥鎖的類型是pthread_mutex_t結構體。
pthread_mutex_init函數用於初始化互斥鎖。mutexattr參數指定互斥鎖的屬性。如果將它設置為NULL,則表示使用默認屬性。
pthread_mutex_destroy函數用於銷毀互斥鎖,以釋放其占用的內核資源。銷毀一個已經加鎖的互斥鎖將導致不可預期的后果。
pthread_mutex_lock函數以原子操作的方式給一個互斥鎖加鎖。如果目標互斥鎖已經被鎖上,則pthread_mutex_lock調用將阻塞,直到該互斥鎖的占有者將其解鎖。
pthread_mutex_trylock與pthread_mutex_lock函數類似,不過它始終立即返回,而不論被操作的互斥鎖是否已經被加鎖,相當於pthread_mutex_lock的非阻塞版本。當目標互斥鎖未被加鎖時,pthread_mutex_trylock將返回錯誤碼EBUSY。
pthread_mutex_unlock函數以原子操作的方式給一個互斥鎖解鎖。如果此時有其他線程正在等待這個互斥鎖,則這些線程中的某一個將獲得它。
上面這些函數,成功時返回0,失敗則返回錯誤碼。
互斥鎖屬性type指定互斥鎖的類型。Linux支持如下4種類型的互斥鎖:
1、PTHREAD_MUTEX_NORMAL,普通鎖。這是互斥鎖默認的類型。當一個線程對一個普通鎖加鎖以后,其余請求該鎖的線程將形成一個等待隊列,並在該鎖解鎖后按優先級獲得它。這種鎖類型保證了資源分配的公平性。但這種鎖也很容易引發的問題:一個線程如果對一個已經加鎖的普通鎖再次加鎖,將引發死鎖;對一個已經被其他線程加鎖的普通鎖解鎖,或者對一個已經解鎖的普通鎖再次解鎖,將導致不可預期的后果。
2、PTHREAD_MUTEX_ERRORCHECK,檢查鎖。一個線程如果對一個已經加鎖的檢查鎖再次加鎖,則加鎖操作返回EDEADLK;對一個已經被其他線程加鎖的檢查鎖解鎖,或者對一個已經解鎖的檢查鎖再次解鎖,則解鎖操作返回EPERM。
3、PTHREAD_MUTEX_RECURSIVE,嵌套鎖。這種鎖允許一個線程在釋放鎖之前多次對它加鎖而不發生死鎖。不過其他線程如果要獲得這個鎖,則當前鎖的擁有者必須執行相應次數的解鎖操作。對一個已經被其他線程加鎖的嵌套鎖解鎖,或者對一個已經解鎖的嵌套鎖再次解鎖,則解鎖操作返回EPERM。
4、PTHREAD_MUTEX_DEFAULT,默認鎖。一個線程如果對一個已經加鎖的默認所再次加鎖,或者對一個已經被其他線程加鎖的默認鎖解鎖,或者對一個已經解鎖的默認所再次解鎖,將導致不可預期的后果。這種鎖在實現的時候可能被映射為上面三種鎖之一。
六、可重入函數
如果一個函數能被多個線程同時調用且不發生競態條件,則我們稱它是線程安全的,或者說它是可重入函數。Linux庫函數只有一小部分是不可重入的,這些庫函數之所以不可重入,只要是其內部使用了靜態變量。
七、線程和進程
如果一個多線程程序的某個線程調用了fork函數,那么新創建的子進程是否將自動創建和父進程相同數量的線程呢?
答:不是的。子進程只擁有一個執行程序,它是調用fork的那個線程的完整復制。並且子進程將自動繼承父進程中的互斥鎖的狀態。也就是說,父進程中已經被加鎖的互斥鎖在子進程中也是被鎖住的。這就引發了一個問題:子進程可能不清楚從父進程繼承而來的互斥鎖的具體狀態(是加鎖還是不加鎖)。這個互斥鎖可能被加鎖了,但並不是由調用fork函數的那個線程鎖住的,而是由其他線程鎖住的。如果是這樣的情況,而子進程若再次對該互斥鎖執行加鎖操作會導致死鎖。
不過,pthread提供了一個專門的函數pthread_atfork,以確保fork調用后父進程和子進程都擁有一個清楚的鎖的狀態。該函數定義如下:
#include <pthread.h>
pthread_atfork(void (*prepare)(void),void (*parent)(void),void (*child)(void));
prepare句柄將在fork調用創建出子進程之前被執行。它可以用來鎖住所有父進程中的互斥鎖。parent句柄則是fork調用創建出子進程之后,而fork返回之前,在父進程中被執行。它的作用是釋放所有在prepare句柄中被鎖住的互斥鎖。child句柄是fork返回之前,在子進程中被執行。和parent句柄一樣,child句柄也是用於釋放所有在prepare句柄中被鎖住的互斥鎖。
該函數成功時返回0,失敗返回錯誤碼。
八、線程和信號
每個線程都可以獨立的設置信號掩碼。