Linux之線程
- 線程之形
- 線程接口
- 線程安全
- 互斥鎖
- 條件變量&信號量
- 生產者與消費者模型
- 線程池
線程之形
進程是資源分配的基本單位,而線程是進程內部的一個執行單元,也是 CPU 調度的基本單位。
線程之間共享進程地址空間、文件描述符與信號處理,但也有獨立資源:寄存器、棧、線程ID和調度優先級等。
線程切換由于地址空間相同,上下文切換時寄存器切換和內存地址緩存諸如頁表緩存TLB(快表)及硬件緩存切換成本比進程切換低得多。
Linux下的線程并非標準的、獨立于進程實現的線程(參見windows實現線程),而是通過pthread庫封裝了輕量級進程LWP(light weight process)來模擬線程。具體的說法,詳見下文。
說Linux的線程是用LWP模擬的意思是,Linux利用了進程的概念和機制來實現線程的功能,通過讓不同的線程共享某些資源來達到所謂的“輕量級”效果。這種方式使得線程既能夠享受到與進程相似的隔離性和保護性,又能夠在同一程序內部高效地進行數據交換和通信。另,ps -aL查看LWP即線程信息。之前的ps -axj只能查看進程級別信息。
線程接口
- 線程創建:
int pthread_create(pthread_t *restrict thread,const pthread_attr_t *restrict attr,void *(*start_routine)(void *),void *restrict arg);
,thread輸出型參數,存儲用戶層線程ID;attr線程屬性,給NULL即可;start_routine線程執行函數;arg傳給前者的參數。
pthread_create是glibc庫提供的接口,它實際上,調用mmap()在堆區分配struct pthread(線程控制塊 TCB,庫描述與管理線程的結構體,存儲內核級tid,線程棧指針與大小,TLS指針和線程狀態、返回值等)和固定大小的線程棧以及線程局部存儲(TLS,存儲私有變量如errno),再調用int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ... /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
fn即線程執行函數,arg其參數,child_stack傳mmap出來的線程棧棧底地址,flags選項較多自行了解。然后,內核再創建一個task_struct描述新線程,其mm_struct指向同一個地址空間,pid實為內核級tid(t即thread),tgid線程組id為進程id(主線程tid和pid和進程pid是同一個)。而mmap區的struct pthread的起始虛擬地址即為用戶層線程ID,作為上面函數第一個參數帶出。
線程局部存儲,在全局內置類型變量前加__thread修飾,使之成為各個線程局部存儲區變量而非全局區變量。另外,pthread_setname_np(pthread_t,const char*)可將變量放至局部存儲,實現取名字;pthread_getname_np(pthread_t,char*,size_t)得之。
- 線程終止:
void pthread_exit(void *retval);
終止當前線程,retval即線程返回值。注意,exit()終止進程。 - 等待線程結束:
int pthread_join(pthread_t thread, void **retval);
阻塞等待指定ID線程,retval帶出上面的返回值。而線程返回值在終止后會被存放在struct pthread里,等join時拿出來。 - 線程分離:
int pthread_detach(pthread_t thread);
將指定線程置于分離狀態,系統會在該線程結束后自動回收其資源而無需等待。可搭配pthread_t pthread_self(void);分離自己。
線程終止還包括在執行函數return和線程取消int pthread_cancel(pthread_t thread);
,后者給join的retval為-1。如果調用join則會釋放其struct pthread和線程棧以及task_struct,如果detach則結束自動銷毀它們。注意,動態開辟的堆區內存需要手動釋放,否則內存泄漏。
線程安全
線程安全指多個線程并發訪問共享資源時不出因為線程切換等引發的問題。通常需要互斥鎖、條件變量等方案來保證。
互斥鎖
臨界資源是一次僅允許一個執行流使用的共享資源,臨界區是訪問臨界資源的那段代碼,互斥是多線程并發競爭式訪問臨界資源時、同一時間只有一個線程能進入臨界區的情況。
互斥鎖實際上就是同一時刻只讓拿到鎖的線程進入臨界區,讓并行變成串行訪問。
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
初始化鎖,后參為屬性通常NULL,全局鎖也可用PTHREAD_MUTEX_INITIALIZER初始化;pthread_mutex_lock(pthread_mutex_t *mutex);
阻塞式獲取鎖,非阻塞與定時自行了解;pthread_mutex_unlock(pthread_mutex_t *mutex);
釋放鎖;pthread_mutex_destroy(pthread_mutex_t *mutex);
銷毀鎖。
鎖本身作為臨界資源被訪問,多線程并發去lock時是怎么保證互斥的?
偽代碼如下:
lock:movb $0, %al//原子性將al寫入0xchgb %al, mutex//原子性交換鎖和al的內容,鎖初始為1if(al寄存器的內容 > 0)return 0;//搶到鎖了else掛起等待;goto lock;
unlock:movb $1, mutex//原子喚醒等待Mutex的線程;return 0;
原子性指一個操作只有01兩態,即有無兩態,無中間態。原子操作就是遵守原子性的操作。上面鎖的代碼中寫入movb和交換xchgb為原子操作,如果一個線程搶到了鎖,則mutex里為0,al里為1,其他線程永遠拿不到這個1,因為mutex里為0,除非該線程unlock,再把1放回mutex。
關于RAII風格的lockguard,即構造lock mutex,析構unlock之。
條件變量&信號量
同步值在互斥前提下,多線程按序訪問臨界資源。
條件變量Condition即允許多線程排隊等待某個條件變量就緒,等候通知從而按序訪問臨界資源。
pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
初始化條件變量,也可PTHREAD_COND_INITIALIZER;pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
等待條件,后參為互斥鎖,該函數會1先解鎖,既防止死鎖(因為其他線程無法拿到鎖并修改條件、發出通知)也提高性能,允許其他線程訪問臨界資源,2阻塞等待,進入條件變量的等待隊列中,3一旦被喚醒成功,重新獲取并上原來的鎖。該函數應在while(檢測條件)內部使用,因為存在偽喚醒(比如因為系統中斷或異常處理、內核優化等導致調度變化)或等待失敗、函數返回的情況;pthread_cond_signal(pthread_cond_t *cond);
喚醒一個等待該條件的線程;pthread_cond_broadcast(pthread_cond_t *cond);
喚醒所有;pthread_cond_destroy(pthread_cond_t *cond);
銷毀之。
信號量Semaphore(此處是POSIX標準下的)是一種預定機制,想象去電影院前先買票再排隊入場,信號量就是票,所有人互斥(通過原子操作保證)搶票來預定座位,然后排隊進場看電影。
int sem_init(sem_t *sem, int pshared, unsigned int value);
初始化信號量,二參0為線程間共享、非0進程間共享,三參為初始值;sem_wait(sem_t *sem); // 如果信號量值大于0,則減1;否則阻塞
P操作,申請信號量即信號量減1,類比買票;sem_post(sem_t *sem);
V操作,釋放信號量即信號量加1,類比退票或放票。
生產者與消費者模型
多個生產者并行生產商品,串行投入商品至超市,多個消費者串行拿取商品出超市,并行消費商品。
3種關系,生產者之間互斥,消費者之間互斥,生消之間互斥且同步;2種角色,生消;1個交易場所,以特定結構構成的內存空間。
可以通過阻塞隊列存放任務+鎖+條件變量同步等待(生產者生產任務了通知等待的消費者、滿了等待消費者通知,消費者消費任務了通知等待的生產者、完了等待生產者通知)實現,也可以通過環形隊列存放任務+鎖+信號量(生產者對空位量P操作、任務量V操作,消費者對空位量V操作、任務量P操作)實現。
線程池
提前創造一批線程,等待任務到來,來一個召喚一個線程去處理一個。可采用鎖+條件變量的方式實現。
最后兩個模塊的代碼具體實現,還沒考慮好是否要呈現以及如果要呈現的話要怎么呈現,所以只提供了思路。
線程over,終于進入網絡部分了,理論系統學習生涯結束指日可待啊!