linux 系统编程之线程

线程

文章目录

  • 线程
    • 1 线程概念
    • 2 NPT
      • 安装线程 man page:
      • 查看指定线程的 LWP 号:
    • 3 线程的特点
    • 4 线程共享资源
    • 5 线程非共享资源
    • 6 线程的优缺点
    • 7线程常用操作
      • 1 线程号
        • pthread_self函数:
        • pthread_equal函数:
        • 参考代码
      • 2 错误返回值分析
        • 参考代码
      • 3 线程的创建
        • pthread_create函数:
        • 参考代码
        • 创建多个线程
          • 传地址作为函数参数
          • 传值作为函数参数
      • 4 线程共享资源的验证
          • 共享数据段
          • 共享堆空间
      • 5 线程资源回收
        • pthread_join函数:
        • 参考代码
      • 6 线程分离
        • pthread_detach函数:
        • 参考代码
      • 7 线程退出
        • pthread_exit函数
        • 参考代码
      • 8 线程取消
        • pthread_calcel函数
        • 参考代码
        • 无效的线程取消
          • 参考代码
        • 线程对Cancel信号的处理
          • 参考代码(修复无效的线程取消)
      • 9 线程清理
        • 使用方法
        • 线程清理函数调用
        • 线程清理例程
    • 8 线程属性
      • 8.2 线程属性初始化和销毁
      • 8.3 线程分离状态
      • 8.4 线程栈地址
      • 8.5 线程栈大小
      • 8.6 综合参考程序
      • 8.7 线程使用注意事项
    • 补充 : pthread_join的第二个参数

1 线程概念

在许多经典的操作系统教科书中,总是把进程定义为程序的执行实例,它并不执行什么, 只是维护应用程序所需的各种资源,而线程则是真正的执行实体。所以,线程是轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍是进程。为了让进程完成一定的工作,进程必须至少包含一个线程。

linux 系统编程之线程_第1张图片

linux 系统编程之线程_第2张图片

  • 进程,直观点说,保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己的地址空间,有自己的堆,上级挂靠单位是操作系统。操作系统会以进程为单位,分配系统资源,所以我们也说,进程是CPU分配资源的最小单位

  • 线程存在与进程当中(进程可以认为是线程的容器),是操作系统调度执行的最小单位。说通俗点,线程就是干活的。

  • 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。

  • 线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

  • 如果说进程是一个资源管家,负责从主人那里要资源的话,那么线程就是干活的苦力。一个管家必须完成一项工作,就需要最少一个苦力,也就是说,一个进程最少包含一个线程,也可以包含多个线程。苦力要干活,就需要依托于管家,所以说一个线程,必须属于某一个进程。

  • 进程有自己的地址空间,线程使用进程的地址空间,也就是说,进程里的资源,线程都是有权访问的,比如说堆啊,栈啊,静态存储区什么的。

  • 多线程和多进程的区别:

    • 多进程共享的资源:
      • 代码
      • 文件描述符
      • 内存映射区 --mmap
    • 多线程共享的资源:
      • 全局变量
      • 相比多进程,更加节省系统资源;对于系统 CPU 轮转时间片来说,不论是线程还是进程,它不认识,只认 PCB。
  • 主线程和子线程:

    • 共享:
      • 用户区内,除了栈区是不共享的,其余都是不共享的。
    • 不共享:
      • 栈区(当有 1 主 + 4 子线程时候,栈区会被平分为 5 份)
  • 在 Linux 下:

    • 线程就是进程 – 轻量级的进程
    • 对于内核来说,线程就是进程(内核只会用)

进程是操作系统分配资源的最小单位

线程是操作系统调度的最小单位

2 NPT

  • 当 Linux 最初开发时,在内核中并不能真正支持线程。但是它的确可以通过 clone() 系统调用将进程作为可调度的实体。这个调用创建了调用进程(calling process)的一个拷贝,这个拷贝与调用进程共享相同的地址空间。LinuxThreads 项目使用这个调用来完全在用户空间模拟对线程的支持。不幸的是,这种方法有一些缺点,尤其是在信号处理、调度和进程间同步原语方面都存在问题。另外,这个线程模型也不符合POSIX的要求。

  • 要改进 LinuxThreads,非常明显我们需要内核的支持,并且需要重写线程库。有两个相互竞争的项目开始来满足这些要求。一个包括 IBM 的开发人员的团队开展了 NGPTNext-GenerationPOSIX Threads)项目。同时,Red Hat 的一些开发人员开展了 NPTL 项目。NGPT 在 2003 年中期被放弃了,把这个领域完全留给了 NPTL

  • NPTL,或称为 Native POSIX Thread Library,是 Linux 线程的一个新实现,它克服了 LinuxThreads 的缺点,同时也符合POSIX的需求。与 LinuxThreads 相比,它在性能和稳定性方面都提供了重大的改进。

  • 查看当前pthread库版本:getconf GNU_LIBPTHREAD_VERSION

linux 系统编程之线程_第3张图片

