天天看點

Linux多線程程式設計

線程分類

    線程按照其排程者可以分為使用者級線程和核心級線程兩種。

    (1)使用者級線程 

    使用者級線程主要解決的是上下文切換的問題,它的排程算法和排程過程全部由使用者自行選擇決定,在運作時不需要特定的核心支援。在這裡,作業系統往往會提供一個使用者空間的線程庫,該線程庫提供了線程的建立、排程、撤銷等功能,而核心仍然僅對程序進行管理。如果一個程序中的某一個線程調用了一個阻塞的系統調用,那麼該程序包括該程序中的其他所有線程也同時被阻塞。這種使用者級線程的主要缺點是在一個程序中的多個線程的排程中無法發揮多處理器的優勢。

    (2)核心級線程 

    這種線程允許不同程序中的線程按照同一相對優先排程方法進行排程,這樣就可以發揮多處理器的并發優勢。 

    現在大多數系統都采用使用者級線程與核心級線程并存的方法。一個使用者級線程可以對應一個或幾個核心級線程,也就是“一對一”或“多對一”模型。這樣既可滿足多處理機系統的需要,也可以最大限度地減少排程開銷。

線程建立的linux實作

    linux的線程實作是在核外進行的,核内提供的是建立程序的接口do_fork()。核心提供了兩個系統調用clone()和fork(),最終都用不同的參數調用do_fork()核内api。當然,要想實作線程,沒有核心對多程序(其實是輕量級程序)共享資料段的支援是不行的,是以,do_fork()提供了很多參數,包括clone_vm(共享記憶體空間)、clone_fs(共享檔案系統資訊)、 clone_files(共享檔案描述符表)、clone_sighand(共享信号句柄表)和clone_pid(共享程序id,僅對核内程序,即0号程序有效)。當使用fork系統調用時,核心調用do_fork()不使用任何共享屬性,程序擁有獨立的運作環境,而使用 pthread_create()來建立線程時,則最終設定了所有這些屬性來調用__clone(),而這些參數又全部傳給核内的do_fork(),進而建立的“程序”擁有共享的運作環境,隻有棧是獨立的,由__clone()傳入。

    linux線程在核内是以輕量級程序的形式存在的,擁有獨立的程序表項,而所有的建立、同步、删除等操作都在核外pthread庫中進行。pthread 庫使用一個管理線程(__pthread_manager(),每個程序獨立且唯一)來管理線程的建立和終止,為線程配置設定線程id,發送線程相關的信号(比如cancel),而主線程(pthread_create())的調用者則通過管道将請求資訊傳給管理線程。

主要函數說明

1.線程的建立和退出

    pthread_create 線程建立函數

    int pthread_create (pthread_t * thread_id,__const pthread_attr_t * __attr,void *(*__start_routine) (void *),void *__restrict __arg);

    線程建立函數第一個參數為指向線程辨別符的指針,第二個參數用來設定線程屬性,第三個參數是線程運作函數的起始位址,最後一個參數是運作函數的參數。這裡,我們的函數thread 不需要參數,是以最後一個參數設為空指針。第二個參數我們也設為空指針,這樣将生成預設屬性的線程。當建立線程成功時,函數傳回0,若不為0 則說明建立線程失敗,常見的錯誤傳回代碼為eagain 和einval。前者表示系統限制建立新的線程,例如線程數目過多了;後者表示第二個參數代表的線程屬性值非法。建立線程成功後,新建立的線程則運作參數三和參數四确定的函數,原來的線程則繼續運作下一行代碼。

    pthread_join 函數,來等待一個線程的結束。

    函數原型為:int pthread_join (pthread_t __th, void **__thread_return)

    第一個參數為被等待的線程辨別符,第二個參數為一個使用者定義的指針,它可以用來存儲被等待線程的傳回值。這個函數是一個線程阻塞的函數,調用它的函數将一直等待到被等待的線程結束為止,當函數傳回時,被等待線程的資源被收回。線程隻能被一個線程等待終止,并且應處于joinable狀态(非detached)。

    pthread_exit 函數

    一個線程的結束有兩種途徑,一種是線程運作的函數結束了,調用它的線程也就結束了;

    另一種方式是通過函數pthread_exit 來實作。它的函數原型為:void pthread_exit (void *__retval)唯一的參數是函數的傳回代碼,隻要pthread_join 中的第二個參數thread_return 不是null,這個值将被傳遞給thread_return。最後要說明的是,一個線程不能被多個線程等待,否則第一個接收到信号的線程成功傳回,其餘調用pthread_join 的線程則傳回錯誤代碼esrch。

