4. Linux线程

前面有介绍过进程和多进程编程,进程中还可以有很多独立的线程,这样我们将可以将进程设计成某个时刻不止去做一件事,而是可以同时执行多个任务。多进程和多线程编程各有优劣,我们先来比较下二者。

  1. 进程是程序在某个数据集合上执行的过程,拥有自己独立的地址空间,系统资源分配和调度的独立单位;而进程中的多个线程共享相同的地址空间,一般只有自己独立的堆栈和寄存器信息,是系统调度的基本单元。
  2. 鉴于以上的特性,进程间共享数据只能通过各种通信的方式,这个代价是昂贵的,进程切换的开销很大;而线程共享地址空间,数据也是共享的,线程的开销也相对小很多。
  3. 进程用独立的资源,因此某个进程崩溃了,并不会影响到其它进程;但是线程一旦出问题,同进程内的其它线程势必都将受到影响。

下面我们切入本篇的正题,线程。

4.1 线程标识

如同每个进程都有一个PID一样,线程也有自己的标识符—TID。进程pid在系统内是惟一的,但是进程tid却只有在它所属的进程上下文中才有意义。TID的数据结构时pthread_t,定义在头文件pthread.h中,下面两个函数是tid相关的基本操作:

#inlcude<pthread.h>
pthread_t pthread_self(void);
//返回当前线程tid
int pthread_equal(pthread_t tid1, pthread_t tid2);
//比较tid1 和tid2是否相等,相等返回非0,否则返回0

4.2 线程属性

我们可以在创建前自定义线程的属性。Pthread提供了相关的操作接口。其数据结构表示如下:

typedef struct
{      
  int        detachstate;				//线程的分离状态
  int      	schedpolicy;    			//线程调度策略
  structsched_param  schedparam;//线程的调度參数
  int         inheritsched;     		//线程的继承性
  int         scope;         			//线程的作用域,linux只支持进程内竞争资源
  size_t       guardsize;     			//线程栈末尾的警戒缓冲区大小
  void*       stackaddr;      		//线程栈的位置
  size_t       stacksize;        		//线程栈的大小
}pthread_attr_t;

1. 属性的初始化和销毁

如果想通过自定义线程的属性,我们需在creat 线程前设定好线程的属性。设定线程的属性前我们则需要先初始化该属性变量。

#include<pthread.h>
int pthread_attr_t pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_t pthread_attr_destory(pthread_attr_t *attr);
//成功则返回0,否则返回错误编号

2. 线程分离属性

线程的属性中有分离和可结合两种,分别对应PTHREAD_CREATE_DETATCH和PTHREAD_CREATE_JOINABLE。Joinable状态是指线程即使退出了,其存储器资源(例如堆栈)是不释放的,只有被其它进程回收(pthread_join)之后才会释放;而分离状态则在进程退出的时候由系统回收了资源,默认是joinable。针对这种属性,pthread定义了set和get 两个接口。

#include<pthread.h>
int pthread_attr_getdetachstate(const pthread_attr_t *restrict attr, int *detachstate);
int pthread_attr_setdetachstate(const pthread_attr_t *restrict attr, int *detachstate);
//成功返回0,出错返回错误编号

3. 线程调度策略

Linux中线程的调用有三种策略,默认是第一种:

  • SCHED_OTHER(缺省)非实时调度,只有该线程执行完才会调度下一个任务
  • SCHED_RR(时间片轮转策略):实时调度,当线程的时间片用完,该线程被置于就绪队列尾部,重新分配时间片。这种策略可以保证所有具有相同优先级的RR任务的调度公平
  • SCHED_FIFO(先进先出策略):实时调度,先到先服务。一旦占用cpu则一直运行,直到有更高优先级任务到达或自己放弃。

同样,pthread定义了线程调度策略属性的get 和get接口:

#include<pthread.h>
int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy);  
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
//成功返回0,失败返回错误编号   

4. 线程调度参数

我们通常关心的线程调度参数只有线程的调度优先级,默认是0。