安装线程 man page:

  • 安装线程 man page 命令:

    sudo apt install manpages-posix-dev
    1
    

查看指定线程的 LWP 号:

  • 线程号和线程 ID 是有区别的
  • 线程号是给内核看的
  • 查看方式:
    • 找到程序的进程 ID
    • ps -Lf pid

一个例子,查看火狐浏览器程序由多线程构成:

如: Linux 下 查看火狐浏览器,发现其是由多线程构成的

ps ajx | grep "firefox"

ps -Lf 31102
123

3 线程的特点

类Unix系统中,早期是没有“线程”概念的,80年代才引入,借助进程机制实现出了线程的概念。因此在这类系统中,进程和线程关系密切:

  • \1) 线程是轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone
  • \2) 从内核里看进程和线程是一样的,都有各自不同的PCB.
  • \3) 进程可以蜕变成线程
  • \4) 在linux下,线程最是小的执行单位;进程是最小的分配资源单位

linux 系统编程之线程_第4张图片

查看指定进程的LWP号:

ps -Lf pid

实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数 clone 。

Ø 如果复制对方的地址空间,那么就产出一个“进程”;

Ø 如果共享对方的地址空间,就产生一个“线程”。

Linux内核是不区分进程和线程的, 只在用户层面上进行区分。所以,线程所有操作函数 pthread_* 是库函数,而非系统调用。

4 线程共享资源

  • \1) 文件描述符表

  • \2) 每种信号的处理方式

  • \3) 当前工作目录

  • \4) 用户ID和组ID

  • 内存地址空间 (.text/.data/.bss/heap/共享库)

5 线程非共享资源

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

6 线程的优缺点

优点:

  • Ø 提高程序并发性
  • Ø 开销小
  • Ø 数据通信、共享数据方便

缺点:

  • Ø 库函数,不稳定
  • Ø 调试、编写困难、gdb不支持
  • Ø 对信号支持不好

优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大。

7线程常用操作

1 线程号

就像每个进程都有一个进程号一样,每个线程也有一个线程号。进程号在整个系统中是唯一的,但线程号不同,线程号只在它所属的进程环境中有效。

进程号用 pid_t 数据类型表示,是一个非负整数。线程号则用 pthread_t 数据类型来表示,Linux 使用无符号长整数表示。

有的系统在实现pthread_t 的时候,用一个结构体来表示,所以在可移植的操作系统实现不能把它做为整数处理。

pthread_self函数:

#include 
pthread_t pthread_self(void);
功能:
    获取线程号。
参数:
    无
返回值:
    调用线程的线程 ID 。
    

pthread_equal函数:

int pthread_equal(pthread_t t1, pthread_t t2);
功能:
    判断线程号 t1 和 t2 是否相等。为了方便移植,尽量使用函数来比较线程 ID。
参数:
    t1,t2:待判断的线程号。
返回值:
    相等:  非 0
    不相等:0

参考代码

// todo 创建线程号,获取线程号,比较线程号
#include "../tou.h"
int main()
{
    pthread_t tid = 0;    // todo 创建线程号

//todo 如果不确定 pthread_t 是无符号的整型,还是一个结构体,可以使用下面的方式进行初始化
/*
memset(&tid,0,sizeof(tid));
或者

bzero(&tid,sizeof(tid));
*/
    tid = pthread_self(); // todo 获取当前线程的线程号
    printf("tid: =%ld", tid);
    // todo 为了方便移植, 使用函数来比较线程id
    pthread_t tid2 = pthread_self();
    if (pthread_equal(tid, tid2))
    {
        printf("两个线程相同\n");
    }
    else
    {
        printf("两个线程不同\n");
    }
}

【注意】线程函数的程序在 pthread 库中,故链接时要加上参数 -lpthread。

2 错误返回值分析

注意,所有线程的错误号返回都只能使用strerror这个函数判断,不能使用perror .因为perror是调用进程的全局错误号,不适合单独线程的错误分析,所以只能使用strerror

参考代码

#include "../tou.h"

void *thrd_func(void *arg)
{
    printf("i am detach.\n");
}

int main(void)
{
    pthread_t tid;
    int ret;

    ret = pthread_create(&tid, NULL, thrd_func, NULL);
    if (ret != 0)
    {
        fprintf(stderr, "pthread_create error:%s\n", strerror(ret));
        exit(1);
    }

    ret = pthread_detach(tid);
    if (ret != 0)
    {
        fprintf(stderr, "pthread_detach error:%s\n", strerror(ret));
        exit(1);
    }

    sleep(1);
    ret = pthread_join(tid, NULL);
    if (ret != 0)
    {
        fprintf(stderr, "pthread_detach error:%s\n", strerror(ret));
        exit(1);
    }
    // 如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。
}

在一个线程中调用pthread_create()创建新的线程后,当前线程从pthread_create()返回继续往下执行,而新的线程所执行的代码由我们传给pthread_create的函数指针start_routine决定。

