Apue學習:線程

線程標識

#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 *。指針之間的比較其實比較的是這兩個指針是否指向同一個地址。

參考

POSIX多線程程序設計

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章