APUE笔记—线程概念、创建、终止和属性
1. 线程的概念
- 线程(thread)就是运行在进程上下文中的逻辑流。
逻辑流:程序计数器PC(唯一地对应存放可执行目标文件指令的地方)的序列叫做逻辑控制流,简称逻辑流。简单说就是二进制序列。
- 每一个线程都有它自己的线程上下文(thread context),包括一个唯一的整数线程ID(TID)、栈、栈指针、程序计数器、通用目的寄存器和条件码。
线程上下文:进程上下文实际上是进程执行活动全过程的静态描述。我们把已执行过的进程指令和数据在相关寄存器与堆栈中的内容称为上文,把正在执行的指令和数据在寄存器和堆栈中的内容称为正文,把待执行的指令和数据在寄存器与堆栈中的内容称为下文。具体的说,进程上下文包括计算机系统中与执行该进程有关的各种寄存器(例如通用寄存器,程序计数器PC,程序状态字寄存器PS等)的值。
- 所有运行在一个进程的线程里共享该进程的整个虚拟地址空间。
2. 线程标识
- 线程类似进程的PID一样,有一个线程ID,进程ID唯一标示,但是线程ID只有在它所属的进程上下文中才有意义。
- 进程ID使用一个非负整数标示,但是线程ID使用pthread_t数据类型标示,不能作为整数处理。
使用下面的函数进行两个线程ID的比较。
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
//返回值:若相等,返回非0数值,若出错,则返回0
线程可以通过调用pthread_self()函数来获得自身的线程ID
#include <pthread.h>
pthread_t pthread_self(void);
返回值:调用线程的线程ID
3. 线程创建
- 一个进程最少拥有一个线程,在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参数用于定制不同线程属性,默认属性设置为NULL
- start_routine是新线程的执行的起始地址,它是一个返回值为void ,参数为void 的函数指针,通过第四个参数arg传入。
- 线程创建时并不能保证哪个线程会先运行,并且会继承调用线程的浮点环境和信号屏蔽字,但是线程的挂起信号集会被清除。
注意:pthread函数在调用失败时通常会返回错误码,并不像其他POSIX函数一样设置errno,每个线程有独有errno是为了更好的兼容性,返回错误码会直接将错误限制在引起错误的函数内。
4. 线程终止
如果线程调用exit、_Exit或者_exit函数将终止整个进程。因此,线程通过3种方式安全的退出:
- 从启动例程中返回,退出码是返回值。
- 线程可以被同一进程内的其他线程取消。
- 线程调用pthread_exit。
#include <pthread.h>
void pthread_exit(void *retval);
- 参数retval就是相当于线程的返回值
进程中的其他线程可以通过调用pthread_join函数访问这个指针,也就是说可以得到线程的退出状态。
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
//返回值:若成功,返回0;否则返回错误编号
- pthread指定等待的线程iID
- retval是一个二级指针,存放线程ID为pthread线程退出时返回码的地址,不关心设置为NULL。
- 若果线程被取消,retval被设置为PTHRAD_CANCELED。
线程可以调用pthread_cancel函数来取消同一进程中的其他线程。
#include <pthread.h>
int pthread_cancel(pthread_t thread);
//返回值:若成功,返回0;否则返回错误编号
- 线程将会请求取消PID为pthread的线程,被取消的线程如同条用pthread_exit(PTHRAD_CANCELED)函数。仅仅是请求,线程可以选择忽略取消。
- 统一进程的线程间,pthread_cancel向另一线程发终止信号。系统并不会马上关闭被取消的进程,只有在被取消进程下次系统调用(系统从用户态转为内核态)时,才会真正结束线程。或调用pthread_testcancel,让内核取检测是否需要取消当前线程。
调用pthread_detach函数可以分离线程
#include <pthread.h>
int pthread_detach(pthread_t thread);
//返回值:若成功,返回0;否则返回错误编号
- 默认情况下,线程的终止状态会保存直到对该线程调用pthread_join,如果线程已经被分离,线程的底层存储资源可以在线程终止时立即被收回。
- 被分离的线程,不能调用pthread_join函数来等待其终止状态。
线程原语的代码片段
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
void *thr_fun1(void *arg)
{
printf("thread 1 returning \n");
sleep(3);
return "hello";
}
void *thr_fun2(void *arg)
{
printf("thread 2 exiting \n");
pthread_exit((void *)2);
}
void *thr_fun3(void *arg)
{
while(1){
printf("thread 3 writing\n");
sleep(1);
}
}
int main(void)
{
pthread_t tid;
void *tret;
pthread_create(&tid, NULL, thr_fun1, NULL);
pthread_join(tid, &tret);
printf("thread 1 exit code :%s\n", (char *)tret);
pthread_create(&tid, NULL, thr_fun2, NULL);
pthread_join(tid, &tret);
printf("thread 2 exit code :%ld\n", (long)tret);
pthread_create(&tid, NULL, thr_fun3, NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &tret);
printf("thread 3 exit code :%ld\n", (long)tret);
return 0;
}
运行结果:
➜ THREAD ./a.out
thread 1 returning
//阻塞等待线程1三秒
thread 1 exit code :hello
thread 2 exiting
thread 2 exit code :2
thread 3 writing
thread 3 writing
thread 3 writing
//线程3运行3秒后被取消,被取消的线程退出码为-1
thread 3 exit code :-1
5. 线程属性
线程创建时的默认属性可以使用ulimt -a查看。当然也可以修改线程的属性,来提高性能。
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的堆栈、与父进程同样级别的优先级。
5.1 线程属性初始化
先初始化线程属性,在pthread_create创建线程。
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
//返回值:若成功,返回0,若出错,返回错误编号。
5.2 线程的分离状态(detached state)
线程的分离状态决定一个线程以什么样的方式来终止自己
#include <pthread.h>
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
//返回值:若成功,返回0,若出错,返回错误编号。
- 非分离状态:线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
- 分离状态:分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。
- 这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里pthread_cond_timedwait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。
5.3 修改线程分离属性实例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *th_fun(void *arg)
{
int n = 15;
while(n--) {
printf("%x\t%d\n", (int)pthread_self(), n);
sleep(1);
}
return (void *)1;
}
int main()
{
pthread_t tid;
int err;
void *tret;
pthread_attr_t attr; //attribute属性
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);//属性设置为DETACHED,分离状态
pthread_create(&tid, &attr, th_fun, NULL);
err = pthread_join(tid, &tret);
if(err != 0) {
printf("%s\n", strerror(err));
sleep(5);
pthread_exit((void *)1);
}
return 0;
}
经过5秒后,通过ps -eLf命令查看线程状态信息如下:
5745 5472 5745 0 2 14:47 pts/4 00:00:00 [a.out] <defunct>
5745 5472 5746 0 2 14:47 pts/4 00:00:00 [a.out] <defunct>
线程变成了“僵尸线程”,类似与僵尸进程。
- 产生原因是:被创建出的线程因为修改成了分离状态(detached state),当主线程提前退出时,子线程资源无法被回收进而产生“僵尸线程”。