線程基本函數
當一個程序被啓動時,只有一個主線程,若要實現對其他線程的基本操作,首先必須創建新的線程,新的線程創建可以使用 pthread_create 函數實現,該函數的 API 定義如下:
/* 函數功能:創建新的線程;
* 返回值:若成功則返回0,若出錯則返回正的錯誤碼;
* 函數原型:
*/
#include <pthread.h>
int pthread_create(pthread_t *tid, const pthread_attr_t *attr,
void*(*func)(void*), void *arg);
/* 說明:
* 當該函數成功返回時,由tid指向的內存單元被設置爲新創建線程的線程ID;
* attr參數用於設置線程的屬性,若爲NULL時,表示採用默認屬性創建該線程;
* 新創建的線程從func函數的地址開始運行,該函數只有一個參數arg;
* 若需要向func函數傳遞多個參數,則必須把需要傳遞的參數包裝成一個結構體,
* 然後把該結構體的地址作爲arg參數傳入;
*/
在進程中,若調用了函數 exit,_exit,或_Exit 時,則該進程會終止,同樣,若進程中的線程調用這三個函數時也會使線程所在的進程終止。那麼若只是想退出線程,而不終止線程所在的進程有什麼辦法?下面是在單線程模式下退出線程的三種方式(不會終止線程所在的進程):
- 線程只是從啓動例程中返回,返回值是線程的退出碼;
- 線程被同一進程的其他線程取消;
- 線程調用 pthread_exit 函數;
/* 函數功能:終止一個線程;
* 返回值:無;
* 函數原型:
*/
#include <pthread.h>
void pthread_exit(void *status);
/* 說明:
* 若本線程不處於脫離狀態,則其線程ID和退出狀態碼將一直保留到調用進程內的某個其他線程對它調用pthread_join函數;
* status是向 線程的回收者傳遞其退出信息,執行完之後該信息不會返回給調用者;
*/
通常父進程需要調用 wait 函數族等待子進程終止,避免子進程成爲殭屍進程。在線程中爲確保終止線程的資源對進程可用,即回收終止線程的資源,應該在每個線程結束時分離它們。一個沒有被分離的線程終止時會保留其虛擬內存,包括它們的堆棧和其他系統資源。分離線程意味着通知系統不再需要此線程,允許系統將分配給它的資源回收。
調用 pthread_join 函數將自動分離指定的線程,被分離的線程就再也不能被其他線程連接了,即恢復了系統資源。若線程已處於分離狀態,調用pthread_join 會失敗,將返回EINVAL。所以,如果多個線程需要知道某個特定的線程何時結束,則這些線程應該等待某個條件變量而不是調用 pthread_join。一個進程中的所有線程都可以調用 pthread_join 函數來等待其他線程終止(即回收其他線程的系統資源),該函數的 API 定義如下:
/* 函數功能:等待一個線程終止;
* 返回值:若成功則返回0,若錯誤則返回正的錯誤碼;
* 函數原型:
*/
#include <pthread.h>
int pthread_join(pthread_t tid, void **status);
/* 說明:
* 一個進程中的所有線程可以調用該函數回收其他線程,即等待其他線程終止;
* tid參數是目標線程的標識符,status參數是保存目標線程返回的退出碼;
* 該函數會一直阻塞,直到被回收的線程終止;
*/
每個線程都有自身的線程 ID ,線程 ID 由創建線程的函數 pthread_create 返回,可以使用函數 pthread_self 獲取自身的線程 ID ,其 API 定義如下:
/* 函數功能:獲取自身的線程ID;
* 返回值:返回調用線程的線程ID;
* 函數原型:
*/
#include <pthread.h>
pthread_t pthread_self(void);
一個線程或是可匯合狀態(默認),或是脫離狀態,當一個可匯合狀態的線程終止時,它的線程 ID 和退出狀態將保留到另一個線程對其調用 pthread_join。脫離狀態的線程像守護進程一樣,當它終止時,所有相關資源都被釋放,我們不能等待他們終止。若一個線程需要知道另一個線程啥時候終止,那最好保持第二個線程的可匯合狀態。
調用 pthread_detach 函數可將指定的線程變爲脫離狀態。其定義如下:
/* 函數功能:把指定的線程變爲脫離狀態;
* 返回值:若成功則返回0,若出錯則返回正的錯誤碼;
* 函數原型:
*/
#include <pthread.h>
int pthread_detach(pthread_t tid);
同一個進程中的所有線程可以調用 pthread_cancel 函數向請求取消其他線程,被請求取消的線程可以選擇允許取消或如何取消。取消線程相當於線程異常終止,該函數定義如下:
/* 函數功能:請求取消同一進程的其他線程;
* 返回值:若成功則返回0,出錯則返回正的錯誤碼;
* 函數原型:
*/
#include <pthread.h>
int pthread_cancel(pthread_t tid);
/* 說明:
* tid參數是目標線程的標識符;
* 雖然可以請求取消某個線程,但是該線程可以決定是否允許被取消或者如何取消,這分別由以下兩個函數完成:
*/
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
/* 說明:
* 這兩個函數中的第一個參數是分別用於設置取消狀態(即是否允許取消)和取消類型(即如何取消),第二個參數則是分別記錄線程原來的取消狀態和取消類型;
* state有兩個可選的取值:
* 1、PTHREAD_CANCEL_ENABLE 允許線程被取消(默認情況);
* 2、PTHREAD_CANCEL_DISABLE 禁止線程被取消,這種情況,若一個線程收到取消請求,則它會將請求掛起,直到該線程允許被取消;
*
* type也有兩個可選取值:
* 1、PTHREAD_CANCEL_ASYNCHRONOUS 線程隨時可以被取消;
* 2、PTHREAD_CANCEL_DEFERRED 允許目標線程推遲執行;
*/
線程屬性
在前面介紹的線程操作中都是採用線程的默認屬性進程操作。在創建新的線程時,我們可以使用系統默認的屬性,也可以自己指定線程的主要屬性。我們可以指定 pthread_attr_t 結構修改線程的默認屬性,並把這個屬性與創建線程聯繫起來。下面先看下線程的主要屬性:
/* 結構體定義 */
#include <bits/pthreadtypes.h>
#define __SIZEOF_PTHREAD_ATTR_T 36
typedef union
{
char __size[__SIZEOF_PTHREAD_ATTR_T];
long int __align;
}pthread_attr_t;
線程屬性主要包括以下四種屬性:
/*
* 線程的主要屬性:
* (1)detachstate 線程的脫離狀態屬性;
* (2)guardsize 線程棧末尾的警戒緩衝區大小(字節數);
* (3)stackaddr 線程棧的最低地址;
* (4)stacksize 線程棧的大小(字節數);
*/
在進行線程屬性操作之前必須對其進行初始化,初始化函數定義如下:
/*
* 函數功能:初始化屬性結構;
* 返回值:若成功則返回0,否則返回錯誤編碼;
* 函數原型:
*/
#include <pthread.h>
/* 初始化線程屬性對象 */
int pthread_attr_init(pthread_attr_t *attr);
/* 若線程屬性是動態分配內存的,則在釋放內存之前,必須調用該函數銷燬線程屬性對象 */
int pthread_attr_destroy(pthread_attr_t *attr);
脫離狀態屬性
我們可以通過 pthread_attr_t 結構修改線程脫離狀態屬性 detachstate,下面是關於對該屬性操作的函數:
/*
* 函數功能:修改線程的分離狀態屬性;
* 返回值:若成功則返回0,否則返回錯誤編碼;
* 函數原型:
*/
#include <pthread.h>
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);//獲取當前線程的分離狀態屬性;
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);//設置當前線程的分離狀態屬性;
/*
* 說明:
* detachstate的值爲以下兩種:
* (1)PTHREAD_CREATE_DETACHED 以分離狀態啓動線程;
* (2)PTHREAD_CREATE_JOINABLE 正常啓動線程(默認,即可匯合狀態);
*/
棧屬性
可以通過下面的操作函數來獲取或者修改線程的棧屬性。
/*
* 函數功能:獲取或修改線程的棧屬性;
* 返回值:若成功則返回0,否則返回錯誤編碼;
* 函數原型:
*/
#include <pthread.h>
int pthread_attr_getstack(const pthread_attr_t *attr, void ** stackaddr, size_t stacksize);//獲取線程棧信息;
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);//修改線程棧信息;
int pthread_attr_getstackaddr(const pthread_attr_t *attr, void ** stackaddr);//獲取線程棧起始地址信息;
int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);//修改線程棧起始地址信息;
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);//獲取棧大小的信息;
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);//設置棧大小;
int pthread_attr_getguardsize(const pthread_attr_t *attr, size_t *guardsize);
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
互斥鎖
當多個控制線程共享相同的內存時,需要確保每個線程看到一致的數據視圖。當多個線程對可修改變量進行訪問時,就會出現變量的一致性問題,這時就會涉及到線程同步的問題。
互斥鎖也稱爲互斥量。可以通過使用 pthread 的互斥接口保護數據,確保同一時間只有一個線程訪問數據。互斥量本質上就是一把鎖,在訪問共享資源前對互斥量進行加鎖,在訪問完成後釋放互斥量上的鎖。對互斥量進行加鎖以後,任何其他試圖再次對互斥量加鎖的線程將會被阻塞直到當前線程釋放該互斥鎖。如果釋放互斥鎖時有多個線程阻塞,所有在該互斥鎖上的阻塞線程都會變成運行狀態,第一個變爲運行狀態的線程可以對互斥量進行加鎖,其他線程將會看到互斥鎖依然被鎖住,只能回去等待它重新變爲可用。在這種方式下,每次只有一個線程可以向前執行。
互斥變量使用 pthread_mutex_t 數據類型來表示,在使用互斥量以前,必須先對它進行初始化,可以把它設置爲常量 PTHREAD_MUTEX_INITIALIZER (只對靜態分配的互斥量),也可以通過調用 pthread_mutex_init 函數進行初始化。如果動態地分配互斥量,那麼在釋放內存前需要調用 pthread_mutex_destroy。
/* 互斥量 */
/*
* 函數功能:初始化互斥變量;
* 返回值:若成功則返回0,否則返回錯誤編碼;
* 函數原型:
*/
#include <pthread.h>
/* 初始化互斥鎖 */
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
/* 銷燬互斥鎖,釋放系統資源 */
int pthread_mutex_destroy(pthread_mutex_t *mutex);
/*
* 說明:
* attr表示互斥鎖的屬性,若attr爲NULL,表示初始化互斥量爲默認屬性;
*/
/*
* 函數功能:對互斥量進行加、解鎖;
* 返回值:若成功則返回0,否則返回錯誤編碼;
* 函數原型:
*/
int pthread_mutex_lock(pthread_mutex_t *mutex);//對互斥量進行加鎖,線程被阻塞;
int pthread_mutex_trylock(pthread_mutex_t *mutex);//對互斥變量加鎖,但線程不阻塞;
int pthread_mutex_unlock(pthread_mutex_t *mutex);//對互斥量進行解鎖;
/* 說明:
* 調用pthread_mutex_lock對互斥變量進行加鎖,若互斥變量已經上鎖,則調用線程會被阻塞直到互斥量解鎖;
* 調用pthread_mutex_unlock對互斥量進行解鎖;
* 調用pthread_mutex_trylock對互斥量進行加鎖,不會出現阻塞,否則加鎖失敗,返回EBUSY。
*/
互斥鎖屬性
互斥鎖屬性可以用 pthread_mutexattr_t 數據結構來進行操作,屬性的初始化操作如下:
/* 互斥量屬性 */
/*
* 函數功能:初始化互斥量屬性;
* 返回值:若成功則返回0,否則返回錯誤編碼;
* 函數原型:
*/
#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
/*
* 說明:
* pthread_mutexattr_init函數用默認的互斥量屬性初始化pthread_mutexattr_t結構;
* 兩個屬性是進程共享屬性和類型屬性;
*/
/*
* 函數功能:獲取或修改進程共享屬性;
* 返回值:若成功則返回0,否則返回錯誤編碼;
* 函數原型:
*/
#include <pthread.h>
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *pshared);//獲取互斥量的進程共享屬性
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);//設置互斥量的進程共享屬性
/*
* 說明:
* 進程共享互斥量屬性設置爲PTHREAD_PROCESS_PRIVATE時,允許pthread線程庫提供更加有效的互斥量實現,這在多線程應用程序中是默認的;
* 在多個進程共享多個互斥量的情況下,pthread線程庫可以限制開銷較大的互斥量實現;
*
* 若設置爲PTHREAD_PROCESS_PRIVATE,則表示互斥鎖只能被和鎖初始化線程隸屬於同一進程的線程共享;
*/
/*
* 函數功能:獲取或修改類型屬性;
* 返回值:若成功則返回0,否則返回錯誤編碼;
* 函數原型:
*/
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);//獲取互斥量的類型屬性
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);//修改互斥量的類型屬性
/* 說明:
* type取值如下:
* 1、PTHREAD_MUTEX_NORMAL 普通鎖(默認)
* 2、PTHREAD_MUTEX_ERRORCHECK 檢錯鎖
* 3、PTHREAD_MUTEX_RECUSIVE 嵌套鎖
* 4、PTHREAD_MUTEX_DEFAULT 默認鎖
*/
互斥鎖類型:
- 普通鎖:當一個線程對一個普通鎖加鎖以後,其餘請求該鎖的線程將形成一個等待隊列,並在該鎖解鎖後按優先級獲取該它。這種鎖容易引發死鎖,即當同一個線程對一個已經加鎖的普通鎖再次加鎖時,就會引發死鎖。
- 檢錯鎖:一個線程若對一個已經加鎖的檢錯鎖再次加鎖時,則加鎖操作返回 EDEADLK,對一個已被其他線程加鎖的檢錯鎖解鎖,或對一個已經解鎖的檢錯鎖再次解鎖,則解鎖操作返回 EPERM。
- 嵌套鎖:這種鎖允許一個線程在釋放鎖之前多次對它加鎖而不發生死鎖。但是其他線程若要獲得該鎖,則當前鎖的擁有者必須執行相應次數的解鎖操作。對一個已經被其他線程加鎖的嵌套鎖,或對一個已經解鎖的嵌套鎖再次解鎖,則解鎖操作返回 EPERM。
- 默認鎖:一個線程若對一個已經加鎖的默認鎖再次加鎖,或對一個已經被其他線程加鎖的默認鎖解鎖,或對一個已經解鎖的默認鎖再次解鎖,將導致不可預期的後果。這種鎖的實現可能會被映射爲上面三種鎖之一。
條件變量
互斥鎖是用於同步線程對共享數據的訪問,條件變量則是用於在線程之間同步共享數據的值。互斥鎖提供互斥訪問機制,條件變量提供信號機制,當某個共享數據達到某個值時,喚醒等待這個共享數據的線程。條件變量可以將一個或多個線程進入阻塞狀態,直到收到另外一個線程的通知,或者超時,或者發生了虛假喚醒,才能退出阻塞狀態。
條件變量與互斥量一起使用,允許線程以無競爭的方式等待特定的條件發生,條件本身是由互斥量保護。線程在改變條件狀態前必須先鎖住互斥量,條件變量允許線程等待特定條件發生。條件變量通過允許線程阻塞和等待另一個線程發送信號的方法彌補了互斥鎖的不足,它常和互斥鎖一起使用。使用時,條件變量被用來阻塞一個線程,當條件不滿足時,線程往往解開相應的互斥鎖並等待條件發生變化。一旦其它的某個線程改變了條件變量,它將通知相應的條件變量喚醒一個或多個正被此條件變量阻塞的線程。這些線程將重新鎖定互斥鎖並重新測試條件是否滿足。一般說來,條件變量被用來進行線程間的同步。條件變量類型爲
pthread_cond_t,使用前必須進行初始化。可以有兩種初始化方式:把常量 PTHREAD_COND_INITIALIZER 賦給靜態分配的條件變量,對於動態分配的條件變量,可以使用 pthread_cond_init 進行初始化。操作函數如下:
/* 條件變量 */
/*
* 函數功能:初始化條件變量;
* 返回值:若成功則返回0,否則返回錯誤編碼;
* 函數原型:
*/
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cond);
/* 說明:
* cond參數指向要操作的目標條件變量,attr參數指定條件變量的屬性;
*/
/*
* 函數功能:等待條件變量變爲真;
* 返回值:若成功則返回0,否則返回錯誤編碼;
* 函數原型:
*/
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex,
const struct timespec *timeout);
/*
* 說明:
* 傳遞給pthread_cond_wait的互斥量對條件進行保護,調用者把鎖住的互斥量傳給函數;
* 函數把調用線程放到等待條件的線程列表上,然後對互斥量解鎖,這兩操作是原子操作;
* 這樣就關閉了條件檢查和線程進入休眠狀態等待條件改變這兩個操作之間的時間通道,這樣就不會錯過條件變化;
* pthread_cond_wait返回時,互斥量再次被鎖住;
*/
/*
* 函數功能:喚醒等待條件的線程;
* 返回值:若成功則返回0,否則返回錯誤編碼;
* 函數原型:
*/
int pthread_cond_signal(pthread_cond_t *cond);//喚醒等待該條件的某個線程,具體喚醒哪個線程取決於線程的優先級和調度策略;
int pthread_cond_broadcast(pthread_cond_t *cond);//喚醒等待該條件的所有線程;
條件變量屬性
條件變量也只有進程共享屬性,其操作如下:
/* 條件變量屬性 */
/*
* 函數功能:初始化條件變量屬性;
* 返回值:若成功則返回0,否則返回錯誤編碼;
* 函數原型:
*/
#include <pthread.h>
int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);
/*
* 函數功能:獲取或修改進程共享屬性;
* 返回值:若成功則返回0,否則返回錯誤編碼;
* 函數原型:
*/
#include <pthread.h>
int pthread_condattr_getpshared(const pthread_condattr_t *attr, int *pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared);
線程與信號
當線程被創建時,它會繼承進程的信號掩碼,這個掩碼就會變成線程私有的,所以每個線程可以獨立設置信號掩碼。進程中的所有線程都是共享該進程的信號。多個線程是共享進程的地址空間,每個線程對信號的處理函數是相同的,即如果某個線程修改了與某個信號相關的處理函數後,所在進程中的所有線程都必須共享這個處理函數的改變。也就是說,當在一個線程設置了某個信號的信號處理函數後,它將會覆蓋其他線程爲同一個信號設置的信號處理函數。
每個信號只會被傳遞給一個線程,即進程中的信號是傳遞到單個線程的,傳遞給哪個線程是不確定的。如果信號與硬件故障或計時器超時相關,該信號就被髮送到引起該事件的線程中去。但是alarm 定時器是所有線程共享的資源,所以在多個線程中同時使用alarm 還是會互相干擾。
在進程中可以調用 sigprocmask 來阻止信號發送,但在多線程的進程中它的行爲並沒有定義,它可以不做任何事情。在主線程中調用 pthread_sigmask 使得所有線程都阻塞某個信號,也可以在某個線程中調用它來設置自己的掩碼。
/* 線程與信號 */
/*
* 函數功能:設置線程的信號屏蔽字;
* 返回值:若成功則返回0,否則返回錯誤編碼;
* 函數原型:
*/
#include <signal.h>
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oset);
/*
* 說明:
* 該函數的功能基本上與前面介紹的在進程中設置信號屏蔽字的函數sigprocmask相同;
*/
/*
* 函數功能:等待一個或多個信號發生;
* 返回值:若成功則返回0,否則返回錯誤編碼;
* 函數原型:
*/
int sigwait(const sigset_t *set, int *signop);
/*
* 說明:
* set參數指出線程等待的信號集,signop指向的整數將作爲返回值,表明發送信號的數量;
*/
/*
* 函數功能:給線程發送信號;
* 返回值:若成功則返回0,否則返回錯誤編碼;
* 函數原型:
*/
int pthread_kill(pthread_t thread, int signo);
/*
* 說明:
* signo可以是0來檢查線程是否存在,若信號的默認處理動作是終止整個進程,那麼把信號傳遞給某個線程仍然會殺死整個進程;
*/
如果信號集中的某個信號在sigwait 調用的時候處於未決狀態,那麼sigwait 將立即無阻塞的返回,在返回之前,sigwait 將從進程中移除那些處於未決狀態的信號。爲了避免錯誤動作的發生,線程在調用sigwait 之前,必須阻塞那些它正在等待的信號。sigwait 函數會自動取消信號集的阻塞狀態,直到新的信號被遞送。在返回之前,sigwait 將恢復線程的信號屏蔽字
線程與進程
多線程的父進程調用 fork 函數創建子進程時,子進程繼承了整個地址空間的副本。子進程裏面只有一個線程,它是父進程中調用 fork 函數的線程的副本。在子進程中的線程繼承了在父進程中相同的狀態,即有相同的互斥鎖和條件變量。如果父進程中的線程佔用鎖,則子進程也同樣佔有這些鎖,只是子進程不包含佔有鎖的線程的副本,所以並不知道具體佔有哪些鎖並且需要釋放哪些鎖。
如果子進程從 fork 返回之後沒有立即調用 exec 函數,則需要調用 fork 處理程序清理鎖狀態。可以調用 pthread_atfork 函數實現清理鎖狀態:
/* 線程和 fork */
/*
* 函數功能:清理鎖狀態;
* 返回值:若成功則返回0,否則返回錯誤編碼;
* 函數原型:
*/
#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
/*
* 說明:
* 該函數最多可以安裝三個幫助清理鎖的函數;
* prepare fork處理程序由父進程在fork創建子進程前調用,這個fork處理程序的任務是獲取父進程定義的所有鎖;
*
* parent fork處理程序是在fork創建子進程以後,但在fork返回之前在父進程環境中調用的,這個fork處理程序的任務是對prepare fork處理程序
* 獲取的所有鎖進行解鎖;
*
* child fork處理程序在fork返回之前在子進程環境中調用,與parent fork處理程序一樣,child fork處理程序必須釋放prepare fork處理程序獲得的所有鎖;
*/
參考資料:
《Unix 網絡編程》