1. 程式人生 > >Linux 多執行緒程式設計(二)

Linux 多執行緒程式設計(二)

執行緒管理執行緒管理包含了執行緒的建立、終止、等待、分離、設定屬性等操作。1 執行緒 ID執行緒 ID 可以看作為執行緒的控制代碼,用來引用一個執行緒。Pthreads 執行緒有一個 pthread_t 型別的 ID 來引用。執行緒可以通過呼叫 pthread_self()函式來獲取自己的 ID。 pthread_self()函式原型如下:
pthread_t pthread_self(void);
該函式返回呼叫執行緒的執行緒 ID。由於 pthread_t 型別可能是一個結構體,可以使用 pthread_equal()來比較兩個執行緒 ID 是否相等。 pthread_equal()函式原型如下:
int pthread_equal(pthread_t t1, pthread_t t2);
如果 t1 等於 t2,該函式返回一個非 0 值,否則返回 0。2 建立與終止每個執行緒都有從建立到終止的生命週期。2.1. 建立執行緒在程序中建立一個新執行緒的函式是 pthread_create(),原型如下:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                                        void *(*start_routine) (void *), void *arg);
說明:執行緒被建立後將立即執行。返回值說明:

如果 pthread_create()呼叫成功,函式返回

0,否則返回一個非 0 的錯誤碼, 下表列出 pthread_create()函式呼叫時必須檢查的錯誤碼。

pthread_create()錯誤碼錶
錯誤碼出錯說明
EAGAIN系統沒有建立執行緒所需要的資源
EINVALattr引數無效
EPERM呼叫程式沒有適當的許可權來設定排程策略或attr指定的引數

引數說明:

  • thread 用指向新建立的執行緒的 ID 
  • attr 用來表示一個封裝了執行緒各種屬性的屬性物件,如果 attr NULL,新執行緒就使用預設的屬性, 下面第4部分將討論執行緒屬性的細節; 
  • start_routine 是執行緒開始執行的時候呼叫的函式的名字, start_routine
    函式有一個有指向 void 的指標引數,並有 pthread_create 的第四個引數 arg 指定值,同時start_routine 函式返回一個指向 void 的指標,這個返回值被 pthread_join 當做退出狀態處理, 下面第3部分介紹執行緒的退出狀態; 
  • arg 為引數 start_routine 指定函式的引數。

2.2 終止執行緒

程序的終止可以通過直接呼叫 exit()、執行 main()中的 return、或者通過程序的某個其它執行緒呼叫 exit()來實現。在以上任何一種情況下,所有的執行緒都會終止。如果主執行緒在建立了其它執行緒後沒有任務需要處理,那麼它應該阻塞等待所有執行緒都結束為止,或者應該呼叫pthread_exit(NULL)

呼叫 exit()函式會使整個程序終止,而呼叫 pthread_exit()只會使得呼叫執行緒終止,同時在建立的執行緒的頂層執行 return 執行緒會隱式地呼叫 pthread_exit()pthread_exit()函式原型如下:

void pthread_exit(void *retval);

retval 是一個 void 型別的指標,可以將執行緒的返回值當作 pthread_exit()的引數傳入,這個值同樣被 pthread_join()當作退出狀態處理。如果程序的最後一個執行緒呼叫了 pthread_exit(),程序會帶著狀態返回值 0 退出。

2.3 執行緒範例-1 xialie程式清單給出了執行緒建立和終止的示例程式,主執行緒建立了 5 個執行緒,這 5 個執行緒和主執行緒併發執行,主執行緒建立完執行緒後呼叫 pthread_exit()函式退出執行緒,其它執行緒分別列印當前執行緒的序號。當主執行緒先於其它程序執行 pthread_exit()時,程序不會退出,而是最後一個執行緒完成時才會程序退出。
執行緒的建立與終止 :
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define NUM_THREADS 5


void *PrintHello(void *threadid){ /* 執行緒函式 */
    long tid;
    tid = (long)threadid;
    printf("Hello World! It's me, thread #%ld!\n", tid); /* 列印執行緒對應的引數 */
    pthread_exit(NULL);
}