由于pthread_create的错误码不保存在errno中,因此不能直接用perror()打印错误信息,可以先用strerror()把错误码转换成错误信息再打印。

3 线程的创建

pthread_create函数:

#include 
int pthread_create(pthread_t *thread,
            const pthread_attr_t *attr,
            void *(*start_routine)(void *),
            void *arg );
功能:
    创建一个线程。
参数:
    thread:线程标识符地址。
    attr:线程属性结构体地址,通常设置为 NULL。
    start_routine:线程函数的入口地址。
    arg:传给线程函数的参数。
返回值:
    成功:0
    失败:非 0

参考代码

#include "../tou.h"
void *func(void *arg)
{
    printf("子线程:%ld被执行\n", pthread_self());
    if (arg == NULL)
    {
        printf("参数为空\n");
    }
    return NULL;
}
void *func2(void *arg)
{

    printf("子线程:%ld被执行\n", pthread_self());
    if (arg != NULL)
    {
        int arg_t = (int)(long)arg; // 把八个字节的arg 给四个字节的arg_t .long类型占八字节
        printf("传入的参数为%d\n", arg_t);
    }
    return NULL;
}
int main()
{
    pthread_t tid = -1;
    int ret = -1;
    // 传入空的线程描述符和空的线程函数参数
    ret = pthread_create(&tid, NULL, func, NULL); // 如果ret >0 就说明创建成功
    if (ret == 0)
    {
        printf("2子线程:%ld被执行\n", tid);
    };

    // 传入空的线程描述符和 一个线程参数
    pthread_t tid2 = -1;
    ret = pthread_create(&tid2, NULL, func2, (void *)200); // void* 占8字节
    if (ret == 0)                                          // 如果ret >0 就说明创建成功
    {
        printf("子线程:%ld被执行\n", tid2);
    }; // 传入空的线程描述符和空的线程函数参数
    pthread_join(tid, NULL);
    pthread_join(tid2, NULL);
}

创建多个线程

传地址作为函数参数
#include
#include
#include
#include
#include
#include

void* pthread_fun(void* arg)
{
    int index = *(int*)arg;
    int i = 0;
    for(; i < 5; i++)
    {
        printf("index = %d\n",index);
        sleep(1);
    }
}

int main()
{
    pthread_t id[5];
    int i = 0;
    for(; i < 5; i++)
    {
        pthread_create(&id[i],NULL,pthread_fun,(void*)&i);
    }

    for(i = 0; i < 5; i++)
    {
        pthread_join(id[i],NULL);
    }

    exit(0);
}


运行结果

linux 系统编程之线程_第5张图片

或者

linux 系统编程之线程_第6张图片

为什么会产生这种情况呢?线程并发问题。

这是因为我们向pthread_fun传入i的地址。首先来说说为什么会出现多个线程拿到同一个i的值。线程创建在计算机中需要很多个步骤,我们进入for循环传入i的地址后就去进行下一个for循环,创建的线程还没有从地址中获取打印i的值,主函数就继续创建后面的线程了,导致多个线程并发,拿到同一个i值,而且不是创建该线程的时候i的值。

注意到打印第一个运行结果都是打印0,这是因为主函数第一个for循环已经结束了,后面一个for循环将i又置为0,而这些线程在主函数第一个for循环执行的时候,都没有回获取i的值打印,直到下一个for循环,这些线程才获取i值打印,所以打印出来 都是0。

传值作为函数参数
#include
#include
#include
#include
#include
#include

void* pthread_fun(void* arg)
{
    int index =(int) arg;
    int i = 0;
    for(; i < 5; i++)
    {
        printf("index = %d\n",index);
        sleep(1);
    }
}

int main()
{
    pthread_t id[5];
    int i = 0;
    for(; i < 5; i++)
    {
        pthread_create(&id[i],NULL,pthread_fun,(void*)i);
    }

    for(i = 0; i < 5; i++)
    {
        pthread_join(id[i],NULL);
    }

    exit(0);
}


4 线程共享资源的验证

共享数据段
#include "../tou.h"

// 创建一个全局变量
int num = 100;
void *func(void *arg)
{
    printf("begin func\n");
    num++;
    printf("end func\n");
}
int main()
{
    pthread_t tid;
    bzero(&tid, sizeof(tid)); // todo 初始化 线程号
    // todo 创建线程
    int ret = pthread_create(&tid, NULL, func, NULL);
    pthread_join(tid, NULL);
    if (ret == 0)
    {
        printf("线程创建成功,num 为:%d\n ", num);
    }
}
共享堆空间
#include "../tou.h"

void *func(void *arg)
{
    int *pn = (int *)arg;
    printf("begin func\n");
    (*pn)++;
    printf("end func\n");
}
int main()
{
    // 创建堆空间
    int *p = NULL;
    p = malloc(sizeof(int));
    if (NULL == p)
    {
        printf("分配失败");
        exit(1);
    }
    // 对堆空间进行初始化
    memset(p, 0, sizeof(int));
    // 放入数据到该堆空间
    *p = 828;
    pthread_t tid;
    bzero(&tid, sizeof(tid)); // todo 初始化 线程号
    // todo 创建线程
    int ret = pthread_create(&tid, NULL, func, (void *)p);
    pthread_join(tid, NULL);
    if (ret == 0)
    {
        printf("线程创建成功,*p 为:%d\n ", *p);
    }
}

