Linux 系统编程 学习:09-线程:线程的创建、回收与取消
背景
我们在此之前完成了 有关进程的学习。从这一讲开始我们学习线程。
完全的开发可以参考:《多线程编程指南》
在Linux 系统编程 学习:有关概念中,我们介绍了线程和进程的概念。
概念
基础概念:
- 线程是cpu或操作系统调度的基本单位。线程大部分的资源是共享的,仅仅申请了自己的栈、空间。
- 线程是进程内部的一个执行分支,线程量级很小。
- 在程序中创建线程,可以提高效率,进程内线程越多,争夺到CPU的概率就越大,执行代码的概率就越大(有一个度)。
- 线程可以解决很多问题,而不会像进程一样有那么多的开销。
- 在线程中需要注意同步的问题。一个线程的bug很可能会引起该进程的崩溃。
线程与进程的内存分布不同:
- 每个进程在创建的时候都申请了新的内存空间以存储代码段\数据段\BSS段\堆\栈空间,并且这些的空间的初始化值是父进程空间的,父子进程在创建后不能互访资源。
- 而每个新创建的线程则仅仅申请了自己的栈、空间;与同进程的其他线程共享该进程的其他数据空间包括代码段\数据段\BSS段\堆以及打开的库、mmap映射的文件与共享的空间,使得同进程下的线程共享数据十分的方便,只需借助这些共享区域即可,但也有问题即是同步问题。
线程开发基本步骤
在接下来的开发中,我们会介绍有关的函数;
所有线程是在
<pthread.h>
中,且编译时需要链接pthread
库;下面不再说明。
同时,如果没有特殊说明,所有的函数在失败时都返回错误号(error number)。
线程的创建、回收与取消
创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
描述:创建一个线程,线程创建成功以后,开始执行指定的函数。
默认情况下,一个线程所使用的内存资源在应用pthread_join调用之前不会被重新分配,所以对于每个线程必须调用一次pthread_join函数(分离线程除外)。
参数解析:
thread:存放线程ID号的对象
attr:创建线程时设置的有关属性,可为空(这里先略过,我们会在后面专门讲到)
start_routine:线程执行的函数入口
arg:执行函数时附带的参数,可为空
返回值:成功返回0,从thread可以获取到线程ID;失败返回错误号。
回收线程
int pthread_join(pthread_t thread, void **retval);
描述:阻塞等待一个线程,并回收其资源。
默认情况下,新创建的线程是joinable的,线程退出后,需对其进行pthread_join操作。
如果不关心线程的返回值,我们可以告诉系统,当线程退出时,自动释放线程资源(后面我们会讲到)
参数解析:
thread:指定等待的线程号
retval:非空时,获取由pthread_exit(void *retval);
函数传过来的结果。
返回值:成功返回0,失败返回error number。
提出一个终止线程的请求
int pthread_cancel(pthread_t thread);
描述:一般情况下,线程在其主体函数退出的时候会自动终止,但同时也可以因为接收到另一个线程发来的终止(取消)请求而强制终止。
同一进程的线程间,pthread_cancel向另一线程发终止信号。如何处理Cancel信号则由目标线程自己决定:忽略、立即终止、继续运行至Cancelation-point(取消点),由不同的Cancelation状态(pthread_setcancelstate函数设置状态)决定。
被取消线程可以调用pthread_testcancel,让内核去检测是否需要取消当前线程。被取消的线程退出时总是返回
-1
(常数值PTHREAD_CANCELED)。如果在取消功能处处于禁用状态下调用pthread_testcancel(),则该函数不起作用。 请务必仅在线程取消线程操作安全的序列中插入pthread_testcancel()。除通过pthread_testcancel()调用以编程方式建立的取消点意外,pthread标准还指定了几个取消点。测试退出点,就是测试cancel信号
返回值: 发送成功返回0(不意味着thread会终止);失败返回错误号。
一个取消线程的例程
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *thread_fun(void *arg)
{
int i=1;
printf("thread start \n");
while(1)
{
// pthread_testcancel(); 如果没有这句话,那么线程不会结束。
i++;
}
return (void *)0;
}
int main(int argc, char* argv)
{
void *ret = NULL;
int iret = 0;
pthread_t tid;
pthread_create(&tid, NULL, thread_fun, NULL);
sleep(1);
pthread_cancel(tid);//取消线程
pthread_join(tid, &ret);
printf("thread 3 exit code %d\n", (int)ret);
return 0;
}
线程取消
线程在收到取消请求(pthread_cancel)后会继续运行,直到到达某个取消点(CancellationPoint)。
取消点:线程检查是否被取消并按照请求进行动作的一个位置。
pthreads标准指定了几个取消点,其中包括:
(1)通过pthread_testcancel
调用以编程方式建立线程取消点。
(2)线程等待pthread_cond_wait
或pthread_cond_timewait()
中的特定条件。 (错误的程序设计可能会在取消时导致死锁)
(3)被sigwait()
阻塞的函数
(4)一些标准的库调用。通常,这些调用包括线程可基于阻塞的函数。
设置一个线程能否被取消
int pthread_setcancelstate(int state, int *oldstate)
描述:设置一个线程能否被取消
参数解析:
state: 状态
- PTHREAD_CANCEL_ENABLE(缺省动作,收到信号后设为CANCLED状态)
- PTHREAD_CANCEL_DISABLE(忽略CANCEL信号继续运行)
old_state: 旧状态容器,如果不为 NULL则存入原来的Cancel状态。
设置本线程取消动作的执行时机
int pthread_setcanceltype(int type, int *oldtype)
描述:设置本线程取消动作的执行时机(仅当Cancel状态为Enable时有效)
参数解析:
type : 取消类型
- PTHREAD_CANCEL_DEFFERED (默认,收到信号后继续运行至下一个取消点再退出)
- PTHREAD_CANCEL_ASYCHRONOUS, (立即执行取消动作——退出)
oldtype : 旧状态容器,如果不为NULL则存入运来的取消动作类型值。
手动创建取消点
void pthread_testcancel(void)
描述: 手动创建一个取消点,但线程设置了PTHREAD_CANCEL_ENABLE
与PTHREAD_CANCEL_DEFFERED
属性,且已经有线程发送了取消本线程的请求时,退出;否则直接返回。
注意:由于此函数在线程内执行,执行的位置就是线程退出的位置,所以在执行此函数以前,线程内部的相关资源申请一定要释放掉,很容易造成内存泄露
总结:
1)线程可以调用pthread_setcancelstate()
设置被取消的,或者不能被取消的
2)线程的取消的本质是处理取消信号
3)取消线程可以马上进行的,也可以在取消点才取消 (pthread_setcanceltype()
)
若是在整个程序退出时,要终止各个线程,应该在成功发送 CANCEL 指令后,使用 pthread_join 函数, 等待指定的线程已经完全退出以后, 再继续执行; 否则,很容易产生 “段错误”。
线程遗嘱
遗嘱机制一般用于释放一些资源,比如释放锁,以免其它的线程永远 也不能获得锁,而造成死锁。
void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);
登记执行压栈清理函数
void pthread_cleanup_push(void (*routine)(void *), void *arg);
描述:登记执行压栈清理函数的操作。
参数解析:
routine:清理函数
arg:清理函数的有关参数
当以下描述的情况发生时自动调用的函数:
- 线程调用
pthread_exit()
函数,而不是直接return. - 响应取消请求时,也就是有其它的线程对该线程调用
pthread_cancel()
函数而到达取消条件时。 - 本线程调用pthread_cleanup_pop()函数,并且其参数非0。
从栈中删除清理函数的操作
void pthread_cleanup_pop(int execute);
描述:从栈中删除清理函数的操作。
参数解析:
execute:标志位,当其非0时,执行pthread_cleanup_push
中登记好的函数;否则,将其出栈,不执行。
例程
2条线程使用互斥锁(后面会讲到)抢占一个资源,当占有锁的其中一个线程被意外中断时之前写好了释放锁的遗嘱,另外一条能够正常拿到锁。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
pthread_mutex_t mutex_x=PTHREAD_MUTEX_INITIALIZER;
void clean_handler(void *arg)
{
pthread_mutex_unlock((pthread_mutex_t *)arg); // 防止死锁,所以在这里添加解锁操作
printf("unlocked from clean_handler push by thread_fun1\n");
}
void *thread_fun1(void *arg)
{
int i=1;
printf("thread1 start \n");
pthread_cleanup_push(clean_handler, &mutex_x);//提前登记线程被取消后需要处理的事情
pthread_mutex_lock(&mutex_x);
printf("thread_fun1 locked.\n");
while(1)
{
pthread_testcancel(); //如果没有这句话,那么线程不会结束。
i++;
}
pthread_mutex_unlock(&mutex_x);
printf("thread_fun1 unlocked\n");
pthread_cleanup_pop(0);
return (void *)0;
}
void *thread_fun2(void *arg)
{
int i=1;
printf("thread2 start \n");
for (i = 0; i < 6; ++i)
{
pthread_mutex_lock(&mutex_x);
printf("thread_fun2 locked.\n");
sleep(1);
pthread_mutex_unlock(&mutex_x);
printf("thread_fun2 unlocked\n");
}
printf("thread2 end \n");
return (void *)0;
}
int main(int argc, char* argv)
{
void *ret = NULL;
int iret = 0;
pthread_t tid1;
pthread_t tid2;
pthread_create(&tid1, NULL, thread_fun1, NULL);
pthread_create(&tid2, NULL, thread_fun2, NULL);
sleep(2);
pthread_cancel(tid1);//取消线程
pthread_join(tid1, &ret);
iret =(int)( (int*)ret);
printf("thread 3 exit code %d\n", iret);
while(1);
return 0;
}