int main (int argc, char *argv[])
{
    pthread_t threads[NUM_THREADS];
    int rc;
    long t;
    for(t=0; t<NUM_THREADS; t++){ /* 迴圈建立 5 個執行緒 */
        printf("In main: creating thread %ld\n", t);
        rc = pthread_create(&threads[t], NULL, PrintHello, (void *)t); /* 建立執行緒 */
        if (rc){
            printf("ERROR; return code from pthread_create() is %d\n", rc);
            exit(-1);
        }
    }
    printf("In main: exit!\n");
    pthread_exit(NULL); /* 主執行緒退出 */
    return 0;
}
程式清單的程式執行結果如下圖所示,程式中主執行緒呼叫了 pthread_exit()函式並不會將整個程序終止,而是最後一個執行緒呼叫 pthread_exit()時程式才完成執行。

注意:由於作業系統排程的執行緒的隨機性,多執行緒程式的執行結果可能與本文給出的結果不一致。

3 連線與分離

執行緒可以分為分離執行緒(DETACHED)和非分離執行緒(JOINABLE)兩種:

  • 分離執行緒是指執行緒退出時執行緒將釋放它的資源的執行緒;
  • 非分離執行緒退出後不會立即釋放資源,需要另一個執行緒為它呼叫 pthread_join 函式或者程序退出時才會釋放資源。

只有非分離執行緒才是可連線的,而分離執行緒退出時不會報告執行緒的退出狀態。

3.1 執行緒分離

pthread_detach()函式可以將非分離執行緒設定為分離執行緒,函式原型如下:

int pthread_detach(pthread_t thread);

引數 thread 是要分離的執行緒的 ID

執行緒可以自己來設定分離,也可以由其它執行緒來設定分離,以下程式碼執行緒可設定自身分離:
pthread_detach(pthread_self());
成功返回 0;失敗返回一個非 0 的錯誤碼, 下表列出 pthread_detach 的實現必須檢查的錯誤碼。
pthread_detach 錯誤碼錶
錯誤碼出錯描述
EINVALthread引數所表示的執行緒不是可分離的執行緒
ESRCH沒有找到執行緒ID為thread的執行緒

3.2 執行緒連線

如果一個執行緒是非分離執行緒,那麼其它執行緒可呼叫 pthread_join()函式對非分離執行緒進行連線。 pthread_join()函式原型如下:

int pthread_join(pthread_t thread, void **retval);
pthread_join()函式將呼叫執行緒掛起,直到第一個引數 thread 指定目標執行緒終止執行為止。

引數 retval 為指向執行緒的返回值的指標提供一個位置, 這個返回值是目標執行緒呼叫pthread_exit()或者 return 所提供的值。當目標執行緒無需返回時可使用 NULL 值,呼叫執行緒如果不需對目標執行緒的返回狀態進行檢查可直接將 retval 賦值為 NULL

如果 pthread_join()成功呼叫,它將返回 0 值,如果不成功, pthread_join()返回一個非 0的錯誤碼, 下表列出 pthread_join()的實現必須檢查的錯誤碼。
pthread_join 錯誤碼錶
錯誤碼出錯描述
EINVALthread引數所表示的執行緒不是可分離的執行緒
ESRCH沒有找到執行緒ID為thread的執行緒
為了防止記憶體洩露,長時間執行的程式最終應該為每個執行緒呼叫 pthread_detach()或者被 pthread_join。

3.3 執行緒範例-2 下列程式清單給出了 pthread_join()的使用範例,主執行緒建立了 4 個執行緒來進行數學運算,每個執行緒將運算的結果使用 pthread_exit()函式返回給主執行緒,主執行緒使用 pthread_join()等待 4 個執行緒完成和獲取執行緒的執行結果
pthread_join 函式示例
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#define NUM_THREADS 4

void *BusyWork(void *t) /* 執行緒函式 */
{
	int i;
	long tid;
	double result=0.0;
	tid = (long)t;
	printf("Thread %ld starting...\n",tid);
	
	for (i=0; i<1000000; i++) 
	{
		result = result + sin(i) * tan(i); /* 進行數學運算 */
	}
	printf("Thread %ld done. Result = %e\n",tid, result);
	pthread_exit((void*) t); /* 帶計算結果退出 */
}


