2. 綫程控制

2.1. 創建綫程

#include <pthread.h>

int pthread_create(pthread_t *restrict thread,
	const pthread_attr_t *restrict attr,
	void *(*start_routine)(void*), void *restrict arg);

返回值:成功返回0,失敗返回錯誤號。以前學過的系統函數都是成功返回0,失敗返回-1,而錯誤號保存在全局變數errno中,而pthread庫的函數都是通過返回值返回錯誤號,雖然每個綫程也都有一個errno,但這是為了兼容其它函數介面而提供的,pthread庫本身並不使用它,通過返回值返回錯誤碼更加清晰。

在一個綫程中調用pthread_create()創建新的綫程後,當前線程從pthread_create()返回繼續往下執行,而新的綫程所執行的代碼由我們傳給pthread_create的函數指針start_routine決定。start_routine函數接收一個參數,是通過pthread_createarg參數傳遞給它的,該參數的類型為void *,這個指針按什麼類型解釋由調用者自己定義。start_routine的返回值類型也是void *,這個指針的含義同樣由調用者自己定義。start_routine返回時,這個綫程就退出了,其它綫程可以調用pthread_join得到start_routine的返回值,類似於父進程調用wait(2)得到子進程的退出狀態,稍後詳細介紹pthread_join

pthread_create成功返回後,新創建的綫程的id被填寫到thread參數所指向的內存單元。我們知道進程id的類型是pid_t,每個進程的id在整個系統中是唯一的,調用getpid(2)可以獲得當前進程的id,是一個正整數值。綫程id的類型是thread_t,它只在當前進程中保證是唯一的,在不同的系統中thread_t這個類型有不同的實現,它可能是一個整數值,也可能是一個結構體,也可能是一個地址,所以不能簡單地當成整數用printf打印,調用pthread_self(3)可以獲得當前線程的id。

attr參數表示綫程屬性,本章不深入討論綫程屬性,所有代碼例子都傳NULLattr參數,表示綫程屬性取預設值,感興趣的讀者可以參考[APUE2e]。首先看一個簡單的例子:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.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(arg);
	return NULL;
}

int main(void)
{
	int err;

	err = pthread_create(&ntid, NULL, thr_fn, "new thread: ");
	if (err != 0) {
		fprintf(stderr, "can't create thread: %s\n", strerror(err));
		exit(1);
	}
	printids("main thread:");
	sleep(1);

	return 0;
}

編譯運行結果如下:

$ gcc main.c -lpthread
$ ./a.out
main thread: pid 7398 tid 3084450496 (0xb7d8fac0)
new thread:  pid 7398 tid 3084446608 (0xb7d8eb90)

可知在Linux上,thread_t類型是一個地址值,屬於同一進程的多個綫程調用getpid(2)可以得到相同的進程號,而調用pthread_self(3)得到的綫程號各不相同。

由於pthread_create的錯誤碼不保存在errno中,因此不能直接用perror(3)打印錯誤信息,可以先用strerror(3)把錯誤碼轉換成錯誤信息再打印。

如果任意一個綫程調用了exit_exit,則整個進程的所有線程都終止,由於從main函數return也相當於調用exit,為了防止新創建的綫程還沒有得到執行就終止,我們在main函數return之前延時1秒,這只是一種權宜之計,即使主綫程等待1秒,內核也不一定會調度新創建的綫程執行,下一節我們會看到更好的辦法。

思考題:主綫程在一個全局變數ntid中保存了新創建的綫程的id,如果新創建的綫程不調用pthread_self而是直接打印這個ntid,能不能達到同樣的效果?

2.2. 終止綫程

如果需要只終止某個綫程而不終止整個進程,可以有三種方法:

  • 從綫程函數return。這種方法對主綫程不適用,從main函數return相當於調用exit

  • 一個綫程可以調用pthread_cancel終止同一進程中的另一個綫程。

  • 綫程可以調用pthread_exit終止自己。

pthread_cancel終止一個綫程分同步和非同步兩種情況,比較複雜,本章不打算詳細介紹,讀者可以參考[APUE2e]。下面介紹pthread_exit的和pthread_join的用法。

#include <pthread.h>

void pthread_exit(void *value_ptr);

value_ptrvoid *類型,和綫程函數返回值的用法一樣,其它綫程可以調用pthread_join獲得這個指針。

需要注意,pthread_exit或者return返回的指針所指向的內存單元必須是全局的或者是用malloc分配的,不能在綫程函數的棧上分配,因為當其它綫程得到這個返回指針時綫程函數已經退出了。

#include <pthread.h>

int pthread_join(pthread_t thread, void **value_ptr);

返回值:成功返回0,失敗返回錯誤號

調用該函數的綫程將掛起等待,直到id為thread的綫程終止。thread綫程以不同的方法終止,通過pthread_join得到的終止狀態是不同的,總結如下:

  • 如果thread綫程通過return返回,value_ptr所指向的單元裡存放的是thread綫程函數的返回值。

  • 如果thread綫程被別的綫程調用pthread_cancel異常終止掉,value_ptr所指向的單元裡存放的是常數PTHREAD_CANCELED

  • 如果thread綫程是自己調用pthread_exit終止的,value_ptr所指向的單元存放的是傳給pthread_exit的參數。

如果對thread綫程的終止狀態不感興趣,可以傳NULLvalue_ptr參數。

看下面的例子(省略了出錯處理):

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

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

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

void *thr_fn3(void *arg)
{
	while(1) {
		printf("thread 3 writing\n");
		sleep(1);
	}
}

int main(void)
{
	pthread_t   tid;
	void        *tret;

	pthread_create(&tid, NULL, thr_fn1, NULL);
	pthread_join(tid, &tret);
	printf("thread 1 exit code %d\n", (int)tret);

	pthread_create(&tid, NULL, thr_fn2, NULL);
	pthread_join(tid, &tret);
	printf("thread 2 exit code %d\n", (int)tret);

	pthread_create(&tid, NULL, thr_fn3, NULL);
	sleep(3);
	pthread_cancel(tid);
	pthread_join(tid, &tret);
	printf("thread 3 exit code %d\n", (int)tret);

	return 0;
}

運行結果是:

$ ./a.out 
thread 1 returning
thread 1 exit code 1
thread 2 exiting
thread 2 exit code 2
thread 3 writing
thread 3 writing
thread 3 writing
thread 3 exit code -1

可見在Linux的pthread庫中常數PTHREAD_CANCELED的值是-1。可以在標頭檔pthread.h中找到它的定義:

#define PTHREAD_CANCELED ((void *) -1)

一般情況下,綫程終止後,其終止狀態一直保留到其它綫程調用pthread_join獲取它的狀態為止。但是綫程也可以被置為detach狀態,這樣的綫程一旦終止就立刻回收它占用的所有資源,而不保留終止狀態。不能對一個已經處于detach狀態的綫程調用pthread_join,這樣的調用將返回EINVAL。對一個尚未detach的綫程調用pthread_joinpthread_detach都可以把該綫程置為detach狀態,也就是說,不能對同一綫程調用兩次pthread_join,或者如果已經對一個綫程調用了pthread_detach就不能再調用pthread_join了。

#include <pthread.h>

int pthread_detach(pthread_t tid);

返回值:成功返回0,失敗返回錯誤號。