5 线程资源回收

pthread_join函数:


#include 
int pthread_join(pthread_t thread, void **retval);
功能:
    等待线程结束(此函数会阻塞),并回收线程资源,类似进程的 wait() 函数。如果线程已经结束,那么该函数会立即返回。
参数:
    thread:被等待的线程号。
    retval:用来存储线程退出状态的指针的地址。
返回值:
    成功:0
    失败:非 0

pthread_join得到的终止状态是不同的,总结如下:

  • 1) 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值
  • 2)如果thread线程被别的线程调用pthread_cance异常终止掉, reuva所指向的单元里存放的是常数PTHREAD_CANCELED
  • 3)如果thread线程是自己调用pthread_exit终止的, reuvaI所指向的单元存放的是传给pthread_exit的参数。

参考代码


#include "../tou.h"

void *func(void *arg)
{
    int ret = 999;
    pthread_exit((void *)(long)ret);
    return NULL;
}

void *func2(void *arg)
{

    int ret = 888;
    return ((void *)(long)ret);
}

int main()
{
    pthread_t tid;

    bzero(&tid, sizeof(tid)); // todo 初始化 线程号
    // todo 创建线程
    int ret = pthread_create(&tid, NULL, func, NULL);

    // 定义一个变量来存储线程退出状态的指针的地址
    void *p = NULL;

    // todo 演示 pthread_exit()
    pthread_join(tid, &p); // 这里是void **类型  // 参见  https://www.coder.work/article/1566260
    if (ret == 0)
    {
        printf("线程创建成功");

        printf("*p :%d", (int)(long)p);
    }

    printf("\n\n===================================\n");
    // todo 演示直接返回

    ret = pthread_create(&tid, NULL, func2, NULL);
    pthread_join(tid, &p); // 这里是void **类型  // 参见  https://www.coder.work/article/1566260
    if (ret == 0)
    {
        printf("线程创建成功");

        printf("*p :%d\n", (int)(long)p);
    }
}

pthread_cancel 后面线程取消再演示

6 线程分离

一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。

不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误。也就是说,如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。

pthread_detach函数:

#include 
int pthread_detach(pthread_t thread);
功能:
    使调用线程与当前进程分离,分离后不代表此线程不依赖与当前进程,线程分离的目的是将线程资源的回收工作交由系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。所以,此函数不会阻塞。
参数:
    thread:线程号。
返回值:
    成功:0
    失败:非0

参考代码

#include "../tou.h"

void *func(void *arg)
{
    sleep(5);
    return NULL;
}
int main()
{
    pthread_t tid;
    bzero(&tid, sizeof(tid)); // todo 初始化 线程号
    // todo 创建线程
    int ret = pthread_create(&tid, NULL, func, NULL);
    if (ret == 0)
    {
        printf("线程创建成功\n");
    }
#if 0 
    // 此分支的代码会阻塞5s,再结束主线程
    ret = pthread_join(tid, NULL);
    if (ret == 0)
    {
        printf("阻塞成功\n");
    }

#endif
#if 1
    // 此分支,主线程会立即结束
    ret = pthread_detach(tid);
    if (ret == 0)
    {
        printf("分离成功\n");
    }

#endif
    printf("main end...\n\n");
}

7 线程退出

在进程中我们可以调用exit函数或_exit函数来结束进程,在一个线程中我们可以通过以下三种在不终止整个进程的情况下停止它的控制流。

  • 线程从执行函数中返回。
  • 线程调用pthread_exit退出线程。
  • 线程可以被同一进程中的其它线程取消

pthread_exit函数

pthread_exit函数:

#include 

void pthread_exit(void *retval);
功能:
    退出调用线程。一个进程中的多个线程是共享该进程的数据段,因此,通常线程退出后所占用的资源并不会释放。
参数:
    retval:存储线程退出状态的指针。
返回值:无  

参考代码

#include "../tou.h"

// void *func(void *arg)//todo 使用exit(0)
// {
//     printf("begin func\n");
//     exit(0); //线程和主进程进程直接退出
//     printf("end func\n");
// }

void *func(void *arg) // todo 使用 phread_exit(void*retval)
{
    printf("begin func\n");
    pthread_exit(NULL);
    printf("end func\n");
}

void *func(void *arg) // todo 使用 return NULL
{
    printf("begin func\n");
    return NULL;  //等同于使用pthread_exit()
    printf("end func\n");
}