int main (int argc, char *argv[])
{
	pthread_t thread[NUM_THREADS];
	int rc;
	long t;
	void *status;
	
	for(t=0; t<NUM_THREADS; t++) 
	{
		printf("Main: creating thread %ld\n", t);
		rc = pthread_create(&thread[t], NULL, BusyWork, (void *)t); /* 建立執行緒 */
		if (rc) 
		{
			printf("ERROR; return code from pthread_create() is %d\n", rc);
			exit(-1);
		}
	}
	
	for(t=0; t<NUM_THREADS; t++)
	{
		rc = pthread_join(thread[t], &status); /*等待執行緒終止,並獲取返回值*/
		if (rc) 
		{
			printf("ERROR; return code from pthread_join() is %d\n", rc);
			exit(-1);
		}
		printf("Main: completed join with thread %ld having a status of %ld\n",t,(long)status);
	}
	
	printf("Main: program completed. Exiting.\n");
	pthread_exit(NULL);
}
下圖可以看出四個執行緒的計算結果相同,主執行緒在 4 個執行緒完成後退出。

4 執行緒屬性

前面介紹的執行緒建立 pthread_create()函式, pthread_create()函式的第二個引數為pthread_attr_t 型別, 用於設定執行緒的屬性。

執行緒基本屬性包括: 棧大小、 排程策略和執行緒狀態通常先建立一個屬性物件,然後在屬性物件上設定屬性的值,再將屬性物件傳給pthread_create 函式的第二個引數用來建立含有該屬性的執行緒。 一個屬性物件可以多次傳給 pthread_create()函式建立多個含有相同屬性的執行緒。

4.1 屬性物件

1)初始化屬性物件 pthread_attr_init()函式用於將屬性物件使用預設值進行初始化,函式原型如下:
int pthread_attr_init(pthread_attr_t *attr);
函式只有一個引數, 是一個指向 pthread_attr_t 的屬性物件的指標。成功返回 0, 否則返回一個非 0 的錯誤碼。 2) 銷燬屬性物件 銷燬屬性物件使用 pthread_attr_destroy()函式, 函式原型如下:
int pthread_attr_destroy(pthread_attr_t *attr);
函式只有一個引數, 是一個指向 pthread_attr_t 的屬性物件的指標。成功返回 0, 否則返回一個非 0 的錯誤碼。 4.2 執行緒狀態 執行緒有兩種執行緒狀態,取值可能是:
  • PTHREAD_CREATE_JOINABLE——非分離執行緒;
  • PTHREAD_CREATE_DETACHED——分離執行緒。

1)獲取執行緒狀態

獲取執行緒狀態的函式是 pthread_attr_getdetachstate(),原型如下:

int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);
引數 attr 是一個指向已初始化的屬性物件的指標, detachstate 是獲取結果值的指標。成功返回 0,否則返回一個非 0 的錯誤碼。 

2)設定執行緒狀態
設定執行緒狀態的函式是 pthread_attr_setdetachstate(), 原型如下:

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
引數 attr 是一個指向已初始化的屬性物件的指標, detachstate 是要設定的值。成功返回0,否則返回一個非 0 的錯誤碼。

4.3 執行緒棧 每個執行緒都有一個獨立呼叫棧,執行緒的棧大小線上程建立的時候就已經固定下來, Linux系統執行緒的預設棧大小為 8MB,只有主執行緒的棧大小會在執行過程中自動增長。使用者可以通過屬性物件來設定和獲取棧大小。 1)獲取執行緒棧 獲取執行緒棧大小的函式是 pthread_attr_getstacksize(),原型如下:
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);
引數 attr 都是一個指向已初始化的屬性物件的指標, stacksize 是獲取的棧大小的指標。成功返回 0,否則返回一個非 0 的錯誤碼。 2) 設定執行緒棧 設定執行緒棧大小的函式是 pthread_attr_setstacksize(),原型如下:
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
引數 attr 都是一個指向已初始化的屬性物件的指標, stacksize 是設定的棧大小。成功返回 0,否則返回一個非 0 的錯誤碼。 4.4 執行緒範例-3  下面舉例說明執行緒建立及執行緒屬性的使用方法,主執行緒根據引數列表的引數給出的執行緒棧大小來設定執行緒屬性物件,然後為引數列表的剩餘引數分別建立執行緒來實現小寫轉大寫的功能及列印棧地址。
執行緒屬性示例
#include <pthread.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <ctype.h>

