多線程基礎概念

1、一個進程中的所有線程都可以訪問該進程的內容,例如文件描述符和內存。

2、處理器的數量並不影響程序結構,所以不管處理器的個數是多少,程序都可以通過使用線程得以簡化,所以即使程序運行在單處理器上,也能得到多線程編程模型的好處。

3、線程包含了表示進程內執行環境必需的信息,包括進程中標識線程的線程ID、一組寄存器值、棧、調度優先級和策略、信號屏蔽字、errno變量以及線程私有數據。

4、進程的所有信息對於該進程內的所有線程都是共享的,包括可執行的程序文本、程序的全局內存和堆內存、棧以及文件描述符。

進程ID在整個系統中是唯一的,但是線程ID卻只在它所屬的那個進程環境有效。線程可以通過調用pthread_self函數來獲得自身的線程ID

pthread_t pthread_self(void);

在POSIX線程中,我們通過調用pthread_create函數來創建新線程

#include<pthread.h>  
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
若成功返回0,否則返回錯誤編號,thread所指向的內存單元被設置爲新創建線程的ID,attr用於定製各種不同的線程屬性,新創建的線程從start_routine函數開始執行,需要注意如果向start_routine函數傳遞的參數不止一個,那麼需要把這些參數放到一個結構體中,然後把結構的地址作爲arg參數傳入,新創建的線程可以訪問進程的地址空間,並且繼承調用線程(主線程)的浮點環境和信號屏蔽字,但是新創建線程的未決信號集被清除。

打印線程ID:

#include "apue.h"
#include<pthread.h>

pthread_t ntid;
void
printids(const char *s)
{
	pid_t pid;
	pthread_t tid;

	pid = getpid();
	tid = pthread_self();
	printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int)pid,
			(unsigned int)tid, (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)
		err_quit("can't create thread: %s\n", strerror(err));
	printids("main thread: ");
	sleep(1);
	exit(0);
}

運行程序得到下面的結果:

erhai@erhai-ubuntu:~/APUE$ gcc -o 11-1 11-1.c errorapue.c -pthread
erhai@erhai-ubuntu:~/APUE$ ./11-1
main thread:  pid 5142 tid 1028290304 (0x3d4a7700)
new thread:  pid 5142 tid 1020008192 (0x3ccc1700)

上面的程序中需要注意兩個特別的地方,首先主線程需要休眠,如果主線程不休眠,它就可能退出,這樣在新線程有機會運行前整個進程可能已經終止了。其次是新線程通過調用pthread_self函數獲取自己的線程ID,而不是從共享內存中讀出或者從線程啓動例程中以參數的形式接受到。因爲如果新線程在主線程調用pthread_create返回之前就運行了,那麼新線程得到的是爲初始化的thread的內容,這個內容不是正確的線程ID。


如果進程中任一線程調用exit、_Exit或者_exit,那麼整個進程都會終止。與此類似,如果一個信號的默認動作是終止進程,那麼,把該信號發送的線程會終止整個這個進程。

如果需要在不終止整個進程的情況下需要退出單個線程,可以通過下面的方法實現

  • 線程只是從啓動例程(創建線程)中返回,返回值是線程的退出碼。
  • 線程可以被同一進程中的其他線程取消(通過調用pthread_cancel函數)
  • 線程調用pthread_exit
void pthread_exit(void *retval);

進程中的其他線程可以通過調用pthread_join函數訪問retval這個指針,等待指定的線程終止,並獲得線程的退出狀態。

通過調用pthread_join自動把線程置於分離狀態。默認情況下,線程的終止狀態會保存到對該線程調用pthread_join,因爲線程還不是分離狀態。如果線程已經處於分離狀態,線程底層存儲資源可以在線程終止時立即被收回,而不會再保存終止狀態,當線程被分離時,並不能用pthread_join函數等待它的終止狀態。pthread_detach函數可以使線程進入分離狀態。

