线程(thread)



1、线程概念

1.1 线程和进程的关系
  1. 轻量级进程(light-weight process),也有PCB(Process Control Block,进程管理块),创建线程使用的底层函数和进程一样,都是clone
  2. 从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的
  3. 进程可以蜕变成线程
  4. 在美国人眼里,线程就是寄存器和栈
  5. 在Linux下,线程 最是小的 执行单位进程 是最小的 分配资源单位

1.2 线程间共享资源
  1. 文件描述符表
  2. 每种信号的处理方式
  3. 当前工作目录
  4. 用户ID和组ID
  5. 内存地址空间

text,data,bss,堆,共享库

线程间共享资源:
在这里插入图片描述


1.3 线程间非共享资源
  1. 线程id
  2. 处理器现场和栈指针(内核栈)
  3. 独立的栈空间(用户空间栈)
  4. errno变量
  5. 信号屏蔽字
  6. 调度优先级

1.4 线程优缺点

优点:
(1)提高程序的并发性(Concurrency)
(2)开销小,不用重新分配内存
(3)方便 通信和共享数据

缺点:
(1)线程 不稳定(库函数实现)
(2)线程调试 比较困难(gdb支持不好)
(3)线程 无法使用 Unix 经典事件,例如信号(Signal)


2、线程原语

2.1 pthread_create:创建线程
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
					void *(*start_routine) (void *), void *arg);

pthread_t *thread:传递一个pthread_t变量地址进来,用于保存新线程的tid(线程ID)
const pthread_attr_t *attr:线程属性设置,如使用默认属性,则传NULL
void *(*start_routine) (void *):函数指针,指向新线程应该加载执行的函数模块
void *arg:指定线程将要加载调用的那个函数的参数

返回值:成功返回0,失败返回错误号。以前学过的系统函数都是成功返回0,失败返回-1,而错误号保存在全局变量errno中,而pthread库的函数都是通过返回值返回错误号,虽然每个线程也都有一个errno,但这是为了兼容其它函数接口而提供的,pthread库本身并不使用它,通过返回值返回错误码更加清晰。

在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create() 返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。start_routine 函数接收一个参数,是通过pthread_create的arg参数传递给它的,该参数的类型为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参数表示线程属性,本节不深入讨论线程属性,所有代码例子都传NULL给attr参数,表示线程属性取缺省值。


2.2 pthread_self:获取调用线程tid
#include <pthread.h>
pthread_t pthread_self(void);

2.3 pthread_exit:调用线程退出函数

调用线程退出函数,注意和exit函数的区别,任何线程里exit导致进程退出,其他线程未工作结束,主控线程退出时不能return或exit。
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

#include <pthread.h>
void pthread_exit(void *retval);

void *retval:线程退出时传递出的参数,可以是退出值或地址,如是地址时,不能是线程内部申请的局部地址。

2.4 pthread_join
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

pthread_t thread:回收线程的tid
void **retval:接收退出线程传递出的返回值
返回值:成功返回0,失败返回错误号

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

  1. 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。
  3. 如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。

2.5 pthread_cancel

在进程内某个线程可以取消另一个线程。

#include <pthread.h>
int pthread_cancel(pthread_t thread);

被取消的线程,退出值,定义在 Linux的pthread库中常数PTHREAD_CANCELED的值是-1。可以在头文件pthread.h中找到它的定义:

#define PTHREAD_CANCELED ((void *) -1)

2.6 pthread_detach
#include <pthread.h>
int pthread_detach(pthread_t tid);
pthread_t tid:分离线程tid
返回值:成功返回0,失败返回错误号。

一般情况下,线程终止后,其终止状态一直保留到其它线程调用 pthread_join 获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL。如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。

2.7 pthread_equal

比较两个线程是否相等

#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);

3、线程终止方式

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 从线程主函数return。这种方法对主控线程不适用,从main函数return相当于调用
    exit。
  2. 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。
  3. 线程可以调用pthread_exit终止自己。

同一进程的线程间,pthread_cancel 向另一线程发终止信号。系统并不会马上关闭被取消线程,只有在被取消线程下次系统调用时,才会真正结束线程。或调用pthread_testcancel,让内核去检测是否需要取消当前线程。


4、线程属性(Attribute)

Linux下线程的属性是可以根据实际项目需要,进行设置,之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。如我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。

typedef struct
{
	int etachstate; 	//线程的分离状态
	int schedpolicy; 	//线程调度策略
	structsched_param schedparam; 	//线程的调度参数
	int inheritsched; 	//线程的继承性
	int scope; 			//线程的作用域
	size_t guardsize; 	//线程栈末尾的警戒缓冲区大小
	int stackaddr_set; 	//线程的栈设置
	void* stackaddr; 	//线程栈的位置
	size_t stacksize; 	//线程栈的大小
} pthread_attr_t;