struct sched_param{
		int sched_priority;
}

针对该属性也有两个接口函数:

#include<pthread.h>
int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param);  
int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);
//成功返回0,失败返回错误编号

5. 线程继承调度策略属性

前面有介绍线程的调度策略属性,我们还有一种可以让线程去继承创建者的属性而不是使用我们设置的属性。

int pthread_attr_getinheritsched(const pthread_attr_t *attr, int *inheritsched);  
int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched);
//成功返回0,失败返回错误编号

其中的inheritsched属性有下面两种取值:PTHREAD_INHERIT_SCHED和PTHREAD_EXPLICIT_SCHED,前一种表示继承,但默认是后一种。

6. 线程栈地址和大小

对于进程来说,只有一个栈,所以不存在大小问题,但是对于线程来说,进程内的线程都要共享这同一块栈段,所以要节约着用。当然,如果进程的栈段分配完了,我们可以使用malloc或者mmap来寻找新的空间。

下面就是set 和 get 栈地址和大小的函数:

int pthread_attr_setstack(const pthread_attr_t *restrict attr, void **addr, size_t *stacksize);  
int pthread_attr_getstack(const pthread_attr_t *restrict attr, void *addr, size_t stacksize);
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);  
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);
//成功返回0,失败返回错误编号

7. 线程栈溢出保护区属性

线程属性guardsize控制着线程栈末尾之后是否有一个缓冲区来避免栈溢出,踩到其它线程栈的问题。设置为0时表示不设缓冲区,需要注意的是如果通过setstack设置stack地址后,系统认为我们自己管理栈地址,guardsize机制就失效了。

int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);  
int pthread_attr_getguardsize(pthread_attr_t *attr, size_t *guardsize); 
//成功返回0,失败返回错误编号 

4.3 线程控制

1. 线程创建

#include<pthread.h>
int pthread_creat(pthread *restrict tid,  const pthread_attr_t *restrict attr,
                  void *(*start_rtn)(void*),  void *restrict arg);

参数说明:

  • tid: 线程tid,由函数返回
  • attr: 线程属性,可由4.2节介绍的方式设置,但默认是NULL
  • start_rtn: 线程执行入口函数,没有参数也没有返回值
  • arg: 入口函数的参数

2. 线程终止

线程通常由三种正常的终止方式:

(1). 从启动例程中返回,返回状态为线程的退出吗

(2). 调用pthread_exit

#inlcude<pthread.h>
void pthread_exit(void *rval_ptr);

这里的参数rval_ptr就是exit函数的退出码,其它的线程可以通过pthread_join函数获取到这个退出码。但需要注意的是这个结构在所使用的内存在调用者完成调用后必须依旧是有效的。例如,在调用线程的栈上分配了该结构,在线程退出后,这个栈有可能就被撤销了,这时候其它线程就无法再获得这个结构的值。

(3). 线程可以被同进程中其它线程结束

#inlcude<pthread.h>
void pthread_cancel(pthread_t  tid);

线程可以通过上面的函数去取消另一个线程id为tid的线程,调用后,调用线程并不等待tid线程终止而是继续执行。但tid线程却并一定立即就去终止,这取决于tid进程的可取消状态和取消类型。

  1. 取消状态有enable 和disbale 两种,我们可以通常下面的函数来设置:

#inlcude<pthread.h>

void pthread_setcancelstate(int state,  int *oldstate);//执行成功返回0,失败返回失败编号

  1. PTHREAD_CANCEL_ENABLE: 线程可取消
  2. PTHREAD_CANCEL_DISABLE:线程不可取消,这种状态的线程收到取消请求会挂起这个请求指导状态变为enable 才会执行取消操作。注意只是挂起这个请求,不是挂起进程。
  1. 取消类型立即取消推迟取消,我们可以通过下面的函数来设置:
#inlcude<pthread.h>
void pthread_setcanceltype(int type,  int *oldtype);
//执行成功返回0,失败返回失败编号
  1. 立即取消:PTHREADCANCEL_DEFERRED,线程立即取消
  2. 推迟取消:PTHREAD_CANCEL_ASYNCHRONOUS,线程只有等到下一个取消点才会主动终止,取消点可以是POSIX.1定义的一些函数,也可以由用户通过下面函数设置:
#inlcude<pthread.h>
void pthread_testcancel(void);
//执行成功返回0,失败返回失败编号

 

3. 线程分离

线程可以通过下面的函数让自身阻塞起来等待某个线程终止:

#inlcude<pthread.h>
void pthread_join(pthread_t tid, void  **rval_ptr);
//执行成功返回0,失败返回失败编号

调用这个函数线程会阻塞,等到对应线程终止时,将tid线程置为分离状态。如果tid线程本身就为分离状态,则这个函数失败返回EINVAL。参数rval_ptr 可以获取tid线程的终止状态,如果不想知道这个状态,可以将这个参数设为NULL。

我们还可以通过另一个函数来对线程进行分离:

#inlcude<pthread.h>
void pthread_detach(pthread_t tid);
//执行成功返回0,失败返回失败编号

 

4. 线程清理处理函数

进程在退出的时候会调用登记函数atexit做清理工作。线程也有类似的操作,那就是清理处理程序。

#inlcude<pthread.h>
void pthread_cleanup_push(void* (*rtn(void *), void *arg);
void pthread_cleanup_pop(int excute);

看到push、pop是不是想到了栈?没错这些处理程序记录就是在栈中,所以执行顺序与登记顺序相反。关于清理处理程序,说明如下:

(1). 这两个函数可以实现为宏,所以必须在线程相同的作用域内以配对的形式使用;

(2). Push 函数在以下三种情况时都会被调用:

  •        调用pthread_exit
  •        响应取消请求
  • 以非0参数调用pop函数

(3). Pop函数的参数为0表示push 的函数不被调用;

(4). 不论(2)(3)中哪种情况,push建立的清理函数一定会被删除的。

 

5. 例程

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include"pthread.h"
#include<errno.h>

void cleanup(void *arg)
{
	printf("cleanup:%s\n",(char*)arg);
}

void * thread_fn1(void * arg)
{
	int *ptr = (int*)arg;
	
	printf("Thread1 start:\n");
	pthread_cleanup_push(cleanup,"Thread1 handler");
	pthread_cleanup_pop(1);
	getchar();
}

void * thread_fn2(void * arg)
{
	printf("Thread2 start:\n");
	pthread_cleanup_push(cleanup,"Thread2 handler");

	pthread_cancel((pthread_t)arg);
	
	pthread_cleanup_pop(1);
	pthread_exit((void*)2);
}

int main(int argc, char *argv[])
{
	int err;
	pthread_t tid1, tid2;
	void *tret;

	err = pthread_create(&tid1,NULL,thread_fn1,(void*)1);
	if(err != 0)
	{
		printf("Creat thread1 fail Reason:%s\n",strerror(errno));
	}
	
	err = pthread_create(&tid2,NULL,thread_fn2,(void*)tid1);
	if(err != 0)
	{
		printf("Creat thread2 fail Reason:%s\n",strerror(errno));
	}

	err = pthread_join(tid1,&tret);
	
	if(err != 0)
	{
		printf("Join thread1 fail Reason:%s\n",strerror(errno));
	}
	else
	{
		printf("Thread1 exit code:%ld\n",(long)tret);
	}
	
	err = pthread_join(tid2,&tret);

	if(err != 0)
	{
		printf("Join thread2 fail Reason:%s\n",strerror(errno));
	}
	else
	{
		printf("Thread1 exit code:%ld\n",(long)tret);
	}
	return 0;
}

上面的例子改写了下APUE中11-5的程序。创建两个线程,随后通常pthread_join去等待两个线程结束,并获取两个线程的终止码。原本线程1中执行到getchar() 因为等不到输入,一直卡在这里,但我们的thread2中使用了 pthread_cancel去请求取消thread1。结果如下:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值