一、線程理論基礎
1. 多線程
線程(thread)技術早在60年代就被提出,但真正應用多線程到操作系統中去,是在80年代中期,solaris是這方面的佼佼者。傳統的Unix也支持線程的概念,但是在一個進程(process)中只允許有一個線程,這樣多線程就意味着多進程。現在,多線程技術已經被許多操作系統所支持,包括Windows/NT、Linux。
2. 為什么有了進程,還要引入線程呢?使用多線程到底有哪些好處?
使用多線程的理由之一是:
和進程相比,它是一種非常“節儉”的多任務操作方式。在Linux系統下,啟動一個新的進程必須分配給它獨立的地址空間,建立眾多的數據表來維護它的代碼段、堆棧段和數據段,這是一種"昂貴"的多任務工作方式。
運行於一個進程中的多個線程,它們之間使用相同的地址空間,而且線程間彼此切換所需的時間也遠遠小於進程間切換所需要的時間。據統計,一個進程的開銷大約是一個線程開銷的30倍左右。
使用多線程的理由之二是:
線程間方便的通信機制。對不同進程來說,它們具有獨立的數據空間,要進行數據的傳遞只能通過進程間通信的方式進行,這種方式不僅費時,而且很不方便。線程則不然,由於同一進程下的線程之間共享數據空間,所以一個線程的數據可以直接為其它線程所用,這不僅快捷,而且方便。
除了以上所說的優點外,多線程程序作為一種多任務、並發的工作方式,有如下優點:
1)使多CPU系統更加有效。操作系統會保證當線程數不大於CPU數目時,不同的線程運行於不同的CPU上。
2)改善程序結構。一個既長又復雜的進程可以考慮分為多個線程,成為幾個獨立或半獨立的運行部分,這樣的程序會利於理解和修改。
3. Linux系統下的多線程
Linux系統下的多線程遵循POSIX線程接口,稱為pthread。編寫Linux下的多線程程序,需要使用頭文件pthread.h,連接時需要使用庫libpthread.a。
二、 多線程程序設計
1. 創建線程
#include <pthread.h>
int pthread_create(pthread_t * tidp,const pthread_attr_t *attr, void *(*start_rtn)(void),void *arg)
tidp:線程id
attr:線程屬性(通常為空)
start_rtn:線程要執行的函數,函數返回值為空指針型
arg:start_rtn的參數,線程函數的參數,必須為指針型
2. 編譯
因為pthread的庫不是linux系統的庫,所以在進行編譯的時候要加上-lpthread
# gcc filename -lpthread
3. 實例分析
thread_int.c代碼如下:
#include <stdio.h> #include <pthread.h> #include <unistd.h> void *create(void *arg) // pthread_create()函數最后一個參數
{
int *num; num=(int *)arg; printf("creat parameter is %d\n",*num); return (void *)0; } int main(int argc, char *argv[]) { pthread_t tidp; int error; int test=4; int *attr=&test; error=pthread_create(&tidp,NULL,create,(void *)attr); // 參數類型強制類型轉換 if(error) { printf("pthread_creat is created in not created ...\n"); return -1; } sleep(1); printf("pthread_create is created ...\n"); return 0;
}
thread_share.c代碼如下:
#include <stdio.h> #include <pthread.h> int a=1; void *create(void *arg) { printf("new pthread ... \n"); printf("a=%d \n",a); return (void *)0; } int main(int argc,char *argv[]) { pthread_t tidp; int error; int a=5; error=pthread_create(&tidp,NULL,create,NULL); if(error!=0) { printf("new thread is not create ... \n"); return -1; } sleep(1); printf("new thread is created ... \n"); return 0; }
全局變量在線程函數中也是有效的。
thread_struct.c代碼如下:
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> struct menber { int a; char *s; }; /*線程執行函數*/ void *create(void *arg) { struct menber *temp; temp=(struct menber *)arg; printf("menber->a = %d \n",temp->a); printf("menber->s = %s \n",temp->s); return (void *)0; } int main(int argc,char *argv[]) { pthread_t tidp; int error; struct menber *b; /*為結構體指針b分配內存並賦值*/ b=(struct menber *)malloc( sizeof(struct menber) ); b->a = 4; b->s = "zieckey"; /*創建線程並運行線程執行函數*/ error = pthread_create(&tidp, NULL, create, (void *)b); if( error ) { printf("phread is not created...\n"); return -1; } sleep(1); //進程睡眠一秒使線程執行完后進程才會結束 printf("pthread is created...\n"); return 0; }
4.終止線程
如果進程中任何一個線程中調用exit或_exit,那么整個進程都會終止。線程的正常退出方式有:
(1) 線程從啟動例程中返回
(2) 線程可以被另一個進程終止
(3) 線程自己調用pthread_exit函數
#include <pthread.h>
void pthread_exit(void * rval_ptr)
功能:終止調用線程
Rval_ptr:線程退出返回值的指針,這個值pthread_join()函數可以接受。
5. 線程等待
#include <pthread.h>
int pthread_join(pthread_t tid,void **rval_ptr)
功能:阻塞調用線程,直到指定的線程終止。
Tid :等待退出的線程id
Rval_ptr:線程退出的返回值的指針,函數pthread_exit()函數或return返回的值。
thread_join.c代碼如下:
#include <pthread.h> #include <unistd.h> #include <stdio.h> void *thread(void *str) { int i; for (i = 0; i < 3; ++i) { sleep(2); printf( "This in the thread : %d\n" , i ); } return NULL; } int main() { pthread_t pth; int i; /*創建線程並執行線程執行函數*/ int ret = pthread_create(&pth, NULL, thread, NULL); printf("The main process will be to run,but will be blocked soon\n"); /*阻塞等待線程退出*/ pthread_join(pth, NULL); printf("thread was exit\n"); for (i = 0; i < 3; ++i) { sleep(1); printf( "This in the main : %d\n" , i ); } return 0; }
6. 線程標識
#include <pthread.h>
pthread_t pthread_self(void)
功能:獲取調用線程的 thread identifier
7. 清除
線程終止有兩種情況:正常終止和非正常終止。線程主動調用pthread_exit或者從線程函數中return都將使線程正常退出,這是可預見的退出方式;非正常終止是線程在其他線程的干預下,或者由於自身運行出錯(比如訪問非法地址)而退出,這種退出方式是不可預見的。
不論是可預見的線程終止還是異常終止,都會存在資源釋放的問題,如何保證線程終止時能順利的釋放掉自己所占用的資源,是一個必須考慮解決的問題。
從pthread_cleanup_push的調用點到pthread_cleanup_pop之間的程序段中的終止動作(包括調用pthread_exit()和異常終止,不包括return)都將執行pthread_cleanup_push()所指定的清理函數。
#include <pthread.h>
void pthread_cleanup_push(void (*rtn)(void *),void *arg)
功能:將清除函數壓入清除棧
Rtn:清除函數
Arg:清除函數的參數
#include <pthread.h>
void pthread_cleanup_pop(int execute)
功能:將清除函數彈出清除棧
參數:Execute執行到pthread_cleanup_pop()時是否在彈出清理函數的同時執行該函數,非0:執行; 0:不執行
thread_clean.c代碼如下:
#include <stdio.h> #include <pthread.h> #include <unistd.h> /*線程清理函數*/ void *clean(void *arg) { printf("cleanup :%s\n",(char *)arg); return (void *)0; } /*線程1的執行函數*/ void *thr_fn1(void *arg) { printf("thread 1 start \n"); /*將線程清理函數壓入清除棧兩次*/ pthread_cleanup_push( (void*)clean,"thread 1 first handler"); pthread_cleanup_push( (void*)clean,"thread 1 second hadler"); printf("thread 1 push complete \n"); if(arg) { return((void *)1); //線程運行到這里會結束,后面的代碼不會被運行。由於是用return退出,所以不會執行線程清理函數。 } pthread_cleanup_pop(0); pthread_cleanup_pop(0); return (void *)1; } /*線程2的執行函數*/ void *thr_fn2(void *arg) { printf("thread 2 start \n"); /*將線程清理函數壓入清除棧兩次*/ pthread_cleanup_push( (void*)clean,"thread 2 first handler"); pthread_cleanup_push( (void*)clean,"thread 2 second handler"); printf("thread 2 push complete \n"); if(arg) { pthread_exit((void *)2);//線程運行到這里會結束,后面的代碼不會被運行。由於是用pthread_exit退出,所以會執行線程清理函數。執行的順序是先壓進棧的后執行,即后進先出。 } pthread_cleanup_pop(0); pthread_cleanup_pop(0); pthread_exit((void *)2); } int main(void) { int err; pthread_t tid1,tid2; void *tret; /*創建線程1並執行線程執行函數*/ err=pthread_create(&tid1,NULL,thr_fn1,(void *)1); if(err!=0) { printf("error .... \n"); return -1; } /*創建線程2並執行線程執行函數*/ err=pthread_create(&tid2,NULL,thr_fn2,(void *)1); if(err!=0) { printf("error .... \n"); return -1; } /*阻塞等待線程1退出,並獲取線程1的返回值*/ err=pthread_join(tid1,&tret); if(err!=0) { printf("error .... \n"); return -1; } printf("thread 1 exit code %d \n",(int)tret); /*阻塞等待線程2退出,並獲取線程2的返回值*/ err=pthread_join(tid2,&tret); if(err!=0) { printf("error .... "); return -1; } printf("thread 2 exit code %d \n",(int)tret); return 1; }
三、線程同步
進行多線程編程,因為無法知道哪個線程會在哪個時候對共享資源進行操作,因此讓如何保護共享資源變得復雜,通過下面這些技術的使用,可以解決線程之間對資源的競爭:
1)互斥量Mutex
2)信號燈Semaphore
3)條件變量Conditions
為什么需要互斥量:
Item * p =queue_list;
Queue_list=queue_list->next;
process_job(p);
free(p);
當線程1處理完Item *p=queue_list后,系統停止線程1的運行,改而運行線程2。線程2照樣取出頭節點,然后進行處理,最后釋放了該節點。過了段時間,線程1重新得到運行。而這個時候,p所指向
的節點已經被線程2釋放掉,而線程1對此毫無知曉。他會接着運行process_job(p)。而這將導致無法預料的后果!
對於這種情況,系統給我們提供了互斥量。線程在取出頭節點前必須要等待互斥量,如果此時有其他線程已經獲得該互斥量,那么該線程將會阻塞在這里。只有等到其他線程釋放掉該互斥量后,該線程才有可能得到該互斥量。互斥量從本質上說就是一把鎖, 提供對共享資源的保護訪問。
在Linux中, 互斥量使用類型pthread_mutex_t表示。在使用前, 要對它進行初始化:
1)對於靜態分配的互斥量, 可以把它設置為默認的mutex對象PTHREAD_MUTEX_INITIALIZER
2)對於動態分配的互斥量, 在申請內存(malloc)之后, 通過pthread_mutex_init進行初始化, 並且在釋放內存(free)前需要調用pthread_mutex_destroy。
創建
#include <pthread.h>
1)int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr)
2)int pthread_mutex_destroy(pthread_mutex_t *mutex)
加鎖
對共享資源的訪問, 要使用互斥量進行加鎖, 如果互斥量已經上了鎖, 調用線程會阻塞, 直到互斥量被解鎖。
1)int pthread_mutex_lock(pthread_mutex_t *mutex)
2)int pthread_mutex_trylock(pthread_mutex_t *mutex)
返回值: 成功則返回0, 出錯則返回錯誤編號。
trylock是非阻塞調用模式, 如果互斥量沒被鎖住, trylock函數將對互斥量加鎖, 並獲得對共享資源的訪問權限; 如果互斥量被鎖住了, trylock函數將不會阻塞等待而直接返回EBUSY, 表示共享資源處於忙狀態。
解鎖
在操作完成后,必須給互斥量解鎖,也就是前面所說的釋放。這樣其他等待該鎖的線程才有機會獲得該鎖,否則其他線程將會永遠阻塞。
int pthread_mutex_unlock(pthread_mutex_t *mutex)
互斥量PK信號量
1)Mutex是一把鑰匙,一個人拿了就可進入一個房間,出來的時候把鑰匙交給隊列的第一個。
2)Semaphore是一件可以容納N人的房間,如果人不滿就可以進去,如果人滿了,就要等待有人出來。對於N=1的情況,稱為binary semaphore。
3)Binary semaphore與Mutex的差異:
1. mutex要由獲得鎖的線程來釋放(誰獲得,誰釋放)。而semaphore可以由其它線程釋放
2. 初始狀態可能不一樣:mutex的初始值是1 ,而semaphore的初始值可能是0(或者為1)。