Linux學習之多執行緒程式設計(四)
言之者無罪,聞之者足以戒。 ——《詩序》
三、Linux執行緒的高階控制
1、一次性初始化
有些事需要且只能執行一次(比如互斥量初始化)。通常當初始化應用程式時,可以比較容易地將其放在main函式中。但當你寫一個庫函式時,就不能在main裡面初始化了,你可以用靜態初始化,但使用一次初始(pthread_once_t)會比較容易些。
首先要定義一個pthread_once_t變數,這個變數要用巨集PTHREAD_ONCE_INIT初始化。然後建立一個與控制變數相關的初始化函式。pthread_once_t once_control = PTHREAD_ONCE_INIT;
void init_routine() { //初始化互斥量 //初始化讀寫鎖 ...... }
接下來就可以在任何時刻呼叫pthread_once函式
(1)、pthread_once一次性初始化函式
int pthread_once(pthread_once_t* once_control, void (*init_routine)(void))
第一個引數:once_control(初始化函式呼叫情況的標誌)
第二個引數:初始化執行函式
返回值:成功返回0,失敗返回錯誤碼
功能:此函式使用初值為PTHREAD_ONCE_INIT的once_control變數保證init_routine()函式在本程序執行序列中僅執行一次。在多執行緒程式設計環境下,儘管pthread_once()呼叫會出現在多個執行緒中,init_routine()函式僅執行一次,究竟在哪個執行緒中執行是不定的,是由核心排程來決定。
Linux Threads使用互斥鎖和條件變數保證由pthread_once()指定的函式執行且僅執行一次。實際"一次性函式"的執行狀態有三種:
NEVER(0)、IN_PROGRESS(1)、DONE (2),用once_control來表示pthread_once()的執行狀態:
1)、如果once_control初值為0,那麼 pthread_once從未執行過,init_routine()函式會執行。
2)、如果once_control初值設為1,則由於所有pthread_once()都必須等待其中一個激發"已執行一次"訊號, 因此所有pthread_once ()都會陷入永久的等待中,init_routine()就無法執行
3)、如果once_control設為2,則表示pthread_once()函式已執行過一次,從而所有pthread_once()都會立即 返回,init_routine()就沒有機會執行
當pthread_once函式成功返回,once_control就會被設定為2
直接給出程式碼:
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
pthread_t tid;
pthread_once_t once=PTHREAD_ONCE_INIT;
void thread_init()
{
printf("I am in thread 0x%x\n",tid);
}
void *thread_fun1(void *arg)
{
tid = pthread_self();
printf("I am thread 0x%x\n",tid);
printf("once is %d\n",once);
pthread_once(&once,thread_init);
printf("once is %d\n",once);
return NULL;
}
void *thread_fun2(void *arg)
{
sleep(2);
tid = pthread_self();
printf("I am thread 0x%x\n",tid);
// printf("once is %d\n",once);
pthread_once(&once,thread_init);
// printf("once is %d\n",once);
return NULL;
}
int main()
{
pthread_t tid1, tid2;
int err;
err = pthread_create(&tid1, NULL, thread_fun1, NULL);
if(err != 0)
{
printf("create new thread 1 failed\n");
return ;
}
err = pthread_create(&tid2, NULL, thread_fun2, NULL);
if(err != 0)
{
printf("create new thread 1 failed\n");
return ;
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
2、執行緒的屬性
執行緒的屬性用pthread_attr_t型別的結構表示,在建立執行緒的時候可以不用傳入NULL,而是傳入一個pthread_attr_t結構,有使用者自己來配置執行緒的屬性。pthread_attr_t型別對應用程式是不透明的,也就是說應用程式不需要了解有關屬性物件內部結構的任何細節,因而可以增加程式的可移植性。
執行緒屬性:
名稱 | 描述 |
detachstate | 執行緒的分離狀態 |
guardsize | 執行緒棧末尾的警戒區域大小(位元組數) |
stackaddr | 執行緒棧的最低地址 |
stacksize | 執行緒棧的大小(位元組數) |
並不是所有的系統都支援執行緒的這些屬性,因此你需要檢查當前系統是否支援你設定的屬性。當然還有一些屬性不包含在pthread_attr_t結構中,例如:執行緒的可取消狀態、取消型別、併發度
(1)、pthread_attr_init執行緒屬性初始化函式
int pthread_attr_init(pthread_attr_t *attr)
引數:要初始化的屬性
返回值:成功返回0 ,失敗返回錯誤碼
(2)、pthread_attr_destory執行緒屬性銷燬函式
int pthread_attr_destroy(pthread_attr_t *attr)
引數:要初始化的屬性
返回值:成功返回0 ,失敗返回錯誤碼
如果在呼叫pthread_attr_init初始化屬性的時候分配了記憶體空間,那麼pthread_attr_destroy將釋放記憶體空間。除此之外,pthread_atty_destroy還會用無效的值初始化pthread_attr_t物件,因此如果該屬性物件被誤用,會導致建立執行緒失敗。
注意:pthread_attr_t結構在使用之前需要初始化,使用完之後需要銷燬
什麼叫做分離?
分離一個正在執行的執行緒並不影響它,僅僅是通知當前系統該執行緒結束時,其所屬的資源可以回收。一個沒有被分離的執行緒在終止時會保留它的虛擬記憶體,包括他們的堆疊和其他系統資源,有時這種執行緒被稱為“殭屍執行緒”。建立執行緒時預設是非分離的。如果執行緒具有分離屬性,執行緒終止時會被立刻回收,回收將釋放掉所有線上程終止時未釋放的系統資源和程序資源,包括儲存執行緒返回值的記憶體空間、堆疊、儲存暫存器的記憶體空間等。
(3)、pthread_attr_setdetachstate設定執行緒的分離屬性
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)
第一個引數:要設定的分離狀態屬性
第二個引數:狀態設定(有兩種值:PTHREAD_CREATE_DETACHED分離的、PTHREAD_CREATE_JOINABLE 非分離的,可連線的)
返回值:成功返回0 ,失敗返回錯誤碼
(4)、pthread_attr_getdetachstate分離狀態屬性獲取函式
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate)
第一個引數:要設定的分離狀態屬性
第二個引數:已經設定的分離狀態屬性
返回值:成功返回0 ,失敗返回錯誤碼
設定執行緒分離屬性的步驟 1、定義執行緒屬性變數pthread_attr_t attr 2、初始化attr,pthread_attr_init(&attr) 3、設定執行緒為分離或非分離 pthread_attr_setdetachstate(&attr, detachstate) 4、建立執行緒pthread_create(&tid, &attr, thread_fun, NULL) 所有的系統都會支援執行緒的分離狀態屬性,
下面看一下程式框圖:
程式程式碼:
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
void *thread_fun1(void *arg)
{
printf("I'm new thread 1\n");
return (void *)1;
}
void *thread_fun2(void *arg)
{
printf("I'm new thread 2\n");
return (void *)2;
}
int main()
{
pthread_t tid1, tid2;
int err;
int *flag;
//定義屬性變數
pthread_attr_t attr;
//初始化屬性
pthread_attr_init(&attr);
//設定分離狀態屬性,置為已分離
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
//獲取分離狀態的屬性
pthread_attr_getdetachstate(&attr,flag );
printf("flag is %p\n",flag);
err = pthread_create(&tid1, &attr, thread_fun1, NULL);
if(err)
{
printf("create new thread 1 failed\n");
return;
}
err = pthread_create(&tid2, NULL, thread_fun2, NULL);
if(err)
{
printf("create new thread 2 failed\n");
return;
}
//連線執行緒1
err = pthread_join(tid1, NULL);
if(!err)
printf("join thread 1 success\n");
else
printf("join thread 1 failed\n");
//連線執行緒2
err = pthread_join(tid2, NULL);
if(!err)
printf("join thread 2 success\n");
else
printf("join thread 2 failed\n");
//銷燬attr
pthread_attr_destroy(&attr);
return 0;
}
對於程序來說,虛擬地址空間的大小是固定的,程序中只有一個棧,因此它的大小通常不是問題。但對執行緒來說,同樣的虛擬地址被所有的執行緒共享。如果應用程式使用了太多的執行緒,致使執行緒棧累計超過可用的虛擬地址空間,這個時候就需要減少執行緒預設的棧大小。另外,如果執行緒分配了大量的自動變數或者執行緒的棧幀太深,那麼這個時候需要的棧要比預設的大。如果用完了虛擬地址空間,可以使用malloc或者mmap來為其他棧分配空間,並修改棧的位置。
(5)、pthread_attr_setstack修改棧的屬性
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize)
第一個引數:棧屬性
第二個引數:棧的記憶體單元最低地址
第三個引數:棧的大小
返回值:成功返回0,失敗返回錯誤碼
(6)、phread_attr_getstack獲取棧屬性
int pthread_attr_getstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize)
第一個引數:棧屬性
第二個引數:棧的記憶體單元最低地址
第三個引數:棧的大小
返回值:成功返回0,失敗返回錯誤碼
引數stackaddr是棧的記憶體單元最低地址,引數stacksize是棧的大小。要注意stackaddr並不一定是棧的開始,對於一些處理器,棧的地址是從高往低的,那麼這是stackaddr是棧的結尾。
當然也可以單獨獲取或者修改棧的大小,而不去修改棧的地址。對於棧大小設定,不能小於PTHREAD_STACK_MIN(需要標頭檔案limit.h)
(7)、pthread_attr_setstacksize設定棧的大小
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize)
第一個引數:棧屬性
第二個引數:要設定的棧大小
返回值:成功返回0,失敗返回錯誤碼
(8)、pthread_attr_getstacksize獲取棧的大小
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize)
第一個引數:棧屬性
第二個引數:要獲取的棧大小
返回值:成功返回0,失敗返回錯誤碼
對於棧大小的設定,在建立執行緒之後,還可以修改。
對於遵循POSIX標準的系統來說,不一定要支援執行緒的棧屬性,因此你需要檢查
1)、在編譯階段使用
_POSIX_THREAD_ATTR_STACKADDR 和 _POSIX_THREAD_ATTR_STACKSIZE符號來檢查系統
_POSIX_THREAD_ATTR_STACKSIZE符號來檢查系統在/usr/include/bits/posix_opt.h(可以用#ifdef和#endif來實現)
2)、在執行階段
把_SC_THREAD_ATTR_STACKADD和 _SC_THREAD_THREAD_ATTR_STACKSIZE傳遞給sysconf函式檢查系統對執行緒棧屬性的支援
執行緒屬性guardsize控制著執行緒棧末尾以後用以避免棧溢位的擴充套件記憶體的大小,這個屬性預設是PAGESIZE個位元組。你可以把它設為0,這樣就不會提供警戒緩衝區。同樣的,如果你修改了stackaddr,系統會認為你自己要管理棧,警戒緩衝區會無效。
(9)、pthread_attr_setguardsize設定設定guardsize
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize)
第一個引數:棧屬性
第二個引數:擴充套件記憶體的大小
返回值:成功返回0,失敗返回錯誤碼
(10)、pthread_attr_getguardsize獲取guardsize
int pthread_attr_getguardsize(pthread_attr_t *attr, size_t *guardsize)
第一個引數:棧屬性
第二個引數:擴充套件記憶體的大小
返回值:成功返回0,失敗返回錯誤碼
下面來看一下程式,熟悉一下上面說的函式:
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
pthread_attr_t attr;
void *thread_fun(void *arg)
{
size_t stacksize;
#ifdef _POSIX_THREAD_ATTR_STACKSIZE
pthread_attr_getstacksize(&attr,&stacksize);
printf("new thread stack size is %d\n",stacksize);
pthread_attr_setstacksize(&attr,17684);
pthread_attr_getstacksize(&attr,&stacksize);
printf("new thread stack size is %d\n",stacksize);
#endif
return (void *)1;
}
int main()
{
pthread_t tid;
int err;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
#ifdef _POSIX_THREAD_ATTR_STACKSIZE
pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN);
#endif
err = pthread_create(&tid, &attr, thread_fun, NULL);
if(err)
{
printf("create new thread failed\n");
return;
}
pthread_join(tid,NULL);
return 0;
}