2.線程屬性

    pthread_create函數的第二個參數線程的屬性。将該值設為null,也就是采用預設屬性,線程的多項屬性都是可以更改的。這些屬性主要包括綁定屬性、分離屬性、堆棧位址、堆棧大小、優先級。其中系統預設的屬性為非綁定、非分離、預設1m 的堆棧、與父程序同樣級别的優先級。下面首先對綁定屬性和分離屬性的基本概念進行講解。 

    綁定屬性:linux中采用“一對一”的線程機制,也就是一個使用者線程對應一個核心線程。綁定屬性就是指一個使用者線程固定地配置設定給一個核心線程,因為cpu時間片的排程是面向核心線程 (也就是輕量級程序)的,是以具有綁定屬性的線程可以保證在需要的時候總有一個核心線程與之對應。而與之相對的非綁定屬性就是指使用者線程和核心線程的關系不是始終固定的,而是由系統來控制配置設定的。 

    分離屬性:分離屬性是用來決定一個線程以什麼樣的方式來終止自己。在非分離情況下,當一個線程結束時,它所占用的系統資源并沒有被釋放,也就是沒有真正的終止。隻有當pthread_join()函數傳回時,建立的線程才能釋放自己占用的系統資源。而在分離屬性情況下,一個線程結束時立即釋放它所占有的系統資源。

    這裡要注意的一點是,如果設定一個線程的分離屬性,而這個線程運作又非常快,那麼它很可能在pthread_create 函數傳回之前就終止了,它終止以後就可能将線程号和系統資源移交給其他的線程使用,這時調用pthread_create 的線程就得到了錯誤的線程号。

設定綁定屬性:

int pthread_attr_init(pthread_attr_t *attr) 

int pthread_attr_setscope(pthread_attr_t *attr, int scope)

int pthread_attr_getscope(pthread_attr_t *tattr, int *scope)

scope:pthread_scope_system:綁定,此線程與系統中所有的線程競争 pthread_scope_process:非綁定,此線程與程序中的其他線程競争

設定分離屬性:

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)

int pthread_attr_getdetachstate(const pthread_attr_t *tattr,int *detachstate)

detachstate pthread_create_detached:分離 pthread _create_joinable:非分離

設定排程政策:

int pthread_attr_setschedpolicy(pthread_attr_t * tattr, int policy)

int pthread_attr_getschedpolicy(pthread_attr_t * tattr, int *policy)

policy sched_fifo:先入先出 sched_rr:循環 sched_other:實作定義的方法

設定優先級:

int pthread_attr_setschedparam (pthread_attr_t *attr, struct sched_param *param) 

int pthread_attr_getschedparam (pthread_attr_t *attr, struct sched_param *param)

3.線程通路控制

1)互斥鎖(mutex)

通過鎖機制實作線程間的同步。同一時刻隻允許一個線程執行一個關鍵部分的代碼。

1 int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);

2 int pthread_mutex_lock(pthread_mutex_t *mutex);

3 int pthread_mutex_unlock(pthread_mutex_t *mutex);

4 int pthread_mutex_destroy(pthread_mutex_t *mutex);

(1)先初始化鎖init()或靜态指派pthread_mutex_t mutex=pthread_mutex_initialier

(2)加鎖,lock,trylock,lock阻塞等待鎖,trylock立即傳回ebusy

(3)解鎖,unlock需滿足是加鎖狀态,且由加鎖線程解鎖