#define handle_error_en(en, msg) \ /* 出錯處理巨集供返回錯誤碼的函式使用 */
do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)
#define handle_error(msg) \ /* 出錯處理巨集 */
do { perror(msg); exit(EXIT_FAILURE); } while (0)

struct thread_info {
	pthread_t thread_id;
	int thread_num;
	char *argv_string;
};

static void *thread_start(void *arg){ /* 執行緒執行函式 */
	struct thread_info *tinfo = arg;
	char *uargv, *p;
	printf("Thread %d: top of stack near %p; argv_string=%s\n", /* 通過 p 的地址來計算棧的起始地址*/
							tinfo->thread_num, &p, tinfo->argv_string);
	
	uargv = strdup(tinfo->argv_string);	
	if (uargv == NULL)
		handle_error("strdup");
	for (p = uargv; *p != '\0'; p++)
		*p = toupper(*p); /* 小寫字元轉換大寫字元 */
	
	return uargv; /* 將轉換結果返回 */
}

int main(int argc, char *argv[])
{
	int s, tnum, opt, num_threads;
	struct thread_info *tinfo;
	pthread_attr_t attr;
	int stack_size;
	oid *res;
	stack_size = -1;
	
	while ((opt = getopt(argc, argv, "s:")) != -1) { /* 處理引數-s 所指定的棧大小 */
		switch (opt) {
			case 's':
				stack_size = strtoul(optarg, NULL, 0);
			break;
			default:
				fprintf(stderr, "Usage: %s [-s stack-size] arg...\n",argv[0]);
				exit(EXIT_FAILURE);
		}
	}
	
	num_threads = argc - optind;
	s = pthread_attr_init(&attr); /* 初始化屬性物件 */
	if (s != 0)
		handle_error_en(s, "pthread_attr_init");
	
	if (stack_size > 0) 
	{
		s = pthread_attr_setstacksize(&attr, stack_size); /* 設定屬性物件的棧大小 */
		if (s != 0)
			handle_error_en(s, "pthread_attr_setstacksize");
	}
	tinfo = calloc(num_threads, sizeof(struct thread_info));
	
	if (tinfo == NULL)
		handle_error("calloc");
	
	for (tnum = 0; tnum < num_threads; tnum++) 
	{
		tinfo[tnum].thread_num = tnum + 1;
		tinfo[tnum].argv_string = argv[optind + tnum];
		s = pthread_create(&tinfo[tnum].thread_id, &attr, /* 根據屬性建立執行緒 */
														&thread_start, &tinfo[tnum]);
		if (s != 0)
			handle_error_en(s, "pthread_create");
	}
	
	s = pthread_attr_destroy(&attr); /* 銷燬屬性物件 */
	if (s != 0)
		handle_error_en(s, "pthread_attr_destroy");
	
	for (tnum = 0; tnum < num_threads; tnum++) 
	{
		s = pthread_join(tinfo[tnum].thread_id, &res); /* 等待執行緒終止,並獲取返回值 */
		if (s != 0)
			handle_error_en(s, "pthread_join");
		
		printf("Joined with thread %d; returned value was %s\n",
		tinfo[tnum].thread_num, (char *) res);
		free(res);
	}
	
	free(tinfo);
	exit(EXIT_SUCCESS);
}

下圖是程式清單的一個執行結果,執行此程式是使用-s 引數指定每個建立執行緒的棧大小,每個執行緒執行起來後都先取棧變數的地址用過列印變數地址來大概估計棧起始的地址。然後每個執行緒將執行緒引數給出的字串轉換為大寫並返回給主執行緒,主執行緒使用
pthread_join()等待並獲取執行緒的結果。