int pthread_join(pthread_t thread, void **retval);
int pthread_detach(pthread_t tid);

調用線程(主線程)將一直阻塞,直到指定的線程thread調用pthread_exit、從啓動例程(創建線程)中返回或者被其他線程取消。如果線程只是從它的啓動例程返回,retval將包含返回碼。如果線程被取消,有retval指定的內存單元就置爲PTHREAD_CANCELED。如果對線程返回值不感興趣,可以把retval置爲NULL。

獲得線程退出狀態的實例:

#include "apue.h"
#include<pthread.h>

void *
thr_fn1(void *arg)
{
	printf("thread 1 returning\n");
	return ((void *)10);
}

void *
thr_fn2(void *arg)
{
	printf("thread 2 exiting\n");
	pthread_exit((void *)20);
}

int
main(void)
{
	int err;
	pthread_t tid1, tid2;
	void *tret;

	err = pthread_create(&tid1, NULL, thr_fn1, NULL);
	if (err != 0)
		err_quit("can't create thread 1: %s\n", strerror(err));
	err = pthread_create(&tid2, NULL, thr_fn2, NULL);
	if (err != 0)
		err_quit("can't create thread 2: %s\n", strerror(err));

	err = pthread_join(tid1, &tret);
	if (err != 0)
		err_quit("can't join with thread 1:%s\n", strerror(err));
	printf("thread 1 exit code %d\n", (int)tret);
	err = pthread_join(tid2, &tret);
	if (err != 0)
		err_quit("can't join with thread 2: %s\n", strerror(err));
	printf("thread2 exit code %d\n", (int)tret);
	exit(0);
}
以上程序的運行結果是:

erhai@erhai-ubuntu:~/APUE$ gcc -o 11-2 11-2.c errorapue.c -lpthread
11-2.c: 在函數‘main’中:
11-2.c:42:36: 警告: 將一個指針轉換爲大小不同的整數 [-Wpointer-to-int-cast]
11-2.c:46:35: 警告: 將一個指針轉換爲大小不同的整數 [-Wpointer-to-int-cast]
erhai@erhai-ubuntu:~/APUE$ ./11-2
thread 2 exiting
thread 1 returning
thread 1 exit code 10
thread2 exit code 20
通過運行結果可以看出,當一個線程通過調用pthread_exit退出或者只是從啓動例程返回時,進程中的其他線程(例如實例中的主線程)就可以通過調用pthread_join函數獲得這個線程的退出狀態。


pthread_create和pthread_exit函數的無類型指針參數能傳遞的數值可以不止一個,該指針可以傳遞包含更復雜信息的結構的地址,但是這個需要知道這個結構所使用的內存在調用者完成調用後必須仍然是有效的,否則就會出現無效或非法內存訪問。

下面的實例給出了用自動變量(分配在棧上)作爲pthread_exit函數的參數時出現的問題:

#include "apue.h"
#include<pthread.h>

struct foo {
	int a, b, c, d;
};

void 
printfoo(const char *s, const struct foo *fp)
{
	printf(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 %d\n", (int)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)
		err_quit("can't create thread 1: %s\n", strerror(err));
	err = pthread_join(tid1, (void *)&fp);
	if (err != 0)
		err_quit("can't join with thread 1: %s\n", strerror(err));
	sleep(1);

	printf("parent starting second thread\n");
	err = pthread_create(&tid2, NULL, thr_fn2, NULL);
	if (err != 0)
		err_quit("can't create thread 2: %s\n", strerror(err));
	sleep(1);
	printfoo("parent:\n", fp);
	exit(0);
}
erhai@erhai-ubuntu:~/APUE$ gcc -o 11-3 11-3.c errorapue.c -lpthread
11-3.c: 在函數‘printfoo’中:
11-3.c:18:2: 警告: 格式字符串不是一個字面字符串而且沒有待格式化的實參 [-Wformat-security]
11-3.c:19:33: 警告: 將一個指針轉換爲大小不同的整數 [-Wpointer-to-int-cast]
erhai@erhai-ubuntu:~/APUE$ ./11-3 
thread 1: 
 structure at 0x239c9ed0

 foo.a = 1
 foo.b = 2
 foo.c = 3
 foo.d = 4
parent starting second thread
thread 2: ID is 597468928
parent:
 structure at 0x239c9ed0

 foo.a = 597469632
 foo.b = 32714
 foo.c = 1
 foo.d = 0

通過結果可以看出,在線程tid1的棧上分配的結構內容,當主線程通過調用printfoo得到的內容已經改變了。線程tid1和tid2的structure最終內存地址是一樣的,說明線程tid2的棧把線程tid1的棧已經完全覆蓋了。爲了解決這個問題,可以使用全局結構或者使用malloc函數動態分配結構。

線程可以通過調用pthread_cancel函數來請求取消同一進程中的其他線程:

int pthread_cancel(pthread_t tid);
但是線程可以選擇忽略取消方式或是控制取消方式,pthread_cancel並不等待線程終止,它僅僅提出請求。

線程也可以安排它退出時需要調用的函數,這與進程調用atexit函數安排進程退出時需要調用的函數是類似的。這樣的函數被稱爲線程清理處理程序,線程可以建立多個清理處理程序。處理程序記錄在棧中,所以說他們的執行的順序與註冊的順序是相反的。

void pthread_cleanup_push(void (*rtn)(void *), void *arg);
void pthread_cleanup_pop(int execute);
當線程執行以下動作時調用清理函數,調用參數爲arg,清理函數rtn的調用順序由pthread_cleanup_push函數來安排。

  • 調用pthread_exit時
  • 響應取消請求時
  • 用非零execute參數調用pthread_cleanup_pop函數時
如果execute參數置爲0,那麼清理函數將不執行。無論哪種情況,pthread_cleanup_pop都將刪除上次pthread_cleanup_push調用建立的清理處理程序。

下面的實例顯示瞭如何使用線程清理處理程序:

#include "apue.h"
#include<pthread.h>

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(0);
	pthread_cleanup_pop(0);
	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)
		err_quit("can't create thread 1: %s\n", strerror(err));
	
	err = pthread_create(&tid2, NULL, thr_fn2, (void *)1);
	if (err != 0)
		err_quit("can't create thread 2: %s\n", strerror(err));

	err = pthread_join(tid1, &tret);
	if (err != 0)
		err_quit("can't join with thread 1: %s\n", strerror(err));
	printf("thread 1 exit code %d\n", (int)tret);
	
	err = pthread_join(tid2, &tret);
	if (err != 0)
		err_quit("can't join with thread 2: %s\n", strerror(err));
	printf("thread 2 exit code %d\n", (int)tret);

	exit(0);
}


程序的運行結果是:

erhai@erhai-ubuntu:~/APUE$ gcc -o 11-4 11-4.c errorapue.c -lpthread
11-4.c: 在函數‘main’中:
11-4.c:63:36: 警告: 將一個指針轉換爲大小不同的整數 [-Wpointer-to-int-cast]
11-4.c:68:36: 警告: 將一個指針轉換爲大小不同的整數 [-Wpointer-to-int-cast]
erhai@erhai-ubuntu:~/APUE$ ./11-4
thread 2 start
thread 2 push complete
thread 1 start
thread 1 push complete
thread 1 exit code 1
cleanup: thread 2 second handler
cleanup: thread 2 first handler
thread 2 exit code 2

通過運行結果可以看出,只調用了第二個線程的清理處理程序,所以如果線程是通過它的啓動例程(創建線程)中返回而終止的話,那麼它的清理處理程序就不會被調用,還要注意清理處理程序的執行順序與設置的順序是相反的。







上面的程序編譯產生的警告信息沒有找到解決辦法,請知道的朋友告知一下,非常感謝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章