線程
在linux內(nèi)核那一部分我們知道,線程其實(shí)就是一種特殊的進(jìn)程,只是他們共享進(jìn)程的文件和內(nèi)存等資源,無(wú)論如何對(duì)于資源共享,就必須處理一致性的問(wèn)題。
1.線程概念
一個(gè)典型的Unix程序可以看做只有一個(gè)線程:一個(gè)進(jìn)程在某一時(shí)刻只能做一件事情。有了多線程控制以后,在程序設(shè)計(jì)時(shí)就可以把程序設(shè)計(jì)成在某一時(shí)刻能夠做不止一件事情,每個(gè)線程都能獨(dú)立處理各自獨(dú)立的任務(wù)。
每個(gè)線程包含有表示執(zhí)行環(huán)節(jié)所必須的信息,其中包括線程ID、一組寄存器值、棧、調(diào)度優(yōu)先級(jí)和策略、信號(hào)屏蔽字、errno變量以及線程私有數(shù)據(jù)。
一個(gè)進(jìn)程的所有信息對(duì)該線程的所有線程都是共享的,包括可執(zhí)行程序的代碼、程序的全局內(nèi)存和堆內(nèi)存、棧以及文件描述符。
2.線程標(biāo)識(shí)
進(jìn)程有進(jìn)程的ID,而線程同樣有線程ID。進(jìn)程ID在系統(tǒng)中是唯一標(biāo)識(shí),但線程ID不同,線程ID只有在它所屬的進(jìn)程上下文中才有意義。
進(jìn)程ID使用pid_t數(shù)據(jù)類(lèi)型,而線程ID是用pthread_t數(shù)據(jù)類(lèi)型標(biāo)識(shí),通常使用一個(gè)結(jié)構(gòu)。下面函數(shù)用來(lái)比較兩個(gè)線程ID。
#include <pthread.h>
int pthread_equal(pthread_t tid1, pthread_t tid2);
/* 返回值:相等返回非0,否則返回0*/
獲取線程自身ID可以通過(guò)調(diào)用函數(shù)pthread_self。
#include <pthread.h>
pthread_t pthread_self(void);
/* 返回值:調(diào)用線程的線程ID */
3.線程創(chuàng)建
新增線程可以通過(guò)函數(shù)pthread_create函數(shù)創(chuàng)建。
#include <pthread.h>
int pthread_create(pthread_t *restrict tidp,
const pthread_attr_t *restrict attr,
void *(*start_rtn)(void *),
void *restrict arg);
/*返回值:成功返回0,否則返回錯(cuò)誤編號(hào)*/
新創(chuàng)建的線程ID會(huì)設(shè)置為tidp
指向的內(nèi)存單元;新創(chuàng)建的線程從start_rtn
函數(shù)的地址開(kāi)始運(yùn)行,從定義可以看到該函數(shù)沒(méi)有輸入?yún)?shù),因此要傳遞參數(shù),可以將所有參數(shù)放到一個(gè)結(jié)構(gòu)中,然后把這個(gè)結(jié)構(gòu)的地址作為arg
參數(shù)傳入。
線程創(chuàng)建時(shí)并不能保證哪個(gè)線程先執(zhí)行。新創(chuàng)建的線程可以訪問(wèn)進(jìn)程的地址空間,并且繼承調(diào)用線程的浮點(diǎn)環(huán)境和信號(hào)屏蔽字,但是該線程的掛起信號(hào)會(huì)被清除。
下面實(shí)例創(chuàng)建了一個(gè)線程,打印了進(jìn)程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);
}
上面的程序主要注意兩點(diǎn):
- 主線程需要休眠,如果不休眠,可能主線程已經(jīng)退出了,此時(shí)新建線程還沒(méi)機(jī)會(huì)運(yùn)行
- 新線程通過(guò)pthread_self獲取自己的線程ID,而不是從共享內(nèi)存中讀取。雖然調(diào)用pthread_create可以通過(guò)第一個(gè)參數(shù)
tidp
指定新線程的ID,但是如果新線程在主線程調(diào)用pthread_create返回之前運(yùn)行了,那么新線程看到的是未經(jīng)初始化的ntid
的內(nèi)容。
4.線程終結(jié)
進(jìn)程中任意線程調(diào)用了exit、_Exit或者_exit,那么整個(gè)進(jìn)程就會(huì)終止。與此類(lèi)似,如果默認(rèn)的動(dòng)作是終止進(jìn)程,那么,發(fā)送到線程的信號(hào)就會(huì)終止整個(gè)進(jìn)程。
單個(gè)線程有3種方式退出,而不終止進(jìn)程。
- 線程可以簡(jiǎn)單地從啟動(dòng)例程中返回,返回值是線程的退出碼
- 線程可以被同一進(jìn)程的其他線程取消
- 線程調(diào)用pthread_exit
#include <pthread.h>
void pthread_exit(void *rval_ptr);
rval_ptr
參數(shù)是一個(gè)無(wú)類(lèi)型指針,與傳給啟動(dòng)例程的單個(gè)參數(shù)類(lèi)似。進(jìn)程中的其他線程可以通過(guò)pthread_join函數(shù)訪問(wèn)到這個(gè)指針。
#include <pthread.h>
int pthread_join(pthread_t thread, void **rval_ptr);
/* 返回值:成功返回0,失敗返回錯(cuò)誤編號(hào)*/
調(diào)用線程將一直阻塞,直到指定的線程調(diào)用pthread_exit、從啟動(dòng)例程中返回或者被取消。如果線程簡(jiǎn)單地從啟動(dòng)例程返回,rval_ptr
就包含返回碼。如果線程被取消,由rval_ptr
指定的內(nèi)存單元就設(shè)置為PTHREAD_CANCELED。
可以通過(guò)pthread_join自動(dòng)將線程置為分離狀態(tài),這樣資源可以恢復(fù)。如果線程以及處于分離狀態(tài),pthread_join調(diào)用會(huì)失敗,返回EINVAL。
如果對(duì)線程的返回值不感興趣,則可以把rval_ptr
設(shè)置為NULL。這種情況下,調(diào)用pthread_join函數(shù)可以等待指定的線程終止,但并不獲取線程的終止?fàn)顟B(tài)。
下面實(shí)例展示如何獲取以及終止的線程的退出碼。
#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);
}
運(yùn)行結(jié)果為:
可以看到進(jìn)程1,2分別return或者exit時(shí)設(shè)定了返回碼,進(jìn)程中其他線程可以通過(guò)調(diào)用pthread_join函數(shù)獲得該線程的退出碼。
需要注意的是pthread_create和pthread_exit函數(shù)的無(wú)類(lèi)型指針參數(shù)可以傳遞的值不止一個(gè),這個(gè)指針可以包含復(fù)雜信息的結(jié)構(gòu)指針,同時(shí)注意,這個(gè)結(jié)構(gòu)所使用的內(nèi)存調(diào)用者在完成調(diào)用以后必須仍然有效(畢竟前面說(shuō)了不能確定哪個(gè)線程先執(zhí)行,如果調(diào)用線程先執(zhí)行可能這個(gè)結(jié)構(gòu)還沒(méi)傳遞給操作的對(duì)象線程)。
線程可以通過(guò)pthread_cancel函數(shù)來(lái)請(qǐng)求取消同一進(jìn)程中的其他線程。
#include<pthread_cancel>
int pthread_cancel(pthread_t tid);
/* 返回值:成功返回0,失敗返回錯(cuò)誤編號(hào)*/
在默認(rèn)情況下,pthread_cancel函數(shù)會(huì)使得由tid
標(biāo)識(shí)的線程的行為表現(xiàn)為如同調(diào)用了參數(shù)為PTHREAD_CANCELED的pthread_exit函數(shù),但是線程可以選擇忽略取消或控制如何取消(所以它只是請(qǐng)求取消,而不是控制線程終止)。
線程可以安排它退出時(shí)需要的函數(shù),這樣的函數(shù)稱(chēng)為線程處理程序(thread cleanup handler)。一個(gè)線程可以建立多個(gè)清楚處理程序,由于處理程序記錄在棧中,因此他們的執(zhí)行順序和注冊(cè)時(shí)相反:
#include <pthread.h>
void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);
當(dāng)線程執(zhí)行以下動(dòng)作時(shí),清理函數(shù)rtn
是由pthread_cleanup_push函數(shù)調(diào)度的,調(diào)用時(shí)只有一個(gè)參數(shù)arg
:
- 調(diào)用pthread_exit時(shí)
- 響應(yīng)取消請(qǐng)求時(shí)
- 用非0的
execute
參數(shù)調(diào)用pthread_cleanup_pop時(shí)
若execute
參數(shù)設(shè)置為0,清理函數(shù)將不被調(diào)用。上述任一情況下,pthread_cleanup_pop都將刪除上次pthread_cleanup_push調(diào)用建立的清除處理程序。
下面通過(guò)一個(gè)實(shí)例來(lái)說(shuō)明線程清理處理程序的使用。
#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執(zhí)行結(jié)果如下(Linux下不會(huì)出錯(cuò)):
Mac出錯(cuò)的原因是pthread_cleanup_pop和pthread_cleanup_push這兩個(gè)函數(shù)是用宏實(shí)現(xiàn)的,而宏把某些上下文放在棧上。線程1在調(diào)用push和pop之間返回時(shí),棧已經(jīng)被改寫(xiě),而Mac在調(diào)用清除處理程序時(shí)就用了這個(gè)改寫(xiě)的上下文。
在默認(rèn)情況下,線程的終止?fàn)顟B(tài)會(huì)保存直到對(duì)線程調(diào)用pthread_join。如果線程已經(jīng)被分離,則線程的底層存儲(chǔ)資源可以在線程終止時(shí)立即被返回。在線程被分離后,我們不能用pthread_join函數(shù)等待它的終止?fàn)顟B(tài),因?yàn)閷?duì)分離狀態(tài)的線程調(diào)用pthread_join會(huì)產(chǎn)生未定義行為。可以調(diào)用pthread_detach分離線程。
#include <pthread.h>
int pthread_detach(pthread_t tid);
/* 返回值:成功返回0,失敗返回錯(cuò)誤編號(hào)*/
關(guān)于分離狀態(tài)的線程內(nèi)容,在線程控制這篇文章有介紹。
5.線程同步
由于多個(gè)線程之間共享相同的內(nèi)存,因此需要同步技術(shù)來(lái)保證每個(gè)線程看到相同的數(shù)據(jù)視圖。
5.1互斥量
可以使用pthread的互斥接口來(lái)保護(hù)數(shù)據(jù),確保同一時(shí)間只有一個(gè)線程訪問(wèn)數(shù)據(jù)。由Linux內(nèi)核部分的知識(shí)可以知道互斥量本質(zhì)上是一把鎖,在訪問(wèn)共享資源前對(duì)互斥量進(jìn)行加鎖,在訪問(wèn)之后釋放鎖。如果釋放互斥量時(shí)有一個(gè)以上的線程阻塞,那么所有該鎖上的阻塞線程都會(huì)變成可運(yùn)行狀態(tài),第一個(gè)變成運(yùn)行的線程就可以對(duì)互斥量加鎖,其他線程繼續(xù)阻塞。因此每次只有一個(gè)線程可以執(zhí)行。
互斥變量是用pthrea_mutex_t數(shù)據(jù)表示的。在使用互斥變量之前,必須對(duì)它進(jìn)行初始化,可以把它設(shè)置為常量PTHREAD_MUTEX_INITIALIZER(只適用于靜態(tài)分配),也可以通過(guò)調(diào)用pthread_mutex_init函數(shù)進(jìn)行初始化。若動(dòng)態(tài)分配互斥量(比如malloc分配),在釋放內(nèi)存前需要調(diào)用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,失敗返回錯(cuò)誤編號(hào)*/
要用默認(rèn)的屬性初始化互斥量只需要把attr
設(shè)為NULL。
對(duì)互斥量加鎖需要調(diào)用pthread_mutex_lock。如果互斥量已經(jīng)上鎖,則調(diào)用用線程將阻塞直到互斥量被解鎖。對(duì)互斥量解鎖,需要調(diào)用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,失敗返回錯(cuò)誤編號(hào)*/
如果線程不希望被阻塞,它可以使用pthread_mutex_trylock嘗試對(duì)互斥量進(jìn)行加鎖。如果調(diào)用pthread_mutex_trylock時(shí)互斥量處于未鎖住狀態(tài),那么pthread_mutex_trylock將鎖住互斥量,不會(huì)出現(xiàn)阻塞直接返回0,否則pthread_mutex_trylock就會(huì)失敗,并返回EBUSY。
下面一個(gè)實(shí)例描述了前面所述的函數(shù)用法,線程引用結(jié)構(gòu)體時(shí)首先獲取結(jié)構(gòu)體的鎖,然后對(duì)引用值+1,減引用時(shí),同樣先獲取線程鎖,防止其他線程修改結(jié)構(gòu)體的值,判定當(dāng)前結(jié)構(gòu)體的引用次數(shù)。
#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避免死鎖
如果線程試圖對(duì)同一互斥量加鎖兩次,那么它自身就會(huì)陷入死鎖狀態(tài)。除此之外,如果兩個(gè)線程相互請(qǐng)求對(duì)方占用的互斥量,同樣會(huì)產(chǎn)生死鎖,對(duì)于這種情況,通常需要設(shè)定程序的執(zhí)行順序,下面的程序?qū)嵗褪菍?shí)現(xiàn)該功能。
#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);
}
}
上面程序中使用了兩個(gè)互斥量,通過(guò)hashlock
互斥量保護(hù)散列表fh
和f_next
。foo
結(jié)構(gòu)中的f_lock
互斥量保護(hù)對(duì)foo
結(jié)構(gòu)中的其他字段的訪問(wèn)。訪問(wèn)這兩個(gè)互斥量時(shí)總是以相同的順序加鎖,避免死鎖。在釋放函數(shù)foo_rele
為了保證加鎖的順序,實(shí)現(xiàn)過(guò)程過(guò)于復(fù)雜,下面給出一個(gè)優(yōu)化版本。
#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函數(shù)pthread_mutex_timedlock
線程獲取一個(gè)已經(jīng)加鎖的互斥量時(shí),pthread_mutex_timedlock允許綁定線程設(shè)定最長(zhǎng)阻塞時(shí)間。
#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict tsptr);
/* 返回值:成功返回0,失敗返回錯(cuò)誤編號(hào) */
這個(gè)超時(shí)時(shí)間用timespec
結(jié)構(gòu)表示,它用秒和納秒來(lái)描述時(shí)間。
5.4讀寫(xiě)鎖
讀寫(xiě)鎖可以有3種狀態(tài):讀模式下加鎖狀態(tài),寫(xiě)模式下加鎖狀態(tài),不加鎖狀態(tài)。一次只有一個(gè)線程可以占有寫(xiě)模式的讀寫(xiě)鎖,但是多個(gè)線程可以同時(shí)占有讀模式的讀寫(xiě)鎖。
讀寫(xiě)鎖是寫(xiě)加鎖時(shí),在這個(gè)鎖被解鎖之前,所有試圖對(duì)這個(gè)鎖加鎖的線程都會(huì)被阻塞。當(dāng)讀寫(xiě)鎖在讀加鎖狀態(tài)時(shí),所有試圖以讀模式對(duì)它進(jìn)行加鎖的線程都可以得到訪問(wèn)權(quán),但是任何希望以寫(xiě)模式對(duì)此鎖進(jìn)行加鎖的線程都會(huì)阻塞。當(dāng)讀寫(xiě)鎖處于讀模式鎖住的狀態(tài)時(shí),如果有一個(gè)線程試圖以寫(xiě)模式獲取鎖時(shí),讀寫(xiě)鎖通常會(huì)阻塞隨后的讀模式鎖請(qǐng)求。
與互斥量相比,讀寫(xiě)鎖在使用之前必須初始化,在釋放他們底層的內(nèi)存之前必須銷(xiāo)毀。
#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,失敗返回錯(cuò)誤編號(hào) */
在讀模式下加鎖,寫(xiě)模式下加鎖分別用到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,失敗返回錯(cuò)誤編號(hào)*/
同樣對(duì)于讀寫(xiě)鎖也有條件版本的函數(shù)
#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
/* 返回值:成功返回0,失敗返回錯(cuò)誤編號(hào) */
上面兩個(gè)函數(shù)在可以獲得鎖時(shí)返回0,否則返回錯(cuò)誤EBUSY。
下面一個(gè)實(shí)例用來(lái)描述讀寫(xiě)鎖的使用過(guò)程。
#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);
}
這個(gè)例子中,無(wú)論是對(duì)隊(duì)列增加工作或者三處工作都需寫(xiě)模式來(lái)鎖住隊(duì)列的讀寫(xiě)鎖。搜索隊(duì)列時(shí)允許所有的工作線程并發(fā)地搜索隊(duì)列。
5.5帶超時(shí)的讀寫(xiě)鎖
與互斥量一樣,讀寫(xiě)鎖也有一組了帶超時(shí)的讀寫(xiě)鎖函數(shù)。
#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,失敗返回錯(cuò)誤編號(hào) */
tsptr
同樣指向timespec結(jié)構(gòu),指定線程應(yīng)該停止阻塞的時(shí)間,如果超時(shí)并未獲取到鎖則返回ETIMEDOUT錯(cuò)誤。
5.6條件變量
條件變量是線程可用的另一種同步機(jī)制,條件變量給多線程提供了一個(gè)會(huì)和的場(chǎng)所。條件變量和互斥量一起使用時(shí),允許線程以無(wú)競(jìng)爭(zhēng)的方式等待特定的條件發(fā)生(就是說(shuō)多個(gè)線程等待某一時(shí)間發(fā)生,該事件一旦發(fā)生,這些等待的線程就可以繼續(xù)工作了)。
條件由互斥量保護(hù),線程在改變條件狀態(tài)之前必須首先鎖住互斥量。
在使用條件變量之前,必須對(duì)它初始化。由pthread_cond_t數(shù)據(jù)類(lèi)型表示的條件變量可以用兩種方式初始化:用常量PTHREAD_COND_INITIALIZER賦給靜態(tài)分配的條件變量;用pthread_cond_init函數(shù)對(duì)動(dòng)態(tài)分配的條件變量進(jìn)行初始化。同樣釋放條件變量底層內(nèi)存空間之前,用pthread_cond_destroy函數(shù)進(jìn)行反初始化。
#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,失敗返回錯(cuò)誤編號(hào) */
若使用默認(rèn)屬性的條件變量則attr
參數(shù)可以設(shè)置為NULL。
我們可以使用pthread_cond_wait等待條件變量為真,如果在給定時(shí)間內(nèi)條件變量不能滿足,那么會(huì)生成一個(gè)返回錯(cuò)誤碼的變量。
#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,失敗返回錯(cuò)誤編號(hào) */
傳遞給pthread_cond_wait的互斥量對(duì)條件進(jìn)行保護(hù),調(diào)用者把鎖住的互斥量傳給函數(shù),函數(shù)然后自動(dòng)把調(diào)用線程放到等待條件的線程列表上,對(duì)互斥量解鎖。這就關(guān)閉了條件檢查和線程進(jìn)入休眠狀態(tài)等待條件改變著兩個(gè)操作之間的時(shí)間通道,這樣線程就不會(huì)錯(cuò)過(guò)條件的任何變化。pthread_cond_wait返回時(shí),互斥量再次被鎖住。
有兩個(gè)函數(shù)用于通知線程條件已經(jīng)滿足。pthread_cond_signal函數(shù)至少能喚醒一個(gè)等待該條件的線程,而pthread_cond_broadcast函數(shù)則能喚醒等待該條件的所有線程。
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
/* 返回值:成功返回0,失敗返回錯(cuò)誤編號(hào)*/
下面實(shí)例給出如何結(jié)合條件變量和互斥量進(jìn)行同步。
#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);
}
上述程序中,條件是工作隊(duì)列的狀態(tài),我們用互斥量保護(hù)條件,在while循環(huán)中判斷條件。把消息放到工作隊(duì)列時(shí),需要占用互斥量,但在給等待線程發(fā)信號(hào)時(shí),不需要占用互斥量。
5.7自旋鎖
自旋鎖與互斥量類(lèi)似,但是它不是通過(guò)休眠使線程阻塞,而是在獲取鎖之前一直處于阻塞狀態(tài)。因此自旋鎖可以用于:鎖被持有的時(shí)間短,而且線程并不希望在重新調(diào)度上花太多成本。
自旋鎖的初始化和反初始化函數(shù)如下:
#include <pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
/* 返回值:成功返回0,失敗返回錯(cuò)誤編號(hào) */
自旋鎖還有一個(gè)特殊的pshared
參數(shù)表示進(jìn)程共享屬性,如果該參數(shù)設(shè)置為PTHREAD_PROCESS_SHARED,則自旋鎖可以被訪問(wèn)鎖底層內(nèi)存的線程獲取,即使那些線程不屬于同一個(gè)進(jìn)程。
可以通過(guò)pthread_spin_lock和pthread_spin_trylock對(duì)自旋鎖加鎖,簽字在獲取鎖之前一直自旋,后者如果不能獲取鎖就立即返回EBUSY錯(cuò)誤。
#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,失敗返回錯(cuò)誤編碼*/
5.8屏障(barrier)
屏障同樣用于多個(gè)線程并行工作的同步。屏障允許每個(gè)線程等待,知道所有的合作線程均到達(dá)某一點(diǎn),然后從該點(diǎn)繼續(xù)執(zhí)行。
屏障初始化和反初始化函數(shù)如下:
#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,失敗返回錯(cuò)誤編碼*/
初始化中,count
指定到達(dá)屏障的線程數(shù),attr
指定屏障對(duì)象的屬性。
可以使用pthread_barrier_wait函數(shù)表明線程已經(jīng)完成工作,等待其他線程趕上來(lái)。
#include <pthread.h>
int pthread_barrier_wait(pthread_barrier_t *barrier);
/* 返回值:成功返回0或PTHREAD_BARRIER_SERIAL_THREAD,失敗返回錯(cuò)誤編號(hào) */
調(diào)用pthread_barrier_wait的線程在屏障計(jì)數(shù)未滿足條件時(shí),會(huì)進(jìn)入休眠狀態(tài)。如果該線程是最后一個(gè)調(diào)用pthread_barrier_wait的線程,就滿足屏障計(jì)數(shù),所有的線程被喚醒。
下面通過(guò)實(shí)例給出在一個(gè)任務(wù)上合作的多個(gè)線程之間如何用屏障進(jìn)行同步。
#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);
}
這個(gè)實(shí)例使用了8個(gè)并行線程和1個(gè)合并結(jié)果的線程,進(jìn)行堆排序。