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 系统没有创建线程所需要的资源
EINVAL attr参数无效
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 函数或者进程退出时才会释放资源。

只有非分离线程才是可连接的,而分离线程退出时不会报告线程的退出状态。

扫描二维码关注公众号,回复: 1680489 查看本文章

3.1 线程分离

pthread_detach()函数可以将非分离线程设置为分离线程,函数原型如下:

int pthread_detach(pthread_t thread);

参数 thread 是要分离的线程的 ID

线程可以自己来设置分离,也可以由其它线程来设置分离,以下代码线程可设置自身分离:
pthread_detach(pthread_self());
成功返回 0 ;失败返回一个非 0 的错误码, 下表 列出 pthread_detach 的实现必须检查的错误码。
pthread_detach 错误码表
错误码 出错描述
EINVAL thread参数所表示的线程不是可分离的线程
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 错误码表
错误码 出错描述
EINVAL thread参数所表示的线程不是可分离的线程
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() 等待并获取线程的结果。











猜你喜欢

转载自blog.csdn.net/faihung/article/details/80516965