線程標識
#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
pthread_t pthread_self(void);
線程創建
Unix中進程起始時只有一個master threads,除非使用pthread_create來創建更多的線程。
#include <pthread.h>
int phread_create(pthread_t *restrict tidp,
const pthread_attr_t *restrict attr,
void *(*start_rtn)(void *),
void *restrict arg);
//成功返回0,出錯返回錯誤編碼
說明:
注意tidp保存的是線程標識符,線程的屬性有attr指定,或者用NULL表示使用默認屬性,線程創建後從start_rtn開始執行,start_rtn只有一個void *參數,用arg表示。
注意:
- 當一個線程創建之後,線程有權限使用進程空間,以及會繼承calling thread的floating-point
environment以及signal mask,但是pending
signals會被清除。即,如果在A線程中創建B線程,那麼B線程會繼承A線程的signal mask,但是B線程的peding
signal會被清空。
pthread的出錯方式是返回一個錯誤代碼,並不是設置環境變量errno - 使用pthread_create時的注意事項:
並不是在pthread_create返回之後,新線程纔去運行,而是一旦新線程創建完畢,那麼就是新線程與其他線程就會去進程cpu,誰拿到了,誰就去執行。而在pthread_create函數體類,新線程就別創建完畢了,這樣新線程就可能會去執行,而此時ntid卻還沒有別初始化完畢,所以在新線程中我們使用了pthread_self來獲取新新線程的id,而沒有用tid。
線程終止
終止方式
線程終止的方式主要有3種:
1.從線程中return
2.線程被其他線程取消
3.線程中調用pthrea_exit
另外需要注意的是,在任意一個進程中的線程裏面調用eixt(),_exit()都會造成該進程的終止。
pthread_exit函數與pthread_join函數
#include <pthread.h>
void pthread_exit(void *rval_ptr);
int pthread_join(pthread_t thread, void **rval_ptr);
說明:
pthread_exit與pthrea_join
- 1.如果在A線程中創建了B線程,B線程沒有被分離,那麼B線程就相當於A線程的子線程,在B線程調用pthread_exit()後,A線程可以調用pthread_join函數來獲得B線程的一些情況。
- 2.如果B線程被分離出去,那麼A線程調用pthread_join就會返回EINVAL。
- 3.如果線程是被取消的,那麼pthread_join中的rval_ptr所指向的內存地址所指向的內容就被設置爲PTHREAD_CANCELED.
- 4.pthread_join會阻塞線程,直到指定線程退出
注意點:
- 注意的是pthread_exit(void *rval_ptr),這個值是一個rval_ptr,在線程調用這個函數後,rval_ptr就會指向一個內存地址。 而在另一個線程中調用pthread_join(pthrea_d thtrea, void
**rval_ptr),rval_ptr所指向的就是rval_ptr指向的內容。 所以若果一個線程在退出後想要返回一些信息,那麼這個信息最好不要在線程的棧空間中保留,最好使用malloc把信息保存在堆中,這樣這些信息就不會隨着線程的退出而無效。 - 另外需要注意的是一個線程調用pthread_exit(),那麼線程退出後,就會調用線程的退出清理函數,類似於進程的atexit()所註冊的函數。而在線程的啓動例程中調用return就不會調用線程的退出清理函數。
pthread_cancel函數
int pthread_cancel(pthread_t tid)
表明我這個線程想要取消tid這個線程。注意只是表示我想要取消,我並不會等到線程真正取消。
pthread_cleanup_push/pop
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);
說明:
作用類似於atexit()
注意:
rtn函數只有在3中情況下會執行:
1.線程調用pthread_exit
2.線程響應別的線程的取消操作
3.使用pthread_cleanup_pop,並且pexecute的參數非0
4.push 與 pop要成對出現
解釋:
1.如果execute參數爲0,那麼就pthread_cleanup_pop函數就什麼都不做。
2.如果線程是從主例程中使用return返回,那麼就不會執行rtn。主例程指的是創建時使用的start_fun。所以想要執行清理函數要使用pthread_exit。
注意點:
默認情況下,線程的終止狀態會保持到我們調用pthread_join。但是如果線程是處於分離狀態,那麼線程的終止狀態會被立即取回。pthread_join並不能作用於一個處於分離狀態的線程,這樣做會導致pthread_join返回錯誤。
線程同步
1.互斥量
互斥鎖:
互斥鎖指的是當我們訪問共享內容時,我們先上個鎖,在訪問結束之後再解開鎖。如果我們訪問共享內容時,這個共享內容已經被上鎖了,我們就會被阻塞起來,直到有個線程釋放了這個鎖,並且喚醒的是我們。
#include <pthread.h>
int pthread_mutex_init(pthread *restrict mutex,
const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(phtread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int phtread_mutex_unlock(pthread_mutex_t *mutex);
互斥鎖的操作,注意trylock不會造成阻塞,而lock會造成阻塞。
2.避免死鎖
解決死鎖的思路:
1.資源有序加鎖
2.使用pthread_mutex_trylock,成功的時候使用lock,不成功就放棄自己已經獲得的鎖,然後等一會之後重新開始加鎖。
pthread_mutex_timedlock
類似於lock,但是指定了我們可以鎖住的時間是多少,到時間就會返回ETIMEOUT並且釋放自己的鎖。
4.讀寫鎖
說明:
讀寫鎖:
一共有三種狀態:
1.寫鎖狀態
2.讀鎖狀態
3.沒有鎖狀態
寫鎖狀態下,要想再加鎖就會被阻塞住,無論你是想加讀鎖,還是想加寫鎖。在讀鎖狀態下,你可以加讀鎖,並且不會被阻塞,但是如果你想加寫鎖,你就會被鎖住。
注意如果在讀鎖模式下,你加了寫鎖,只能等待前面加上讀鎖的線程都釋放自己的鎖之後,纔可以加上寫鎖。但是你表明自己想要加寫鎖之後,後面的線程想加讀鎖的話就會被阻塞住。
讀寫鎖適合那些讀操作頻繁的程序。
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int phtread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
類似於互斥量的try。
注意當鎖是可以獲得的時候,返回0。否則返回錯誤代碼EBUSY。
5.條件變量
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,
pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cont_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict timeout);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
說明:
- 條件變量要用mutex來保護。
使用過程是:一個線程首先鎖定互斥量mutex,然後調用pthread_cond_wait(),這個函數有一個原子操作,即先解鎖,再把本線程掛起到等待隊列中。(之所以是先解鎖,是因爲掛起之後就不能佔用cpu操作了。)注意到爲什麼是原子操作,在於我們先解鎖了,所以防止解鎖後,條件有改變,而我們卻沒有捕獲到這個改變,所以要用原子操作。線程被阻塞後就要等待別人的線程讓條件爲真然後喚醒本線程。
這個函數返回之後,mutex會再次被鎖住。 - 可以使用signal和broadcast喚醒等待條件變量cond的線程。 signal喚醒至少一個。 broadcast喚醒所有。
注意點:
注意因爲signal與broadcast都有可能喚起多個線程,所有可能一個線程使用了之後,造成條件的再一次不滿足,這時就需要再次使用這個函數了。、
例子:
自旋鎖
自旋鎖會出現忙等,但是不會阻塞。
屏障
屏障允許線程等待,知道所有相關的線程到達了同一點,然後線程各自繼續執行。
int pthread_barrier_init(pthread_barrier_t *restrict barrier,
const pthread_barrierattr_t *restrict attr,
unsigned int count);
int pthread_barrier_destroy(pthread_barrier_t *barrier);
說明:
count指定了有多少個線程要達到這個屏障,纔可以進行下一步工作。
pthread_barrier_wait
int pthread_barrier_wait(pthread_barrier_t *barrier);
說明:
如果一個線程調用了pthread_barrier_wait之後:
1.如果等待在這個屏障上的線程的個數還沒有到達count,那麼這個線程會被阻塞。
2.如果在調用之後,count個數到達了,那麼所有的線程都會被喚醒。
注意:
- 注意只有一個線程會在返回時獲得的返回值爲:PTHREAD_BARRIER_SERIAL_THREAD,這個線程就相等於是一個主線程來處理所有線程處理的結果。
pthread_join就是一個屏障的簡單例子 - 另外要注意的是: 當barrier count到達了,並且所有線程都被解除阻塞,那麼barrier就可以被重用了。但是barrier
count並不可以修改,除非你調用了pthread_barrier_destroy函數,然後在使用init。
總結
線程是一個很重要的概念,他可以提供併發程度。線程可以看成是一個輕量級的進程。在程序開始運行是隻有一個主線程,我們可以在線程中創建多個線程,但是注意一個進程可以創建的線程是有上線的。
另外需要注意的線程退出時的一些情況。例如什麼時候會執行退出線程清理函數。在線程中調用exit()會怎麼樣。線程與線程之間的關係,線程與進程之間的關係。
另一部分就是線程同步問題了:主要有互斥量,讀寫鎖,自旋鎖,條件變量,屏障。注意死鎖問題,已經解決辦法。知道同步方法使用的情況有哪些。
另外在這章中看到了指針的另一種用法,注意指針是一個對象,他本身就是相當於一個整形值,我們只是在聲明的時候表示這個對象指向的是什麼類型。所以可以靈活運用void *。指針之間的比較其實比較的是這兩個指針是否指向同一個地址。