Linux 系统编程 学习:09-线程:线程的创建、回收与取消

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_waitpthread_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_ENABLEPTHREAD_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;
}
posted @ 2020-03-31 19:31  schips  阅读(710)  评论(0编辑  收藏  举报