線程的基本概念
線程是在進程內部運行的一個執行分支,它是運行在進程的地址空間中。
和進程之間的獨立性不同,線程之間更強調共享性,它共享進程的地址空間,比如數據段,代碼段,堆。。。以及文件描述符表。因此,無論是線程的創建,終止,切換的代價都要比進程小很多,線程之間可以直接通信而不需要向進程通信那麼麻煩(共享內存,消息隊列,信號量等機制),當然有優點就有缺點,由於線程強調共享性,一個進程內的所有線程是互相影響的, 一個線程崩潰會導致該進程內的所有線程都崩潰。
線程雖然強調共享性,但是也不是所有東西都共享,對於每個線程來說,它都私有自己的棧,私有自己運行上下文如寄存器,棧指針,線程id,調度優先級等等。
在操作系統課程上,我們講,進程是資源分配的基本單位,線程是CPU調度的基本單位,確實如此,進程負責控制資源,線程之間調度能夠更好的實現併發性。
在不同的平臺下,對線程實現是不一樣的,例如在Windows下是有着類似於PCB控制進程的TCB(線程控制塊)來控制線程,而在Linux下沒有真正意義上的線程,它是用進程模擬實現,所以Linux下的線程有時也叫輕量級進程(LWP),這樣的好處是,增強了代碼的複用性,使得線程能夠複用進程的數據結構,維護性也相對好了起來。
線程的創建
我們所學習的多線程編程都是基於Linux平臺的,我自己主要的參考書籍就是Unix高級環境編程(APUE),關於線程編程主要用的POSIX標準下的pthread庫,在引用這個庫時候,需要
#include<pthread>
//編譯需要加上-lpthread
gcc -o thread thread.c -lpthread
每個線程都有線程id,假設我們叫線程id爲tid,tid可以用一個pthread_t的數據類型表示,需要注意的是,pthread _t在不同的平臺下實現不同,比如在Linux爲無符號整型,其他爲一個結構體,因此,獲取tid最好用對應的函數,判斷兩個tid是否相等也最好用pthread庫提供的接口以提供程序的移植性。
int pthread_equal(pthread_t t1, pthread_t t2); //相等返回非0,不等返回0
pthread_t pthread_self(void);//獲取線程id
線程創建是用pthread_create函數實現的
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start) (void *), void *arg);
- thread參數指向的內存單元被設置爲新創建線程的tid
- attr爲線程屬性,不用時可設置爲NULL
- start爲函數指針,指向一個函數,新創建的線程從start函數的地址處開始運行
- arg是配合start函數,用於向start函數傳遞參數,如果不需要傳遞參數可設置爲NULL,如果有多個參數,則需要將這些參數放在一個結構體,然後傳遞指向結構體的指針。
需要注意的是,在一個進程剛開始運行時候,可以看作是這個進程是一個只有單線程的進程,我們可以把這個單線程看作是主線程,當使用pthread_create創建一個新線程並不保證主線程還是新線程的運行次序,也就是誰先誰後並不知道。
線程的終止
線程的終止有幾種方式,比如暴力的直接終止進程(調用exit,或者發送終止信號給進程),由於線程是運行在進程的地址空間的,因此當進程終止,線程不得不終止。
我們主要討論的是終止一個進程中的單個線程而不對其他線程產生影響。
線程的終止方式:
- 線程函數自己返回(調用return),返回值爲線程的退出碼,但是這種方式對主線程不管用,因爲從main函數return相當於exit
- 調用pthread_exit返回
- 線程可被一個進程內的其他線程取消
需要注意,pthread_exit或者return返回的指針所指向的內存單元必須是全局的或者是用malloc分配的,不能在線程函數的棧上分配,因爲當其它線程得到這個返回指針時線程函數已經退出了。
對於1,2兩種方式,他們返回的都是一種void*指針
//return (void*)1;
//void pthread_exit(void *retval);
對於第3種方式,其他線程可調用pthread_cancel函數取消某個線程=
int pthread_cancel(pthread_t thread);
默認情況下,該函數使得tid爲thread的線程表現出像自己調用pthread_exit函數一樣,但是,線程可以選擇忽略取消方式或者是控制取消方式,注意pthread _ cancel並不等待線程終止,它僅僅是請求。
線程的等待
線程可以被終止,也就可以被等待,等待需要調用下面的函數:
int pthread_join(pthread_t thread, void **retval);
//從thread線程中,接收到thread的返回碼保存到retval中
實例:
介紹了這麼多函數,我們下面寫一個小程序來看看這些函數具體是如何使用的?
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#include<sys/types.h>
void* thread1(void* x)
{
printf("thread1_return : pid = %u, tid = %u\n", getpid(), (unsigned int)pthread_self());
return (void*)1;
}
void* thread2(void* x)
{
printf("thread2_exit : pid = %u, tid = %u\n", getpid(), (unsigned int)pthread_self());
pthread_exit((void*)2);
}
void* thread3(void* x)
{
printf("thread3_cancel : pid = %u, tid = %u\n", getpid(), (unsigned int)pthread_self());
while (1)
{
printf("thread3 needs to be cancelled\n");
sleep(1);
}
}
int main()
{
pthread_t tid1;
pthread_t tid2;
pthread_t tid3;
void* ret1;
void* ret2;
void* ret3;
pthread_create(&tid1, NULL, thread1, NULL);
pthread_join(tid1, &ret1);
printf("thread %u, return code is %d\n", (unsigned)tid1, (int)ret1);
pthread_create(&tid2, NULL, thread2, NULL);
pthread_join(tid2, &ret2);
printf("thread %u, exit code is %d\n", (unsigned)tid2, (int)ret2);
pthread_create(&tid3, NULL, thread3, NULL);
sleep(3);
pthread_cancel(tid3);
pthread_join(tid3, &ret3);
printf("thread %u, cancel code is %d\n", (unsigned)tid3, (int)ret3);
return 0;
}
最後結果如下:
thread1_return : pid = 7800, tid = 3076094784
thread 3076094784, return code is 1
thread2_exit : pid = 7800, tid = 3076094784
thread 3076094784, exit code is 2
thread3_cancel : pid = 7800, tid = 3076094784
thread3 needs to be cancelled
thread3 needs to be cancelled
thread3 needs to be cancelled
thread 3076094784, cancel code is -1
我們創建了三個線程,通過線程的三種終止方式,我們展示瞭如何使用之前我們所說的函數~
線程的分離
一般來說,線程終止,其他線程可調用pthread_join函數來獲取退出狀態,但是線程也可以設置爲分離狀態(detach),這樣的線程一旦退出就立刻回收它的所有的資源,因此不能對一個已經detach狀態的線程調用pthread _join。
對一個尚未detach的線程調用pthread_join或pthread_detach都可以把該線程置爲detach狀態,也就是說,不能對同一線程調用兩次pthread_ join, 或者如果已經對一個線程調用 了pthread_detach就不能再調⽤用pthread_join了。
關於分離線程
在任一時間點,一個線程都是可結合的或者可分離的。可結合的線程可以被其他線程回收資源並殺死,在被其他資源回收之前,它資源是不釋放的。可分離的線程則不能被其他線程回收資源,當它結束時,它的資源由系統自動釋放。
線程的清理處理程序
線程可以安排它退出時候需要調用的函數,這與進程退出用atexit註冊需要調用的函數是類似的,這樣的函數成爲線程清理處理程序。
線程可以建立多個清理處理程序,記錄在棧中,也就是說它的執行順序和註冊順序相反。
void pthread_cleanup_push(void (*clean)(void *), void *arg);
void pthread_cleanup_pop(int execute);
當線程執行以下操作時,調用清理函數clean,調用參數爲arg,clean調用順序有push函數安排:
- 調用pthread_exit
- 響應取消請求
- 用非零execute參數調用pthread_cleanup _pop
如果execute函數爲0,清理函數將不會被調用。無論什麼情況,pop都會刪除上次push調用簡歷建立的清理處理程序。
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#include<sys/types.h>
void clean_up(void* x)
{
printf("clean_up : %s\n", (char*)x);
}
void* thread1(void* arg)
{
printf("thread1 starts\n");
pthread_cleanup_push(clean_up, "thread1 handler 1");
pthread_cleanup_push(clean_up, "thread1 handler 2");
printf("thread1 ends\n");
if (arg != NULL)
return (void*)1;
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
return (void*)1;
}
void* thread2(void* arg)
{
printf("thread2 starts\n");
pthread_cleanup_push(clean_up, "thread2 handler 1");
pthread_cleanup_push(clean_up, "thread2 handler 2");
printf("thread2 ends\n");
if (arg != NULL)
pthread_exit((void*)2);
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
pthread_exit((void*)2);
}
int main()
{
pthread_t tid1;
pthread_t tid2;
void* ret1;
void* ret2;
pthread_create(&tid1, NULL, thread1, (void*)1);
pthread_create(&tid2, NULL, thread2, (void*)2);
pthread_join(tid1, &ret1);
pthread_join(tid2, &ret2);
printf("thread1 exit code is %d\n", (int)ret1);
printf("thread2 exit code is %d\n", (int)ret2);
return 0;
}
運行結果如下:
thread2 starts
thread2 ends
clean_up : thread2 handler 2
clean_up : thread2 handler 1
thread1 starts
thread1 ends
thread1 exit code is 1
thread2 exit code is 2
可以看出,兩個線程都成功退出,但是隻調用了第二個線程的清理程序,所以,如果線程是通過從它的啓動例程中返回(也就是上面的return語句),則不調用對應的線程清理處理程序。
PS:關於線程函數和進程函數的對比可看下圖: