linux线程并不真正并行,Linux系统编程学习札记(十二)线程1
Linux系統編程學習筆記(十二)線程1
線程1:
線程和進程類似,但是線程之間能夠共享更多的信息。一個進程中的所有線程可以共享進程文件描述符和內存。
有了多線程控制,我們可以把我們的程序設計成為在一個進程同時做多個任務,每一個線程做一個獨立的任務,這種
方式可以有以下好處:
1、通過把每一個事件分配給一個線程處理,可以簡化異步事件處理的代碼。每一個線程可以用同步編程模型,而同步
編程要比異步編程簡單的多。
2、多個進程需要使用復雜的機制來共享內存和文件描述符。而線程可以自動共享同一內存地址空間和文件描述符。
3、有一些問題可以劃分以便提高這個程序的吞吐量。一個進程如果有多個任務,需要進行隱式的序列化任務,因為
只有一個線程控制。使用多線程控制,獨立的任務可以將每個任務分配一個線程。
4、交互式的進程可以改善響應時間,通過使用多線程將I/O和程序其他部分分開實現處理。
多線程不光可以在多核系統中得到并行的優勢,而且在單核系統中,也可以提高系統的吞吐量和響應時間,因為當一個線程
阻塞的時候,另一線程可以占有cpu執行。
線程有一些描述線程和執行環境的信息來表示它,包括線程ID,寄存器值的集合,棧,調度優先級和策略,信號的掩碼,errno
變量以及線程特有的一些結構。進程中各個線程共享進程的程序執行文本,程序的全局變量、堆內存、棧和文件描述符。
1、線程標志:
和進程一樣,每一個線程都有一個ID。和進程ID是全系統唯一不同,線程ID是在進程內唯一。進程id用pid_t類型來表示,是一個
非負的整數。線程ID由pthread_t數據類型代表,和進程一樣實現可能為一個結構,所以把pthread_t類型當做一個整數是不具有可
移植性,所以也沒有可移植的打印線程id的方法。這樣也需要一個函數來比較兩個線程的ID是否相同。
#include
int pthread_equal(pthread_t tid1, pthread_t tid2);
一個線程可以獲得使用pthread_self來獲得自己的線程id:
#include
pthread_t pthread_self();
這個方法 可以和pthread_equal配合使用,來識別被打上線程標記的數據結構。
2、進程創建:
通過調用pthread_create可以創建一個線程:
#include
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *),void *restrict arg);
創建成功tidp返回線程的id,attr為線程的屬性,新創建的進程會運行start_rtn函數,并傳入arg作為參數。如果想傳入多個參數到start_rtn
函數中,需要將它們存儲在一個結構體中,并把地址傳到arg中。失敗返回error code,而它們不設定errno。每個線程一個errno只是為了兼容
以前的函數而被使用的。多線程中,返回error code要比依賴于全局變量的errno清晰一些。
例子:創建一個線程,并打印進程id、新創建的線程id和主線程id。
#include
#include
#include
#include
pthread_t ntid;
void printids(const char *s){
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("%s pid %u tid (0x%x) \n",s,(unsigned int)pid,(unsigned int)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){
fprintf(stderr,"create pthread failed: %s",strerror(err));
exit(1);
}
printids("main thread: ");
sleep(1);
exit(0);
}
這個例子有兩個地方比較古怪,主要是為了處理主線程和新創建線程的競爭:1)主線程休眠,以防止主線程終止,導致真個進程的終止,新建的線程沒有機會
運行,我們后面介紹pthread_join可以避免這個。2)新的線程獲得它的線程id是通過調用pthread_self而不是讀取一個共享內存的變量或者傳遞的參數。這是因為
主線程不能安全的使用ntid,新的線程可能在調用pthread_create返回之前開始運行。
3、進程終止
如果進程內的任何一個線程調用exit,_Exit,_exit,整個進程就會終止。類似的如果信號的默認action是終止進程,那么一個發送到線程的信號會終止整個進程。
只終止一個線程有三種方式:
1、線程簡單的返回。返回值就是退出碼。
2、線程可以被進程中的另一個線程取消。
3、線程調用pthread_exit。
#include
void pthread_exit(void *rval_ptr);
rval_ptr是一個無類型的指針,和傳遞到進程的單個參數類似。這個指針可以被調用pthread_join的其他的線程得到。
#include
int pthread_join(pthread_t thread,void **rval_ptr);
調用pthread_join的線程會阻塞,直到指定的線程調用了pthread_exit,從start_rtn中返回,或者被取消。
如果線程簡單的返回,那么rval_ptr被設置成start_rtn的返回值,如果線程被取消,rval_ptr被設置成
PTHREAD_CANCELED。
通過調用pthread_join,我們自動將線程設置成為detached狀態,所以資源會被清除。如果線程已經處于detached狀態,
那么pthread_join就會失敗,返回EINVAL.
如果我們不關心線程的返回值,那么我們可以把rval_ptr設置為NULL。
#include
#include
#include
#include
void * thr_fun1(void *arg){
printf("Thread 1 returning...\n");
return ((void *)1);
}
void * thr_fun2(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_fun1,NULL);
if(err != 0){
fprintf(stderr,"create thread1 failed: %s",strerror(err));
exit(1);
}
err = pthread_create(&tid2,NULL,thr_fun2,NULL);
if(err != 0){
fprintf(stderr,"create thread2 failed: %s",strerror(err));
exit(1);
}
err = pthread_join(tid1,&tret);
if( err != 0 ){
fprintf(stderr,"join thread1 failed: %s",strerror(err));
exit(1);
}
printf("Thread 1 exit code %d\n",(int)tret);
err = pthread_join(tid2,&tret);
if( err != 0 ){
fprintf(stderr,"join thread2 failed: %s",strerror(err));
exit(1);
}
printf("Thread 2 exit code %d\n",(int)tret);
exit(0);
}
無類型的指針傳遞給pthread_create和pthread_exit,使用它可以傳遞多個值,這個指針可以指向包含復雜的結構。但是需要注意這個結構在調用返回時仍然合法。如果
這個結構是在調用者的棧中,內存的內容在使用的可能時候已經被改變。比如一個線程申請在棧中申請了一個結構,然后將結構的指針傳遞給pthread_exit,接著這個
棧在調用thread_join的時候可能已經被銷毀。
例子:
#include
#include
#include
#include
struct foo{
int a,b,c,d;
};
void printfoo(const char *s, const struct foo *fp){
printf("%s",s);
printf(" structure at 0x%x\n",(unsigned) fp);
printf(" foo.a = %d\n", fp->a);
printf(" foo.b = %d\n", fp->b);
printf(" foo.c = %d\n", fp->c);
printf(" foo.d = %d\n", fp->d);
}
void *thr_fn1(void *arg){
struct foo foo = {1,2,3,4};
printfoo("thread 1:\n",&foo);
pthread_exit((void *)&foo);
}
void *thr_fn2(void *arg){
printf("thread 2: ID is %u\n",(unsigned)pthread_self());
pthread_exit((void *)0);
}
int main(void){
int err;
pthread_t tid1,tid2;
struct foo *fp;
err = pthread_create(&tid1,NULL,thr_fn1,NULL);
if(err != 0){
fprintf(stderr,"create thread1 failed: %s",strerror(err));
exit(1);
}
err = pthread_join(tid1,(void *)&fp);
if(err != 0){
fprintf(stderr,"thread_join failed: %s",strerror(err));
exit(1);
}
sleep(1);
printf("Parent starting second thread\n");
err = pthread_create(&tid2,NULL,thr_fn2,NULL);
if(err != 0){
fprintf(stderr,"create thread2 failed: %s",strerror(err));
exit(1);
}
sleep(1);
printfoo("Parent:\n",fp);
exit(0);
}
一個線程可以可以使用pthread_cancel來取消同一進程中的其他線程。
#include
int pthread_cancel(pthread_t tid);
在默認的條件下,pthread_cancle將會使由tid指定的線程像調用了pthread_exit(PTHREAD_CANCELED)一樣,但是一個線程可以選擇忽略和如果控制被取消。pthread_cancel
并不等待線程的終止,而只是發送一個請求。
一個線程可以注冊函數,當它終止的時候被調用,這個和進程使用atexit注冊函數,當進程終止的時候調用類似。這個函數比較出名的就是線程清理函數。一個線程可以
加入多個線程清理函數,這個清理函數保存在棧中,所以執行的順序和注冊的順序相反:
#include
void pthread_cleanup_push(void (*rtn)(void *), void arg);
void pthread_cleanup_pop(int execute);
pthread_cleanup_push來注冊清理函數rtn,這個函數有一個參數arg。但一下三種情形之一發生時,注冊的清理函數被執行:
1)調用pthread_exit
2)作為對取消線程請求(pthread_cancel)的響應。
3)以非0參數調用pthread_cleanup_pop。
如果pthread_cleanup_pop被傳遞0參數,則清除函數不會被調用,但是仍然會清除處于棧頂的清理函數。
一個限制是這兩個函數可能被實現為一個宏,所以在線程的同一作用域必須以匹配的成對出現。pthread_cleanup_push可能有{,而pthread_cleanup_pop可能有匹配這個
字符的}字符。
#include
#include
#include
#include
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(1);
pthread_cleanup_pop(1);
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){
fprintf(stderr,"create thread1 failed: %s",strerror(err));
exit(1);
}
err = pthread_create(&tid2,NULL,thr_fn2,(void *)2);
if(err != 0){
fprintf(stderr,"create thread 2 failed: %s",strerror(err));
exit(1);
}
err = pthread_join(tid1,&tret);
if(err != 0){
fprintf(stderr,"thread1 join failed: %s",strerror(err));
exit(1);
}
printf("thread 1 exit code %d\n",(int)tret);
err = pthread_join(tid2,&tret);
if(err != 0){
fprintf(stderr,"thread2 join failed: %s",strerror(err));
exit(1);
}
printf("thread 2 exit code %d\n",(int) tret);
exit(0);
}
如果線程從開始例程(start routine)中返回(by return statement),清理函數不會被調用。
線程的終止狀態,直到pthread_join被調用的時候才能得到。如果一個線程已經被detached,這個線程的空間將會被回收。pthread_join不能等待detached的線程,獲得其
終止狀態。pthread_join一個detached線程將會失敗,并返回EINVAL,我們可以通過pthread_detach來detach一個線程:
#include
int pthread_detach(pthread_t tid);
4、線程同步
當多個線程共享相同的內存時,我們需要保證每一個線程都看到一個一致的數據。如果一個線程的變量別的線程不能夠讀寫,或者變量時只讀的,那么不會有不一致的狀態。
然而一個線程可以修改一個變量,而其他的進程同時可以讀取或者修改它,我們需要同步線程來保證它們訪問變量,使用的是一個合法的值。
1)互斥量(Mutexes):
我們可以通過pthread提供的互斥量接口來保護我們的數據,確保每次只有一個線程訪問。一個mutex基本上是一個鎖,我們在訪問共享數據的時候設置(上鎖),在訪問完成
后釋放(解鎖)。當我們解鎖的互斥量的時候,當有多余一個的線程被阻塞時,所有阻塞在這個鎖的進程都被喚醒,變成可以運行的狀態,只有一個線程開始運行并設置鎖,
其他的看到互斥量仍然是被鎖定,繼續等待。
互斥量使用pthread_mutex_t數據類型,在我們使用一個互斥量變量時,我們必須先初始化它,可以初始化為PTHREAD_MUTEX_INITIALIZER(靜態初始化)或者調用
pthread_mutext_init,如果我們動態申請了互斥量,我們需要調用pthread_mutext_destory來銷毀它:
#include
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t * restrict attr);
int pthread_mutex_destory(pthread_mutex_t *mutex);
如果想使用默認的屬性來初始化互斥量,我們把attr設置為NULL。
例子:
1)靜態初始化
pthread_mutex_t mylock = PTHREAD_MUTEX_INITIALIZER;
2)動態初始化:
int error;
pthread_mutex_t mylock;
if (error = pthread_mutex_init(&mylock, NULL))
fprintf(stderr, "Failed to initialize mylock:%s\n", strerror(error));
想給一個互斥量上鎖,我們調用pthread_mutex_lock.如果mutex已經上鎖,調用的線程將會被阻塞,直至信號量解鎖。要解鎖一個信號量,我們調用phtread_mutex_unlock
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
一個線程如果lock一個已經上鎖的互斥量,不想被阻塞,那么可以使用pthread_mutex_trylock,如果調用它的時候沒有被上鎖,就鎖住這個互斥量,如果已經上鎖,
就會失敗,并返回EBUSY。
例子:
我們使用mutex來保護數據結構:當多個進程需要訪問動態申請的結構,我們嵌入了引用計數,來保證知道所有線程都使用完它時,我們才釋放它。
#include
#include
struct foo{
int f_count;
pthread_mutex_t f_lock;
/* ...more stuff here... */
};
struct foo * foo_alloc(void){
struct foo *fp;
if((fp = malloc(sizeof(struct foo))) != NULL){
fp->f_count = 1;
if(pthread_mutex_init(&fp->f_lock,NULL) != 0){
free(fp);
return NULL;
}
}
return fp;
}
void foo_hold(struct foo *fp){
pthread_mutex_lock(&fp->f_lock);
fp->f_count++;
pthread_mutex_unlock(&fp->f_lock);
}
void foo_rele(struct foo *fp){
pthread_mutex_lock(&fp->f_lock);
if(--fp->f_count == 0){
pthread_mutex_unlock(&fp->f_lock);
pthread_mutex_destroy(&fp->f_lock);
free(fp);
}else{
pthread_mutex_unlock(&fp->f_lock);
}
}
2)讀寫鎖:
讀寫鎖也叫共享-排他鎖,和互斥量類似,除了它可以提供更高的并行性。使用mutex,它的狀態要么處于鎖住和未鎖狀態,只有一個線程可以上鎖。而讀
寫鎖有更多的狀態:在讀狀態鎖住,在寫狀態鎖住,未鎖住。只有一個線程可以獲得寫鎖,多個線程可以同時獲得讀鎖。當讀寫鎖處于寫鎖住狀態,所有
試圖上鎖的進程都被阻塞,當讀寫鎖處于讀鎖住狀態時,所有試圖上讀狀態的鎖成功,但是試圖獲得寫狀態鎖將會被阻塞,直到所有的讀進程都釋放讀狀
態鎖,此后來到試圖上讀鎖的線程也被阻塞。
讀寫鎖適合讀比寫頻繁情形。讀寫鎖和互斥量一樣也需要在使用前初始化,在釋放他們內存的時候銷毀。
#include
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *restrict rwlock);
一個讀寫鎖可以調用pthread_rwlock_init來初始化,我們可以傳遞NULL作為attr的參數,這樣會使用讀寫鎖的默認屬性。
我們可以調用pthread_rwlock_destroy來清理,銷毀它所占的內存空間。
上鎖:
#include
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
實現上可能會對讀寫鎖中讀模式的鎖鎖住次數有一定的限制,所以我們需要檢查返回值,以確定是否成功。而其他的兩個函數
會返回錯誤,但是只要我們的鎖設計的恰當,我們可以不必做檢查。
Single UNIX規范另外兩個讀寫鎖原語:
#include
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
當鎖成功獲取時,返回0,否則返回EBUSY。這兩個函數使用在一個上鎖的結構不能夠保證產生死鎖的時候,它可以避免死鎖。
3)條件變量:
條件變量時另一中線程同步的機制,允許線程以無競爭的方式等待特定的條件發生。條件變量本身需要互斥量的保護,線程在改變條件前必須首先鎖住互斥量,
且只有在鎖住互斥量以后才能計算條件。條件變量使用之前必須首先進行初始化,pthread_cond_t數據類型代表的條件變量可以用兩種方式初始化。
可以把常量PTHREAD_COND_INITIALIZER賦給靜態分配的條件變量,但是如果條件變量是動態分配的,可以使用pthread_cond_init函數進行初始化。
在釋放底層的內存空間前,可以使用pthread_mutex_destroy函數對條件變量進行銷毀。除非需要創建一個非默認屬性的條件變量,否則pthread_cond_init
函數的attr參數可以設置為NULL。
#include
int pthread_cond_init(pthread_cond_t *restrict cond,
pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
成功返回0,失敗返回錯誤碼。使用pthread_cond_wait等待條件變為真,如果在給定時間內條件不能滿足,那么會生成一個代表出錯碼的返回值。
調用者需要把鎖住的互斥量傳給pthread_cond_wait對條件進行保護。函數把調用線程放到等待條件的線程列表上,然后對互斥量解鎖,這兩個操作
是原子操作。當pthread_cond_wait返回時,互斥量再次被鎖住。
#include
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_timewait(pthread_cond_t * restict cond, pthread_mutex_t *restrict mutex,const struct timespec * restrict timeout);
pthread_cond_timedwait函數的工作方式與pthread_cond_wait函數相似。timeout值指定了等待的時間,它通過timespec結構指定。時間值用秒數或者
分秒數表示,分秒數的單位是納秒。時間值是一個絕對數而不是相對數。可以使用gettimeofday獲取用timeval結構表示的當前時間,然后把這個時間加
上要等待的時間轉換成timespec結構:
void maketimeout(struct timespec *tsp, long minutes){
struct timeval now;
/* get the current time */
gettimeofday(&now);
tsp->tv_sec = now.tv_sec;
tsp->tv_nsec = now.tv_usec * 10000; /* usec to nsec */
tsp->tv_sec += minutes * 60;
}
如果時間值到了但是條件還沒有出現,pthread_cond_timedwait將重新獲取互斥量,然后返回錯誤ETIMEDOUT。從pthread_cond_wait或者pthread_cond_timedwait
調用成功返回時,線程需要重新計算條件,因為其它線程可能已經在運行并改變了條件。pthread_cond_signal函數將喚醒等待該條件的某個線程,而pthread_cond_broadcast
函數將喚醒等待該條件的所有線程。必須注意一定要在改變條件狀態以后再喚醒等待線程
#include
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
例子:
#include
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) /*wait返回后要重新檢查條件*/
pthread_cond_wait(&qready, &qlock); /*wait期間釋放互斥量,返回時再次鎖住*/
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); /*喚醒等待線程時不需要占有互斥量*/
/*如果希望在wait返回時不用再檢查條件,就需要在喚醒時占有互斥量*/
}
參考:
《Advanced programming in Unix Environment 2ed》第11章
1 樓
zhu_jinlong
2010-05-14
博主寫得好!繼續寫下去!
總結
以上是生活随笔為你收集整理的linux线程并不真正并行,Linux系统编程学习札记(十二)线程1的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux 运行多个docker,Doc
- 下一篇: linux如何查看硬件驱动,linux查