線程
在linux內核那一部分我們知道,線程其實就是一種特殊的進程,只是他們共享進程的文件和內存等資源,無論如何對于資源共享,就必須處理一致性的問題。
1.線程概念
一個典型的Unix程序可以看做只有一個線程:一個進程在某一時刻只能做一件事情。有了多線程控制以后,在程序設計時就可以把程序設計成在某一時刻能夠做不止一件事情,每個線程都能獨立處理各自獨立的任務。
每個線程包含有表示執行環節所必須的信息,其中包括線程ID、一組寄存器值、棧、調度優先級和策略、信號屏蔽字、errno變量以及線程私有數據。
一個進程的所有信息對該線程的所有線程都是共享的,包括可執行程序的代碼、程序的全局內存和堆內存、棧以及文件描述符。
2.線程標識
進程有進程的ID,而線程同樣有線程ID。進程ID在系統中是唯一標識,但線程ID不同,線程ID只有在它所屬的進程上下文中才有意義。
進程ID使用pid_t數據類型,而線程ID是用pthread_t數據類型標識,通常使用一個結構。下面函數用來比較兩個線程ID。
#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
/* 返回值:相等返回非0,否則返回0*/
獲取線程自身ID可以通過調用函數pthread_self。
#include <pthread.h>
pthread_t pthread_self(void);
/* 返回值:調用線程的線程ID */
3.線程創建
新增線程可以通過函數pthread_create函數創建。
#include <pthread.h>
int pthread_create(pthread_t *restrict tidp,
const pthread_attr_t *restrict attr,
void *(*start_rtn)(void *),
void *restrict arg);
/*返回值:成功返回0,否則返回錯誤編號*/
新創建的線程ID會設置為tidp
指向的內存單元;新創建的線程從start_rtn
函數的地址開始運行,從定義可以看到該函數沒有輸入參數,因此要傳遞參數,可以將所有參數放到一個結構中,然后把這個結構的地址作為arg
參數傳入。
線程創建時并不能保證哪個線程先執行。新創建的線程可以訪問進程的地址空間,并且繼承調用線程的浮點環境和信號屏蔽字,但是該線程的掛起信號會被清除。
下面實例創建了一個線程,打印了進程ID、新線程ID以及初始化線程的線程ID:
#include "apue.h"
#include <pthread.h>
pthread_t ntid;
void
printids(const char *s)
{
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("%s pid %lu tid %lu (0x%lx)\n", s, (unsigned long)pid,
(unsigned long)tid, (unsigned long)tid);
}
void *
thr_fn(void *arg)
{
printids("new thread: ");
return((void *)0);
}
int
main(void)
{
int err;
err = pthread_create(&ntid, NULL, thr_fn, NULL);
if (err != 0)
err_exit(err, "can't create thread");
printids("main thread:");
sleep(1);
exit(0);
}
上面的程序主要注意兩點:
- 主線程需要休眠,如果不休眠,可能主線程已經退出了,此時新建線程還沒機會運行
- 新線程通過pthread_self獲取自己的線程ID,而不是從共享內存中讀取。雖然調用pthread_create可以通過第一個參數
tidp
指定新線程的ID,但是如果新線程在主線程調用pthread_create返回之前運行了,那么新線程看到的是未經初始化的ntid
的內容。
4.線程終結
進程中任意線程調用了exit、_Exit或者_exit,那么整個進程就會終止。與此類似,如果默認的動作是終止進程,那么,發送到線程的信號就會終止整個進程。
單個線程有3種方式退出,而不終止進程。
- 線程可以簡單地從啟動例程中返回,返回值是線程的退出碼
- 線程可以被同一進程的其他線程取消
- 線程調用pthread_exit
#include <pthread.h>
void pthread_exit(void *rval_ptr);
rval_ptr
參數是一個無類型指針,與傳給啟動例程的單個參數類似。進程中的其他線程可以通過pthread_join函數訪問到這個指針。
#include <pthread.h>
int pthread_join(pthread_t thread, void **rval_ptr);
/* 返回值:成功返回0,失敗返回錯誤編號*/
調用線程將一直阻塞,直到指定的線程調用pthread_exit、從啟動例程中返回或者被取消。如果線程簡單地從啟動例程返回,rval_ptr
就包含返回碼。如果線程被取消,由rval_ptr
指定的內存單元就設置為PTHREAD_CANCELED。
可以通過pthread_join自動將線程置為分離狀態,這樣資源可以恢復。如果線程以及處于分離狀態,pthread_join調用會失敗,返回EINVAL。
如果對線程的返回值不感興趣,則可以把rval_ptr
設置為NULL。這種情況下,調用pthread_join函數可以等待指定的線程終止,但并不獲取線程的終止狀態。
下面實例展示如何獲取以及終止的線程的退出碼。
#include "apue.h"
#include <pthread.h>
void *
thr_fn1(void *arg)
{
printf("thread 1 returning\n");
return((void *)1);
}
void *
thr_fn2(void *arg)
{
printf("thread 2 exiting\n");
pthread_exit((void *)2);
}
int
main(void)
{
int err;
pthread_t tid1, tid2;
void *tret;
err = pthread_create(&tid1, NULL, thr_fn1, NULL);
if (err != 0)
err_exit(err, "can't create thread 1");
err = pthread_create(&tid2, NULL, thr_fn2, NULL);
if (err != 0)
err_exit(err, "can't create thread 2");
err = pthread_join(tid1, &tret);
if (err != 0)
err_exit(err, "can't join with thread 1");
printf("thread 1 exit code %ld\n", (long)tret);
err = pthread_join(tid2, &tret);
if (err != 0)
err_exit(err, "can't join with thread 2");
printf("thread 2 exit code %ld\n", (long)tret);
exit(0);
}
運行結果為:
可以看到進程1,2分別return或者exit時設定了返回碼,進程中其他線程可以通過調用pthread_join函數獲得該線程的退出碼。
需要注意的是pthread_create和pthread_exit函數的無類型指針參數可以傳遞的值不止一個,這個指針可以包含復雜信息的結構指針,同時注意,這個結構所使用的內存調用者在完成調用以后必須仍然有效(畢竟前面說了不能確定哪個線程先執行,如果調用線程先執行可能這個結構還沒傳遞給操作的對象線程)。
線程可以通過pthread_cancel函數來請求取消同一進程中的其他線程。
#include<pthread_cancel>
int pthread_cancel(pthread_t tid);
/* 返回值:成功返回0,失敗返回錯誤編號*/
在默認情況下,pthread_cancel函數會使得由tid
標識的線程的行為表現為如同調用了參數為PTHREAD_CANCELED的pthread_exit函數,但是線程可以選擇忽略取消或控制如何取消(所以它只是請求取消,而不是控制線程終止)。
線程可以安排它退出時需要的函數,這樣的函數稱為線程處理程序(thread cleanup handler)。一個線程可以建立多個清楚處理程序,由于處理程序記錄在棧中,因此他們的執行順序和注冊時相反:
#include <pthread.h>
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);
當線程執行以下動作時,清理函數rtn
是由pthread_cleanup_push函數調度的,調用時只有一個參數arg
:
- 調用pthread_exit時
- 響應取消請求時
- 用非0的
execute
參數調用pthread_cleanup_pop時
若execute
參數設置為0,清理函數將不被調用。上述任一情況下,pthread_cleanup_pop都將刪除上次pthread_cleanup_push調用建立的清除處理程序。
下面通過一個實例來說明線程清理處理程序的使用。
#include "apue.h"
#include <pthread.h>
void
cleanup(void *arg)
{
printf("cleanup: %s\n", (char *)arg);
}
void *
thr_fn1(void *arg)
{
printf("thread 1 start\n");
pthread_cleanup_push(cleanup, "thread 1 first handler");
pthread_cleanup_push(cleanup, "thread 1 second handler");
printf("thread 1 push complete\n");
if (arg)
return((void *)1);
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
return((void *)1);
}
void *
thr_fn2(void *arg)
{
printf("thread 2 start\n");
pthread_cleanup_push(cleanup, "thread 2 first handler");
pthread_cleanup_push(cleanup, "thread 2 second handler");
printf("thread 2 push complete\n");
if (arg)
pthread_exit((void *)2);
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
pthread_exit((void *)2);
}
int
main(void)
{
int err;
pthread_t tid1, tid2;
void *tret;
err = pthread_create(&tid1, NULL, thr_fn1, (void *)1);
if (err != 0)
err_exit(err, "can't create thread 1");
err = pthread_create(&tid2, NULL, thr_fn2, (void *)1);
if (err != 0)
err_exit(err, "can't create thread 2");
err = pthread_join(tid1, &tret);
if (err != 0)
err_exit(err, "can't join with thread 1");
printf("thread 1 exit code %ld\n", (long)tret);
err = pthread_join(tid2, &tret);
if (err != 0)
err_exit(err, "can't join with thread 2");
printf("thread 2 exit code %ld\n", (long)tret);
exit(0);
}
我的電腦是Mac執行結果如下(Linux下不會出錯):
Mac出錯的原因是pthread_cleanup_pop和pthread_cleanup_push這兩個函數是用宏實現的,而宏把某些上下文放在棧上。線程1在調用push和pop之間返回時,棧已經被改寫,而Mac在調用清除處理程序時就用了這個改寫的上下文。
在默認情況下,線程的終止狀態會保存直到對線程調用pthread_join。如果線程已經被分離,則線程的底層存儲資源可以在線程終止時立即被返回。在線程被分離后,我們不能用pthread_join函數等待它的終止狀態,因為對分離狀態的線程調用pthread_join會產生未定義行為。可以調用pthread_detach分離線程。
#include <pthread.h>
int pthread_detach(pthread_t tid);
/* 返回值:成功返回0,失敗返回錯誤編號*/
關于分離狀態的線程內容,在線程控制這篇文章有介紹。
5.線程同步
由于多個線程之間共享相同的內存,因此需要同步技術來保證每個線程看到相同的數據視圖。
5.1互斥量
可以使用pthread的互斥接口來保護數據,確保同一時間只有一個線程訪問數據。由Linux內核部分的知識可以知道互斥量本質上是一把鎖,在訪問共享資源前對互斥量進行加鎖,在訪問之后釋放鎖。如果釋放互斥量時有一個以上的線程阻塞,那么所有該鎖上的阻塞線程都會變成可運行狀態,第一個變成運行的線程就可以對互斥量加鎖,其他線程繼續阻塞。因此每次只有一個線程可以執行。
互斥變量是用pthrea_mutex_t數據表示的。在使用互斥變量之前,必須對它進行初始化,可以把它設置為常量PTHREAD_MUTEX_INITIALIZER(只適用于靜態分配),也可以通過調用pthread_mutex_init函數進行初始化。若動態分配互斥量(比如malloc分配),在釋放內存前需要調用pthread_mutex_destroy。
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
/* 返回值:成功返回0,失敗返回錯誤編號*/
要用默認的屬性初始化互斥量只需要把attr
設為NULL。
對互斥量加鎖需要調用pthread_mutex_lock。如果互斥量已經上鎖,則調用用線程將阻塞直到互斥量被解鎖。對互斥量解鎖,需要調用pthread_mutex_unlock。
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex);
/* 返回值:成功返回0,失敗返回錯誤編號*/
如果線程不希望被阻塞,它可以使用pthread_mutex_trylock嘗試對互斥量進行加鎖。如果調用pthread_mutex_trylock時互斥量處于未鎖住狀態,那么pthread_mutex_trylock將鎖住互斥量,不會出現阻塞直接返回0,否則pthread_mutex_trylock就會失敗,并返回EBUSY。
下面一個實例描述了前面所述的函數用法,線程引用結構體時首先獲取結構體的鎖,然后對引用值+1,減引用時,同樣先獲取線程鎖,防止其他線程修改結構體的值,判定當前結構體的引用次數。
#include <stdlib.h>
#include <pthread.h>
struct foo {
int f_count;
pthread_mutex_t f_lock;
int f_id;
/* ... more stuff here ... */
};
struct foo *
foo_alloc(int id) /* allocate the object */
{
struct foo *fp;
if ((fp = malloc(sizeof(struct foo))) != NULL) {
fp->f_count = 1;
fp->f_id = id;
if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
free(fp);
return(NULL);
}
/* ... continue initialization ... */
}
return(fp);
}
void
foo_hold(struct foo *fp) /* add a reference to the object */
{
pthread_mutex_lock(&fp->f_lock);
fp->f_count++;
pthread_mutex_unlock(&fp->f_lock);
}
void
foo_rele(struct foo *fp) /* release a reference to the object */
{
pthread_mutex_lock(&fp->f_lock);
if (--fp->f_count == 0) { /* last reference */
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_destroy(&fp->f_lock);
free(fp);
} else {
pthread_mutex_unlock(&fp->f_lock);
}
}
5.2避免死鎖
如果線程試圖對同一互斥量加鎖兩次,那么它自身就會陷入死鎖狀態。除此之外,如果兩個線程相互請求對方占用的互斥量,同樣會產生死鎖,對于這種情況,通常需要設定程序的執行順序,下面的程序實例就是實現該功能。
#include <stdlib.h>
#include <pthread.h>
#define NHASH 29
#define HASH(id) (((unsigned long)id)%NHASH)
struct foo *fh[NHASH];
pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER;
struct foo {
int f_count;
pthread_mutex_t f_lock;
int f_id;
struct foo *f_next; /* protected by hashlock */
/* ... more stuff here ... */
};
struct foo *
foo_alloc(int id) /* allocate the object */
{
struct foo *fp;
int idx;
if ((fp = malloc(sizeof(struct foo))) != NULL) {
fp->f_count = 1;
fp->f_id = id;
if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
free(fp);
return(NULL);
}
idx = HASH(id);
pthread_mutex_lock(&hashlock);
fp->f_next = fh[idx];
fh[idx] = fp;
pthread_mutex_lock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
/* ... continue initialization ... */
pthread_mutex_unlock(&fp->f_lock);
}
return(fp);
}
void
foo_hold(struct foo *fp) /* add a reference to the object */
{
pthread_mutex_lock(&fp->f_lock);
fp->f_count++;
pthread_mutex_unlock(&fp->f_lock);
}
struct foo *
foo_find(int id) /* find an existing object */
{
struct foo *fp;
pthread_mutex_lock(&hashlock);
for (fp = fh[HASH(id)]; fp != NULL; fp = fp->f_next) {
if (fp->f_id == id) {
foo_hold(fp);
break;
}
}
pthread_mutex_unlock(&hashlock);
return(fp);
}
void
foo_rele(struct foo *fp) /* release a reference to the object */
{
struct foo *tfp;
int idx;
pthread_mutex_lock(&fp->f_lock);
if (fp->f_count == 1) { /* last reference */
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_lock(&hashlock);
pthread_mutex_lock(&fp->f_lock);
/* need to recheck the condition */
if (fp->f_count != 1) {
fp->f_count--;
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
return;
}
/* remove from list */
idx = HASH(fp->f_id);
tfp = fh[idx];
if (tfp == fp) {
fh[idx] = fp->f_next;
} else {
while (tfp->f_next != fp)
tfp = tfp->f_next;
tfp->f_next = fp->f_next;
}
pthread_mutex_unlock(&hashlock);
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_destroy(&fp->f_lock);
free(fp);
} else {
fp->f_count--;
pthread_mutex_unlock(&fp->f_lock);
}
}
上面程序中使用了兩個互斥量,通過hashlock
互斥量保護散列表fh
和f_next
。foo
結構中的f_lock
互斥量保護對foo
結構中的其他字段的訪問。訪問這兩個互斥量時總是以相同的順序加鎖,避免死鎖。在釋放函數foo_rele
為了保證加鎖的順序,實現過程過于復雜,下面給出一個優化版本。
#include <stdlib.h>
#include <pthread.h>
#define NHASH 29
#define HASH(id) (((unsigned long)id)%NHASH)
struct foo *fh[NHASH];
pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER;
struct foo {
int f_count; /* protected by hashlock */
pthread_mutex_t f_lock;
int f_id;
struct foo *f_next; /* protected by hashlock */
/* ... more stuff here ... */
};
struct foo *
foo_alloc(int id) /* allocate the object */
{
struct foo *fp;
int idx;
if ((fp = malloc(sizeof(struct foo))) != NULL) {
fp->f_count = 1;
fp->f_id = id;
if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
free(fp);
return(NULL);
}
idx = HASH(id);
pthread_mutex_lock(&hashlock);
fp->f_next = fh[idx];
fh[idx] = fp;
pthread_mutex_lock(&fp->f_lock);
pthread_mutex_unlock(&hashlock);
/* ... continue initialization ... */
pthread_mutex_unlock(&fp->f_lock);
}
return(fp);
}
void
foo_hold(struct foo *fp) /* add a reference to the object */
{
pthread_mutex_lock(&hashlock);
fp->f_count++;
pthread_mutex_unlock(&hashlock);
}
struct foo *
foo_find(int id) /* find an existing object */
{
struct foo *fp;
pthread_mutex_lock(&hashlock);
for (fp = fh[HASH(id)]; fp != NULL; fp = fp->f_next) {
if (fp->f_id == id) {
fp->f_count++;
break;
}
}
pthread_mutex_unlock(&hashlock);
return(fp);
}
void
foo_rele(struct foo *fp) /* release a reference to the object */
{
struct foo *tfp;
int idx;
pthread_mutex_lock(&hashlock);
if (--fp->f_count == 0) { /* last reference, remove from list */
idx = HASH(fp->f_id);
tfp = fh[idx];
if (tfp == fp) {
fh[idx] = fp->f_next;
} else {
while (tfp->f_next != fp)
tfp = tfp->f_next;
tfp->f_next = fp->f_next;
}
pthread_mutex_unlock(&hashlock);
pthread_mutex_destroy(&fp->f_lock);
free(fp);
} else {
pthread_mutex_unlock(&hashlock);
}
}
5.3函數pthread_mutex_timedlock
線程獲取一個已經加鎖的互斥量時,pthread_mutex_timedlock允許綁定線程設定最長阻塞時間。
#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict tsptr);
/* 返回值:成功返回0,失敗返回錯誤編號 */
這個超時時間用timespec
結構表示,它用秒和納秒來描述時間。
5.4讀寫鎖
讀寫鎖可以有3種狀態:讀模式下加鎖狀態,寫模式下加鎖狀態,不加鎖狀態。一次只有一個線程可以占有寫模式的讀寫鎖,但是多個線程可以同時占有讀模式的讀寫鎖。
讀寫鎖是寫加鎖時,在這個鎖被解鎖之前,所有試圖對這個鎖加鎖的線程都會被阻塞。當讀寫鎖在讀加鎖狀態時,所有試圖以讀模式對它進行加鎖的線程都可以得到訪問權,但是任何希望以寫模式對此鎖進行加鎖的線程都會阻塞。當讀寫鎖處于讀模式鎖住的狀態時,如果有一個線程試圖以寫模式獲取鎖時,讀寫鎖通常會阻塞隨后的讀模式鎖請求。
與互斥量相比,讀寫鎖在使用之前必須初始化,在釋放他們底層的內存之前必須銷毀。
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
/* 返回值:成功返回0,失敗返回錯誤編號 */
在讀模式下加鎖,寫模式下加鎖分別用到pthread_rwlock_rdlock和pthread_rwlock_wrlock。
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
/* 返回值:成功返回0,失敗返回錯誤編號*/
同樣對于讀寫鎖也有條件版本的函數
#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
/* 返回值:成功返回0,失敗返回錯誤編號 */
上面兩個函數在可以獲得鎖時返回0,否則返回錯誤EBUSY。
下面一個實例用來描述讀寫鎖的使用過程。
#include <stdlib.h>
#include <pthread.h>
struct job {
struct job *j_next;
struct job *j_prev;
pthread_t j_id; /* tells which thread handles this job */
/* ... more stuff here ... */
};
struct queue {
struct job *q_head;
struct job *q_tail;
pthread_rwlock_t q_lock;
};
/*
* Initialize a queue.
*/
int
queue_init(struct queue *qp)
{
int err;
qp->q_head = NULL;
qp->q_tail = NULL;
err = pthread_rwlock_init(&qp->q_lock, NULL);
if (err != 0)
return(err);
/* ... continue initialization ... */
return(0);
}
/*
* Insert a job at the head of the queue.
*/
void
job_insert(struct queue *qp, struct job *jp)
{
pthread_rwlock_wrlock(&qp->q_lock);
jp->j_next = qp->q_head;
jp->j_prev = NULL;
if (qp->q_head != NULL)
qp->q_head->j_prev = jp;
else
qp->q_tail = jp; /* list was empty */
qp->q_head = jp;
pthread_rwlock_unlock(&qp->q_lock);
}
/*
* Append a job on the tail of the queue.
*/
void
job_append(struct queue *qp, struct job *jp)
{
pthread_rwlock_wrlock(&qp->q_lock);
jp->j_next = NULL;
jp->j_prev = qp->q_tail;
if (qp->q_tail != NULL)
qp->q_tail->j_next = jp;
else
qp->q_head = jp; /* list was empty */
qp->q_tail = jp;
pthread_rwlock_unlock(&qp->q_lock);
}
/*
* Remove the given job from a queue.
*/
void
job_remove(struct queue *qp, struct job *jp)
{
pthread_rwlock_wrlock(&qp->q_lock);
if (jp == qp->q_head) {
qp->q_head = jp->j_next;
if (qp->q_tail == jp)
qp->q_tail = NULL;
else
jp->j_next->j_prev = jp->j_prev;
} else if (jp == qp->q_tail) {
qp->q_tail = jp->j_prev;
jp->j_prev->j_next = jp->j_next;
} else {
jp->j_prev->j_next = jp->j_next;
jp->j_next->j_prev = jp->j_prev;
}
pthread_rwlock_unlock(&qp->q_lock);
}
/*
* Find a job for the given thread ID.
*/
struct job *
job_find(struct queue *qp, pthread_t id)
{
struct job *jp;
if (pthread_rwlock_rdlock(&qp->q_lock) != 0)
return(NULL);
for (jp = qp->q_head; jp != NULL; jp = jp->j_next)
if (pthread_equal(jp->j_id, id))
break;
pthread_rwlock_unlock(&qp->q_lock);
return(jp);
}
這個例子中,無論是對隊列增加工作或者三處工作都需寫模式來鎖住隊列的讀寫鎖。搜索隊列時允許所有的工作線程并發地搜索隊列。
5.5帶超時的讀寫鎖
與互斥量一樣,讀寫鎖也有一組了帶超時的讀寫鎖函數。
#include <pthread.h>
#include <time.h>
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock,
const struct timespec *restrict tsptr);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock,
const struct timespec *restrict tsptr);
/* 返回值:成功返回0,失敗返回錯誤編號 */
tsptr
同樣指向timespec結構,指定線程應該停止阻塞的時間,如果超時并未獲取到鎖則返回ETIMEDOUT錯誤。
5.6條件變量
條件變量是線程可用的另一種同步機制,條件變量給多線程提供了一個會和的場所。條件變量和互斥量一起使用時,允許線程以無競爭的方式等待特定的條件發生(就是說多個線程等待某一時間發生,該事件一旦發生,這些等待的線程就可以繼續工作了)。
條件由互斥量保護,線程在改變條件狀態之前必須首先鎖住互斥量。
在使用條件變量之前,必須對它初始化。由pthread_cond_t數據類型表示的條件變量可以用兩種方式初始化:用常量PTHREAD_COND_INITIALIZER賦給靜態分配的條件變量;用pthread_cond_init函數對動態分配的條件變量進行初始化。同樣釋放條件變量底層內存空間之前,用pthread_cond_destroy函數進行反初始化。
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
/* 返回值:成功返回0,失敗返回錯誤編號 */
若使用默認屬性的條件變量則attr
參數可以設置為NULL。
我們可以使用pthread_cond_wait等待條件變量為真,如果在給定時間內條件變量不能滿足,那么會生成一個返回錯誤碼的變量。
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex,
const struct timespec *restrict tsptr);
/* 返回值:成功返回0,失敗返回錯誤編號 */
傳遞給pthread_cond_wait的互斥量對條件進行保護,調用者把鎖住的互斥量傳給函數,函數然后自動把調用線程放到等待條件的線程列表上,對互斥量解鎖。這就關閉了條件檢查和線程進入休眠狀態等待條件改變著兩個操作之間的時間通道,這樣線程就不會錯過條件的任何變化。pthread_cond_wait返回時,互斥量再次被鎖住。
有兩個函數用于通知線程條件已經滿足。pthread_cond_signal函數至少能喚醒一個等待該條件的線程,而pthread_cond_broadcast函數則能喚醒等待該條件的所有線程。
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
/* 返回值:成功返回0,失敗返回錯誤編號*/
下面實例給出如何結合條件變量和互斥量進行同步。
#include <pthread.h>
struct msg {
struct msg *m_next;
/* ... more stuff here ... */
};
struct msg *workq;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;
void
process_msg(void)
{
struct msg *mp;
for (;;) {
pthread_mutex_lock(&qlock);
while (workq == NULL)
pthread_cond_wait(&qready, &qlock);
mp = workq;
workq = mp->m_next;
pthread_mutex_unlock(&qlock);
/* now process the message mp */
}
}
void
enqueue_msg(struct msg *mp)
{
pthread_mutex_lock(&qlock);
mp->m_next = workq;
workq = mp;
pthread_mutex_unlock(&qlock);
pthread_cond_signal(&qready);
}
上述程序中,條件是工作隊列的狀態,我們用互斥量保護條件,在while循環中判斷條件。把消息放到工作隊列時,需要占用互斥量,但在給等待線程發信號時,不需要占用互斥量。
5.7自旋鎖
自旋鎖與互斥量類似,但是它不是通過休眠使線程阻塞,而是在獲取鎖之前一直處于阻塞狀態。因此自旋鎖可以用于:鎖被持有的時間短,而且線程并不希望在重新調度上花太多成本。
自旋鎖的初始化和反初始化函數如下:
#include <pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
/* 返回值:成功返回0,失敗返回錯誤編號 */
自旋鎖還有一個特殊的pshared
參數表示進程共享屬性,如果該參數設置為PTHREAD_PROCESS_SHARED,則自旋鎖可以被訪問鎖底層內存的線程獲取,即使那些線程不屬于同一個進程。
可以通過pthread_spin_lock和pthread_spin_trylock對自旋鎖加鎖,簽字在獲取鎖之前一直自旋,后者如果不能獲取鎖就立即返回EBUSY錯誤。
#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
/* 成功返回0,失敗返回錯誤編碼*/
5.8屏障(barrier)
屏障同樣用于多個線程并行工作的同步。屏障允許每個線程等待,知道所有的合作線程均到達某一點,然后從該點繼續執行。
屏障初始化和反初始化函數如下:
#include <pthread.h>
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);
/* 返回值:成功返回0,失敗返回錯誤編碼*/
初始化中,count
指定到達屏障的線程數,attr
指定屏障對象的屬性。
可以使用pthread_barrier_wait函數表明線程已經完成工作,等待其他線程趕上來。
#include <pthread.h>
int pthread_barrier_wait(pthread_barrier_t *barrier);
/* 返回值:成功返回0或PTHREAD_BARRIER_SERIAL_THREAD,失敗返回錯誤編號 */
調用pthread_barrier_wait的線程在屏障計數未滿足條件時,會進入休眠狀態。如果該線程是最后一個調用pthread_barrier_wait的線程,就滿足屏障計數,所有的線程被喚醒。
下面通過實例給出在一個任務上合作的多個線程之間如何用屏障進行同步。
#include "apue.h"
#include <pthread.h>
#include <limits.h>
#include <sys/time.h>
#define NTHR 8 /* number of threads */
#define NUMNUM 8000000L /* number of numbers to sort */
#define TNUM (NUMNUM/NTHR) /* number to sort per thread */
long nums[NUMNUM];
long snums[NUMNUM];
pthread_barrier_t b;
#ifdef SOLARIS
#define heapsort qsort
#else
extern int heapsort(void *, size_t, size_t,
int (*)(const void *, const void *));
#endif
/*
* Compare two long integers (helper function for heapsort)
*/
int
complong(const void *arg1, const void *arg2)
{
long l1 = *(long *)arg1;
long l2 = *(long *)arg2;
if (l1 == l2)
return 0;
else if (l1 < l2)
return -1;
else
return 1;
}
/*
* Worker thread to sort a portion of the set of numbers.
*/
void *
thr_fn(void *arg)
{
long idx = (long)arg;
heapsort(&nums[idx], TNUM, sizeof(long), complong);
pthread_barrier_wait(&b);
/*
* Go off and perform more work ...
*/
return((void *)0);
}
/*
* Merge the results of the individual sorted ranges.
*/
void
merge()
{
long idx[NTHR];
long i, minidx, sidx, num;
for (i = 0; i < NTHR; i++)
idx[i] = i * TNUM;
for (sidx = 0; sidx < NUMNUM; sidx++) {
num = LONG_MAX;
for (i = 0; i < NTHR; i++) {
if ((idx[i] < (i+1)*TNUM) && (nums[idx[i]] < num)) {
num = nums[idx[i]];
minidx = i;
}
}
snums[sidx] = nums[idx[minidx]];
idx[minidx]++;
}
}
int
main()
{
unsigned long i;
struct timeval start, end;
long long startusec, endusec;
double elapsed;
int err;
pthread_t tid;
/*
* Create the initial set of numbers to sort.
*/
srandom(1);
for (i = 0; i < NUMNUM; i++)
nums[i] = random();
/*
* Create 8 threads to sort the numbers.
*/
gettimeofday(&start, NULL);
pthread_barrier_init(&b, NULL, NTHR+1);
for (i = 0; i < NTHR; i++) {
err = pthread_create(&tid, NULL, thr_fn, (void *)(i * TNUM));
if (err != 0)
err_exit(err, "can't create thread");
}
pthread_barrier_wait(&b);
merge();
gettimeofday(&end, NULL);
/*
* Print the sorted list.
*/
startusec = start.tv_sec * 1000000 + start.tv_usec;
endusec = end.tv_sec * 1000000 + end.tv_usec;
elapsed = (double)(endusec - startusec) / 1000000.0;
printf("sort took %.4f seconds\n", elapsed);
for (i = 0; i < NUMNUM; i++)
printf("%ld\n", snums[i]);
exit(0);
}
這個實例使用了8個并行線程和1個合并結果的線程,進行堆排序。