int main()
{
    pthread_t tid;
    bzero(&tid, sizeof(tid)); // todo 初始化 线程号
    // todo 创建线程
    int ret = pthread_create(&tid, NULL, func, NULL);
    if (ret == 0)
    {
        printf("线程创建成功\n");
    }
    pthread_join(tid, NULL);
    printf("主线程开始睡眠\n");
    sleep(5);
    printf("主线程结束睡眠\n");
}

8 线程取消

pthread_calcel函数


#include 

int pthread_cancel(pthread_t thread);
功能:
    杀死(取消)线程
参数:
    thread : 目标线程ID。
返回值:
    成功:0
    失败:出错编号

注意:线程的取消并不是实时的,而又一定的延时。需要等待线程到达某个取消点(检查点)。

类似于玩游戏存档,必须到达指定的场所(存档点,如:客栈、仓库、城里等)才能存储进度。

杀死线程也不是立刻就能完成,必须要到达取消点。

取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write… 执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表。

可粗略认为一个系统调用(进入内核)即为一个取消点。

参考代码

线程取消,线程可以被同一进程中的其它线程取消

#include "../tou.h"
void *func(void *arg) // todo 使用 return NULL
{
    printf("begin func\n");

    while (1)
    {
        sleep(1);
        printf("I am func \n");
    }
    printf("end func\n");
}
void *func2(void *arg) // todo 使用 return NULL
{
    pthread_t tid = (pthread_t)arg;
    printf("begin func2\n");
    sleep(5); // 先让子线程活5s,在取消它
    int ret = pthread_cancel(tid);
    if (ret == 0)
    {
        printf("线程取消成功\n");
    }
    printf("end func2\n");
}
int main()
{
    pthread_t tid;
    bzero(&tid, sizeof(tid)); // todo 初始化 线程号
    // todo 创建线程
    int ret = pthread_create(&tid, NULL, func, NULL);
    if (ret == 0)
    {
        printf("func线程创建成功\n");
    }

    pthread_t tid2;
    ret = pthread_create(&tid2, NULL, func2, (void *)tid);
    if (ret == 0)
    {
        printf("func2线程创建成功\n");
    }
    ret = pthread_join(tid2, NULL);
    if (ret != 0)
    {
        printf("等待func2线程失败\n");
        return 0;
    }
    void *mess = NULL; // 获取线程被强制取消之后的状态

    // 获取已终止线程的返回值
    ret = pthread_join(tid, &mess);
    if (ret != 0)
    {
        printf("等待func线程失败\n");
        return 0;
    }
    // 如果线程被强制终止,其返回值为 PTHREAD_CANCELED
    if (mess == PTHREAD_CANCELED)
    {
        printf("func 线程被强制终止\n");
    }
    else
    {
        printf("func error\n");
    }
}

无效的线程取消

参考代码
#include 
#include 
#include 
#include    //调用 sleep() 函数
void * thread_Fun(void * arg) {
    printf("新建线程开始执行\n");
    //插入无限循环的代码,测试 pthread_cancel()函数的有效性
    while(1);
}
int main()
{
    pthread_t myThread;
    void * mess;
    int value;
    int res;
    res = pthread_create(&myThread, NULL, thread_Fun, NULL);
    if (res != 0) {
        printf("线程创建失败\n");
        return 0;
    }
    sleep(1);
    //令 myThread 线程终止执行
    res = pthread_cancel(myThread);
    if (res != 0) {
        printf("终止 myThread 线程失败\n");
        return 0;
    }
    printf("等待 myThread 线程执行结束:\n");
    res = pthread_join(myThread, &mess);
    if (res != 0) {
        printf("等待线程失败\n");
        return 0;
    }
    if (mess == PTHREAD_CANCELED) {
        printf("myThread 线程被强制终止\n");
    }
    else {
        printf("error\n");
    }
    return 0;
}

https://raw.githubusercontent.com/xkyvvv/blogpic/main/pic1/image-20210711094517042.png

程序中,主线程( main() 函数)试图调用 pthread_cancel() 函数终止 myThread 线程执行。从运行结果不难发现,pthread_cancel() 函数成功发送了 Cancel 信号,但目标线程仍在执行。

也就是说,接收到 Cancel 信号的目标线程并没有立即处理该信号,或者说目标线程根本没有理会此信号。解决类似的问题,我们就需要搞清楚目标线程对 Cancel 信号的处理机制。

根据上节的内容,pthread_join会阻塞调用它的线程,因此程序在执行完printf(“等待 myThread 线程执行结束:\n”);后就会一直阻塞在res = pthread_join(myThread, &mess);等待目标线程myThread执行完毕

线程对Cancel信号的处理

对于默认属性的线程,当有线程借助 pthread_cancel() 函数向它发送 Cancel 信号时,它并不会立即结束执行,而是选择在一个适当的时机结束执行。

所谓适当的时机,POSIX 标准中规定,当线程执行一些特殊的函数时,会响应 Cancel 信号并终止执行,比如常见的 pthread_join()、pthread_testcancel()、sleep()、system() 等,POSIX 标准称此类函数为“cancellation points”(中文可译为“取消点”)。

POSIX 标准中明确列举了所有可以作为取消点的函数,这里不再一一罗列,感兴趣的读者可以自行查阅 POSIX 标准手册。

此外, 头文件还提供有 pthread_setcancelstate() pthread_setcanceltype() 这两个函数,我们可以手动修改目标线程处理 Cancel 信号的方式。

linux 系统编程之线程_第7张图片

linux 系统编程之线程_第8张图片

1)线程取消函数:pthread_cancel()
2)设置线程取消响应—>是否响应取消信号
3)设置响应取消信号的类型---->立即响应、延时响应
代码段:

参考代码(修复无效的线程取消)
#include 
#include 
#include 
#include 
#include 
#include 
struct Data
{
    char pthread_name[10];
};

void *Pthread_Task(void *arg)
{
    //设置线程取消状态---接受取消请求
    int pthread_setcancelstate_ret = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
    if (pthread_setcancelstate_ret != 0)
    {
        perror("pthread_setcancelstate");
        exit(-1);
    }
    while (1)
    {
        struct Data *p = (struct Data *)arg;
        printf("%s\n", p->pthread_name);
        sleep(1);
    }
    pthread_exit(NULL);
}

int main()
{
    pthread_t pid;
    struct Data d1;
    memset(&d1, 0, sizeof(d1));
    strcpy(d1.pthread_name, "hello");
    //创建线程
    int ret = pthread_create(&pid, NULL, Pthread_Task, (void *)&d1);
    if (ret != 0)
    {
        perror("pthread_create");
        exit(-1);
    }
    printf("5s之后发送取消请求\n");
    sleep(5);
    //取消线程
    pthread_cancel(pid);
    pause();
    return 0;
}

9 线程清理

有时候我们希望线程退出时能够自动的执行某些函数,为了能达到此目的,OS 提供了两个函数帮我们完成这个功能:

void pthread_cleanup_push(void (*rtn)(void*), void *arg);
void pthread_cleanup_pop(int execute);

linux 系统编程之线程_第9张图片

使用方法

如果想要你的线程在退出时能够执行清理函数,你需要使用 pthread_cleanup_push 对你的清理函数进行注册,如下:

void clean(void *arg) {
    // ...
}

void *th_fn(void *arg) {
    // push 和 pop 必须对对出现
    pthread_cleanup_push(clean, /* 清理函数 clean 的参数*/);
    // ...
    pthread_cleanup_pop(1);
}

在 Linux 中,pthread_cleanup_push 和 pthread_cleanup_pop 这两个函数是通过宏来做的,pthread_cleanup_push 被替换成以左花括号 { 为开头的一段代码,而 pthread_cleanup_pop 被替换成以右花括号 } 结尾的一段代码,这就意味着这两个函数必须要成对出现才能将左右花括号匹配上,否则就出现编译错误。
有些平台可能不是使用宏来实现,就算不成对也没什么关系。

线程清理函数调用

有三种情况线程清理函数会被调用:
1、线程还未执行 pthread_cleanup_pop 前,被 pthread_cancel 取消
2、线程还未执行 pthread_cleanup_pop 前,主动执行 pthread_exit 终止
3、线程执行 pthread_cleanup_pop,且 pthread_cleanup_pop 的参数不为 0

注意:如果线程还未执行 pthread_cleanup_pop 前通过 return 返回,是不会执行清理函数的。

线程清理例程

程序 clean 需要传入两个参数,第 1 个参数表示是否提前返回(在执行 pthread_cleanup_pop 前返回),第 2 个参数表示 pthread_cleanup_pop 的参数。所以有 4 种组合情况。

#include 
#include 
#include 
#include 

int excute;

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

void* th_fn1(void* arg) {
    puts("thread 1 starting");
    pthread_cleanup_push(cleanup, "线程 1 清理者 1 号");
    pthread_cleanup_push(cleanup, "线程 1 清理者 2 号");
    
    if (arg) {
        printf("线程 1 提前退出\n");
        return (void*)1;
    }
    
    pthread_cleanup_pop(excute);
    pthread_cleanup_pop(excute);
    printf("线程 1 正常退出\n");
    return (void*)10;
}

void* th_fn2(void* arg) {
    puts("thread 2 starting");
    pthread_cleanup_push(cleanup, "线程 2 清理者 1 号");
    pthread_cleanup_push(cleanup, "线程 2 清理者 2 号");
    
    if (arg) {
        printf("线程 2 提前退出\n");
        pthread_exit((void*)2);
    }
    
    pthread_cleanup_pop(excute);
    pthread_cleanup_pop(excute);
    
    printf("线程 2 正常退出\n");
    pthread_exit((void*)20);
}