注:目前线程属性在内核中不是直接这么定义的,抽象太深不宜拿出讲解,为方便大家理解,使用早期的线程属性定义,两者之间定义的主要元素差别不大。

属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在 pthread_create 函数之前调用。之后须用 pthread_attr_destroy 函数来释放资源。线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。默认的属性为非绑定、非分离、缺省M的堆栈、与父进程同样级别的优先级。


4.1 线程属性初始化

先初始化线程属性,再 pthread_create 创建线程

#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr); 		//初始化线程属性
int pthread_attr_destroy(pthread_attr_t *attr); 	//销毁线程属性所占用的资源

4.2 线程的分离状态(detached state)

线程的分离状态决定一个线程以什么样的方式来终止自己。

非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当 pthread_join() 函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。

线程分离状态的函数:

#include <pthread.h>
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); //设置线程属性,分离or非分离
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate); //获取程属性,分离or非分离

pthread_attr_t *attr:被已初始化的线程属性
int *detachstate:可选为PTHREAD_CREATE_DETACHED(分离线程)和 PTHREAD _CREATE_JOINABLE(非分离线程)

这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用 pthread_create 的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用 pthread_cond_timedwait 函数,让这个线程等待一会儿,留出足够的时间让函数 pthread_create 返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。

4.3 线程的栈地址(stack address)

POSIX.1定义了两个常量_POSIX_THREAD_ATTR_STACKADDR 和_POSIX_THREAD_ATTR_STACKSIZE检测系统是否支持栈属性。也可以给sysconf函数传递_SC_THREAD_ATTR_STACKADDR或 _SC_THREAD_ATTR_STACKSIZE 来进行检测。

当进程栈地址空间不够用时,指定新建线程使用由malloc分配的空间作为自己的栈空间。通过pthread_attr_setstackaddr和pthread_attr_getstackaddr两个函数分别设置和获取线程的栈地址。传给pthread_attr_setstackaddr函数的地址是缓冲区的低地址(不一定是栈的开始地址,栈可能从高地址往低地址增长)。

#include <pthread.h>
int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);
int pthread_attr_getstackaddr(pthread_attr_t *attr, void **stackaddr);

attr: 指向一个线程属性的指针
stackaddr: 返回获取的栈地址
返回值:若是成功返回0,否则返回错误的编号
说 明:函数已过时,一般用下面讲到的pthread_attr_getstack来代替

4.4 线程的栈大小(stack size)

当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用,当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。

函数pthread_attr_getstacksize和 pthread_attr_setstacksize提供设置。

#include <pthread.h>
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);

attr 指向一个线程属性的指针
stacksize 返回线程的堆栈大小
返回值:若是成功返回0,否则返回错误的编号

除上述对栈设置的函数外,还有以下两个函数可以获取和设置线程栈属性

#include <pthread.h>
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize);

attr 指向一个线程属性的指针
stackaddr 返回获取的栈地址
stacksize 返回获取的栈大小
返回值:若是成功返回0,否则返回错误的编号

4.5 线程属性控制实例
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <stdlib.h>
#define SIZE 0x10000
int print_ntimes(char *str)
{
	sleep(1);
	printf("%s\n", str);
	return 0;
}
void *th_fun(void *arg)
{
	int n = 3;
	while (n--)
	print_ntimes("hello xwp\n");
}
int main(void)
{
	pthread_t tid;
	int err, detachstate, i = 1;
	pthread_attr_t attr;
	size_t stacksize;
	void *stackaddr;
	
	pthread_attr_init(&attr);
	
	pthread_attr_getstack(&attr, &stackaddr, &stacksize);
	printf("stackadd=%p\n", stackaddr);
	printf("stacksize=%x\n", (int)stacksize);
	
	pthread_attr_getdetachstate(&attr, &detachstate);
	if (detachstate == PTHREAD_CREATE_DETACHED)
		printf("thread detached\n");
	else if (detachstate == PTHREAD_CREATE_JOINABLE)
		printf("thread join\n");
	else
		printf("thread un known\n");
	/* 设置线程分离属性 */
	pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
	
	while (1) {
		/* 在堆上申请内存,指定线程栈的起始地址和大小 */
		stackaddr = malloc(SIZE);
		if (stackaddr == NULL) {
			perror("malloc");
			exit(1);
		}
		stacksize = SIZE;
		pthread_attr_setstack(&attr, stackaddr, stacksize);
		
		err = pthread_create(&tid, &attr, th_fun, NULL);
		if (err != 0) {
			printf("%s\n", strerror(err));
			exit(1);
		}
		printf("%d\n", i++);
	}
	
	pthread_attr_destroy(&attr);
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值