(4)清除鎖,destroy(此時鎖必需unlock,否則傳回ebusy)

    mutex 分為遞歸(recursive) 和非遞歸(non-recursive)兩種,這是posix 的叫法,另外的名字是可重入(reentrant) 與非可重入。這兩種mutex 作為線程間(inter-thread) 的同步工具時沒有差別,它們的惟一差別在于:同一個線程可以重複對recursive mutex 加鎖,但是不能重複對non-recursive mutex 加鎖。

    首選非遞歸mutex,絕對不是為了性能,而是為了展現設計意圖。non-recursive 和recursive 的性能差别其實不大,因為少用一個計數器,前者略快一點點而已。在同一個線程裡多次對non-recursive mutex 加鎖會立刻導緻死鎖,我認為這是它的優點,能幫助我們思考代碼對鎖的期求,并且及早(在編碼階段)發現問題。毫無疑問recursive mutex 使用起來要友善一些,因為不用考慮一個線程會自己把自己給鎖死了,我猜這也是java 和windows 預設提供recursive mutex 的原因。(java 語言自帶的intrinsic lock 是可重入的,它的concurrent 庫裡提供reentrantlock,windows的critical_section 也是可重入的。似乎它們都不提供輕量級的non-recursive mutex。)

2)條件變量(cond)

利用線程間共享的全局變量進行同步的一種機制。

1 int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);     

2 int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);

3 int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex,const timespec *abstime);

4 int pthread_cond_destroy(pthread_cond_t *cond);  

5 int pthread_cond_signal(pthread_cond_t *cond);

6 int pthread_cond_broadcast(pthread_cond_t *cond);  //解除所有線程的阻塞

(1)初始化. init()或者pthread_cond_t cond=pthread_cond_initialier;屬性置為null

(2)等待條件成立. pthread_cond_wait,pthread_cond_timedwait. 

    wait()釋放鎖,并阻塞等待條件變量為真

    timedwait()設定等待時間,仍未signal,傳回etimeout(加鎖保證隻有一個線程wait)

(3)激活條件變量:pthread_cond_signal,pthread_cond_broadcast(激活所有等待線程)

(4)清除條件變量:destroy; 無線程等待,否則傳回ebusy

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);

這兩個函數一定要在mutex的鎖定區域内使用。

    調用 pthread_cond_signal() 釋放被條件阻塞的線程時,如果沒有任何線程基于條件變量阻塞,則調用pthread_cond_signal()不起作用。而對于 windows,當調用 setevent 觸發 auto-reset 的 event 條件時,如果沒有被條件阻塞的線程,那麼此函數仍然起作用,條件變量會處在觸發狀态。

linux下生産者消費者問題(使用互斥鎖和條件變量):

Linux多線程程式設計

view code

3)信号量

如同程序一樣,線程也可以通過信号量來實作通信,雖然是輕量級的。

信号量函數的名字都以"sem_"打頭。線程使用的基本信号量函數有四個。

#include <semaphore.h>

int sem_init(sem_t *sem , int pshared, unsigned int value);

這是對由sem指定的信号量進行初始化,設定好它的共享選項(linux隻支援為0,即表示它是目前程序的局部信号量),然後給它一個初始值value。

兩個原子操作函數:這兩個函數都要用一個由sem_init調用初始化的信号量對象的指針做參數。

int sem_wait(sem_t *sem); //給信号量減1,對一個值為0的信号量調用sem_wait,這個函數将會等待直到有其它線程使它不再是0為止。

int sem_post(sem_t *sem); //給信号量的值加1

int sem_destroy(sem_t *sem);

這個函數的作用是再我們用完信号量後都它進行清理。歸還自己占有的一切資源。

用信号量實作生産者消費者:

    這裡使用4個信号量,其中兩個信号量occupied和empty分别用于解決生産者和消費者線程之間的同步問題,pmut用于多個生産者之間互斥問題,cmut是用于多個消費者之間互斥問題。其中empty初始化為n(有界緩區的空間元數),occupied初始化為0,pmut和cmut初始化為1。

參考代碼:

Linux多線程程式設計
Linux多線程程式設計
Linux多線程程式設計

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。

http://www.cnblogs.com/luxiaoxun/archive/2012/10/06/2712999.html