int main(int argc, char* argv[]) {
    
    if (argc < 3) {
        printf("Usage: %s  \n", argv[0]);
        return -1;
    }
    
    pthread_t tid1, tid2;
    int err;
    void* ret;
    void *arg = NULL;
    excute = 0;
    
    arg = (void*)atoi(argv[1]);
    excute = atoi(argv[2]);
    
    err = pthread_create(&tid1, NULL, th_fn1, arg);
    err = pthread_create(&tid2, NULL, th_fn2, arg);
    
    err = pthread_join(tid1, &ret);
    printf("thread 1 exit code %d\n", (int)ret);
    
    err = pthread_join(tid2, &ret);
    printf("thread 2 exit code %d\n", (int)ret);
    
    return 0;
}

linux 系统编程之线程_第10张图片

结果可以看到:

当 clean 程序中的线程正常返回时,只有 pthread_cleanup_pop 的参数非 0 时,才会正常执行清理函数。

当 clean 程序中的线程在执行 pthread_cleanup_pop 前时,使用 pthread_exit 退出时,清理函数才会被执行,和pthread_cleanup_pop 的参数没有关系。而使用 return 返回的线程 1 并不会执行清理函数。清理函数的执行顺序,是按照注册时候相反的顺序执行的。

注意,在有些系统中(如Mac OS X),提前终止可能会出现段错误。

8 线程属性

Linux下线程的属性是可以根据实际项目需要,进行设置,之前我们讨论的线程都是采用线程的默认属性,默认属性已经可以解决绝大多数开发时遇到的问题。

如我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数。

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

主要结构体成员:

\1) 线程分离状态

\2) 线程栈大小(默认平均分配)

\3) 线程栈警戒缓冲区大小(位于栈末尾)

\4) 线程栈最低地址

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

8.2 线程属性初始化和销毁

#include int pthread_attr_init(pthread_attr_t *attr);
功能:
    初始化线程属性函数,注意:应先初始化线程属性,再pthread_create创建线程
参数:
    attr:线程属性结构体
返回值:
    成功:0
    失败:错误号
​
int pthread_attr_destroy(pthread_attr_t *attr);
功能:
    销毁线程属性所占用的资源函数
参数:
    attr:线程属性结构体
返回值:
    成功:0
    失败:错误号

8.3 线程分离状态

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

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

相关函数:


#include int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
功能:设置线程分离状态
参数:
    attr:已初始化的线程属性
    detachstate:    分离状态
        PTHREAD_CREATE_DETACHED(分离线程)
        PTHREAD_CREATE_JOINABLE(非分离线程)
返回值:
    成功:0
    失败:非0int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
功能:获取线程分离状态
参数:
    attr:已初始化的线程属性
    detachstate:    分离状态
        PTHREAD_CREATE_DETACHED(分离线程)
        PTHREAD _CREATE_JOINABLE(非分离线程)
返回值:
    成功:0
    失败:非0

这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。

要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timedwait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。

设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。

8.4 线程栈地址

POSIX.1定义了两个常量来检测系统是否支持栈属性:

  • _POSIX_THREAD_ATTR_STACKADDR
  • _POSIX_THREAD_ATTR_STACKSIZE

也可以给sysconf函数传递来进行检测:

  • _SC_THREAD_ATTR_STACKADDR
  • _SC_THREAD_ATTR_STACKSIZE

当进程栈地址空间不够用时,指定新建线程使用由malloc分配的空间作为自己的栈空间。通过pthread_attr_setstack和pthread_attr_getstack两个函数分别设置和获取线程的栈地址。

#include int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr,  size_t stacksize);
功能:设置线程的栈地址
参数:
    attr:指向一个线程属性的指针
    stackaddr:内存首地址
    stacksize:返回线程的堆栈大小
返回值:
    成功:0
    失败:错误号
​
int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr,  size_t *stacksize);
功能:获取线程的栈地址
参数:
    attr:指向一个线程属性的指针
    stackaddr:返回获取的栈地址
    stacksize:返回获取的栈大小
返回值:
    成功:0
    失败:错误号
​

8.5 线程栈大小

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

#include int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
功能:设置线程的栈大小
参数:
    attr:指向一个线程属性的指针
    stacksize:线程的堆栈大小
返回值:
    成功:0
    失败:错误号
​
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
功能:获取线程的栈大小
参数: 
    attr:指向一个线程属性的指针
    stacksize:返回线程的堆栈大小
返回值:
    成功:0
    失败:错误号
​

8.6 综合参考程序

#define SIZE 0x100000

void *th_fun(void *arg)
{
    while (1)
    {
        sleep(1);
    }
}

int main()
{
    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); //获取线程的栈地址
    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 unknown\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;
}

8.7 线程使用注意事项

\1) 主线程退出其他线程不退出,主线程应调用pthread_exit

\2) 避免僵尸线程

a) pthread_join

b) pthread_detach

c) pthread_create指定分离属性

被join线程可能在join函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;

\3) malloc和mmap申请的内存可以被其他线程释放

\4) 应避免在多线程模型中调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程t在子进程中均pthread_exit

\5) 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制

补充 : pthread_join的第二个参数

在看pthread相关,遇到了pthread_join函数

#include 
int pthread_join(pthread_t pthread_id, void** retval);

这个函数的第二个参数为什么是void**?有啥用?

首先,这个函数的用途是什么? manpage给出的解释:

The pthread_join() function waits for the thread specified by thread
       to terminate.  If that thread has already terminated, then
       pthread_join() returns immediately.  The thread specified by thread
       must be joinable.

       If retval is not NULL, then pthread_join() copies the exit status of
       the target thread (i.e., the value that the target thread supplied to
       pthread_exit(3)) into the location pointed to by retval.  If the
       target thread was canceled, then PTHREAD_CANCELED is placed in the
       location pointed to by retval.

       If multiple threads simultaneously try to join with the same thread,
       the results are undefined.  If the thread calling pthread_join() is
       canceled, then the target thread will remain joinable (i.e., it will
       not be detached).

意图很明显,以阻塞的方式等待指定线程(可joinable)结束。成功返回0,失败返回错误号。

一个线程的结束,有两种方式,一种是正常结束。一种是使用pthread_exit。对于使用pthread_exit结束的线程,可以返回一个"status"给主线程。

void pthread_exit(void* retval);

那么它的参数就和pthread_join第二个参数就对应上了。看一下怎么用!

//错误演示
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 

struct my_threadfunc_error
{
    int a;
    int b;
};

void* StartFunction(void* arg)
{
    my_threadfunc_error var{1,2};
    pthread_exit((void*)&var);
    return NULL;
}

int main(int argc, char* argv[])
{
    pthread_t my_pthread_t = 0;
    if (0 != pthread_create(&my_pthread_t, NULL, StartFunction, NULL))
    {
        printf("pthread_create error!\n");
        return -1;
    }
    void** p = NULL;
    if (0 != pthread_join(my_pthread_t, p))
    {
        printf("pthread_join error!\n");
        return -1;
    }
    my_threadfunc_error* var = (my_threadfunc_error*)(*p);//11
    printf("var.a = %d, var.b = %d\n", var->a, var->b);
    return 0;
}

上面代码在线程函数中返回的是一个栈上的变量的地址,所在在代码运行到my_threadfunc_error* var = (my_threadfunc_error*)(*p);//11的时候,就出现段错误了。很好理解,因为线程函数结束,线程的栈也会被回收,var的空间也被清理了,所以使用未知的内存发生段错误正常。

既然如此,那么使用malloc或者全局变量即可处理这种问题!

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 

struct my_threadfunc_error
{
    int a;
    int b;
};

void* StartFunction(void* arg)
{
    my_threadfunc_error* var = (my_threadfunc_error*)malloc(sizeof(my_threadfunc_error));
    var->a = 100;
    var->b = 200;
    pthread_exit((void*)var);
    return NULL;
}

int main(int argc, char* argv[])
{
    pthread_t my_pthread_t = 0;
    if (0 != pthread_create(&my_pthread_t, NULL, StartFunction, NULL))
    {
        printf("pthread_create error!\n");
        return -1;
    }
    void* p = NULL;
    if (0 != pthread_join(my_pthread_t, &p))
    {
        printf("pthread_join error!\n");
        return -1;
    }
    my_threadfunc_error* var = (my_threadfunc_error*)(p);
    printf("var.a = %d, var.b = %d\n", var->a, var->b);
    free(var);
    return 0;
}

{
printf(“pthread_create error!\n”);
return -1;
}
void** p = NULL;
if (0 != pthread_join(my_pthread_t, p))
{
printf(“pthread_join error!\n”);
return -1;
}
my_threadfunc_error* var = (my_threadfunc_error*)(*p);//11
printf(“var.a = %d, var.b = %d\n”, var->a, var->b);
return 0;
}


上面代码在线程函数中返回的是一个栈上的变量的地址,所在在代码运行到my_threadfunc_error* var = (my_threadfunc_error*)(*p);//11的时候,就出现段错误了。很好理解,因为线程函数结束,线程的栈也会被回收,var的空间也被清理了,所以使用未知的内存发生段错误正常。

既然如此,那么使用malloc或者全局变量即可处理这种问题!

```c++
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 

struct my_threadfunc_error
{
    int a;
    int b;
};

void* StartFunction(void* arg)
{
    my_threadfunc_error* var = (my_threadfunc_error*)malloc(sizeof(my_threadfunc_error));
    var->a = 100;
    var->b = 200;
    pthread_exit((void*)var);
    return NULL;
}

int main(int argc, char* argv[])
{
    pthread_t my_pthread_t = 0;
    if (0 != pthread_create(&my_pthread_t, NULL, StartFunction, NULL))
    {
        printf("pthread_create error!\n");
        return -1;
    }
    void* p = NULL;
    if (0 != pthread_join(my_pthread_t, &p))
    {
        printf("pthread_join error!\n");
        return -1;
    }
    my_threadfunc_error* var = (my_threadfunc_error*)(p);
    printf("var.a = %d, var.b = %d\n", var->a, var->b);
    free(var);
    return 0;
}

你可能感兴趣的:(linux,linux,运维,服务器)