C/C++ Linux系统编程:线程控制详解,从线程创建到线程终止

📚引言

本文深入探讨 Linux 系统编程中的基本线程控制,从线程的基本概念入手,对比线程与进程的关系,系统性地解析pthread_createpthread_joinpthread_exitpthread_cancel等核心线程API的使用方法、参数含义及注意事项。

一. 什么是线程(Thread)?

基本概念

线程 (Thread)是操作系统能够进行运算调度的最小单位,与进程相比更加轻量。它被包含在进程 之中,是进程中的实际运作单位。一个进程可以包含多个线程,这些线程共享 进程的地址空间、文件描述符、信号处理等内存空间,但各自拥有独立的栈空间寄存器状态。

与进程比较

特性 进程 线程
资源拥有 是系统资源分配的基本单位 与同进程的其他线程共享资源
调度开销 上下文切换开销大 上下文切换开销小
通信方式 需要IPC机制(管道、消息队列等) 可直接通过共享变量通信
创建开销 大(需要复制父进程资源) 小(共享进程资源)
独立性 地址空间相互独立 共享同一地址空间
稳定性 一个进程崩溃不影响其他进程 一个线程崩溃会导致整个进程终止

上述介绍:同一进程的多线程共享大部分,除了每个线程独立的栈空间。这代表线程的创建、销毁、切换要比进程所消耗的资源都要小得多,所以多线程比多进程更容易实现高并发。

二. 线程的创建:pthread_create

函数原型
objectivec 复制代码
#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);
参数:
  • thread:指向线程标识符的指针,用于返回新线程的ID

  • attr:线程属性参数,通常设置为NULL表示使用默认属性

  • start_routine :线程函数的函数指针,格式为void* func(void* arg)

  • arg:传递给线程函数的参数

每个线程都有一个唯一的标识符,即线程ID,这个标识符是通过 pthread_t 类型的变量来表示的,当 pthread_create 成功创建一个线程时,它会将新线程的标识符存储在 thread 参数指向的位置。

pthread_t 定义在头文件 <pthreadtypes.h>中,实际上是 long类型(longlong int是相同类型的不同写法)的别名。

objectivec 复制代码
#include <pthread.h>

typedef unsigned long int pthread_t;

该函数使用事项:新线程所执行的函数声明是 void* (*start_routine)(void* arg) 其入参和返回值都是 void*指针。我们可以通过将传递给线程的参数包装成一个结构体 struct,这样复制到void *arg中, 再在函数内部处理;同理我们可以在线程函数内部将要返回的状态码和返回值包装为结构体,并将指针作为返回值return。

Demo示例
objectivec 复制代码
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define BUF_LEN 1024

char *buf; // 缓存字符串

// 读线程所需要执行的代码逻辑
void *input_thread(void * argv)
{
    int i = 0;
    while (1)
    {
        // 单个字节读取字符
        char c = fgetc(stdin);
        if (c && c != '\n')
        {
            buf[i++] = c;
        }
        // 缓冲区索引溢出
        if (i >= BUF_LEN)
        {
            i = 0;
        }
    } 
}
// 写线程所需要的代码逻辑
void *output_thread(void * argv)
{
    int i = 0;
    while (1)
    {
        if (buf[i])
        {
            // 读取一个字节写出控制台,之后换行
            fputc(buf[i],stdout);
            fputc('\n',stdout);
            buf[i++] = 0;
            // 读取到缓冲区索引溢出
             if (i >= BUF_LEN)
            {
                i = 0;
            }
        }else{
            sleep(1);
        }
    } 
}
// 程序实现创建两个线程
// (1) 读取控制台信息 写入到缓存中
// (2) 将缓存信息写到控制台
int main(int argc, char const *argv[])
{
    // 分配缓存
    buf = malloc(BUF_LEN);
    for (int i = 0; i < BUF_LEN; i++)
    {
        buf[i] = 0;
    }
    
    // 声明线程id
    pthread_t pid_input;
    pthread_t pid_output;
    // 创建线程

    // 创建读线程
    pthread_create(&pid_input,NULL,input_thread,NULL);
    // 创建写线程
    pthread_create(&pid_input,NULL,output_thread,NULL);

    // 主线程等待读写线程结束
    pthread_join(pid_input,NULL);
    pthread_join(pid_output,NULL);

    free(buf);
    return 0;
}

三. 线程的终止与回收

对于线程的终止,有如下几种方式:

  • 线程函数自己执行 return 语句结束线程;

  • 线程函数内部调用 pthread_exit****函数;

  • 其他线程调用 pthread_cancel 函数对该函数进行终止

3.1 pthread_exit:主动退出线程

函数原型
objectivec 复制代码
#include <pthread.h>

void pthread_exit(void *retval);

功能:结束关闭调用该方法的线程,并返回一个内存指针存放结果

参数void* retval 是线程结束返回给其他函数的结果与数据,如果其他函数调用pthread_join获得该结果

3.2 pthread_join:等待线程结束并回收资源

函数原型
objectivec 复制代码
#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);

功能: 等待指定线程结束,获取目标线程的返回值,并在目标线程结束后回收它的资源

参数: pthread_t thread: 指定线程ID,void** retval: 这是一个可选参数,用于接收线程结束后传递的返回值。如果非空,pthread_join 会在成功时将线程的 exit* status 复制到 * retval 所指向的内存位置。如果线程没有显式地通过 phtread_exit提供返回值,则该参数将被设为 NULL 或忽略。

**返回值:**成功返回0,失败返回1

Demo示例(pthread_exit 与 pthread_join)
objectivec 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <math.h>
#include <string.h>

typedef struct Result
{
    char *p; // 接收返回结果
    int len;
} Result;

// 线程1所执行的代码函数
void *red_thread(void * arg)
{
    // 初始化Result结构体
    Result *result = malloc(sizeof(Result));
    // 解析传递的参数
    char code = *((char *) arg);
    // 声明存放读取消息的字符串
    char *ans = malloc(101);
    while (1)
    {
        fgets(ans,100,stdin);
        if (ans[0] == code)
        {
            // 接收到回复的消息
            free(ans);
            printf("红玫瑰离开了\n");
            char *redans = strdup("红玫瑰去了纽约\n");
            result->p = redans;
            result->len = strlen(redans);
            pthread_exit((void*) result);
        }else{
            printf("红玫瑰还在等待\n");
        }
    }
}

// 线程2所要执行的代码函数
void *white_thread(void * arg)
{
    // 初始化Result结构体
    Result *result = malloc(sizeof(Result));
    // 解析传递的参数
    char code = *((char *) arg);
    // 声明存放读取消息的字符串
    char *ans = malloc(101);
    while (1)
    {
        fgets(ans,100,stdin);
        if (ans[0] == code)
        {
            // 接收到回复的消息
            free(ans);
            printf("白玫瑰离开了\n");
            char *redans = strdup("白玫瑰去了伦敦\n");
            result->p = redans;
            result->len = strlen(redans);
            pthread_exit((void*) result);
        }else{
            printf("白玫瑰还在等待\n");
        }
    }
}

int main(int argc, char const *argv[])
{
    pthread_t pid_red;
    pthread_t pid_white;
    char red_code = 'r';
    char white_code = 'w';

    // 创建接收线程的结构体
    Result *red_result = NULL;
    Result *white_result = NULL;
    // 创建线程1
    pthread_create(&pid_red,NULL,red_thread,&red_code);
    pthread_create(&pid_white,NULL,white_thread,&white_code);

    // 获取红玫瑰的线程结果
    pthread_join(pid_red,(void **)&red_result);
    printf("红玫瑰的故事结局:%s",red_result->p);
    // 释放内存
    free(red_result->p);
    free(red_result);
    // 获取白玫瑰的线程结果
    pthread_join(pid_white,(void **)&white_result);
    printf("白玫瑰的故事结局:%s",white_result->p);
     // 释放内存
    free(white_result->p);
    free(white_result);

    return 0;
}

这里我使用了**尚硅谷Linux 应用层开发教程课程部分的例子,对于这个例子的使用个人认为十分恰当,**理解了这个例子就可以理解线程返回值概念,之前我们也说过,线程函数的参数以及返回值都可以封装成一个结构体进行线程间的交流,这里得到很好的印证

3.3 pthread_detach:让线程"自生自灭"

函数原型
objectivec 复制代码
#include <pthread.h>

int pthread_detach(pthread_t thread);

功能: 将线程标记为detached状态。

**返回值:**成功返回0,失败返回错误码。

注意!POSIX线程终止后,如果没有调用 pthread_detachpthread_join ,其资源会继续占用内存,类似于僵尸进程的未回收状态。默认情况下创建线程后,它处于可 join 状态,此时可以调用 pthread_join 等待线程终止并回收资源。但是如果主线程不需要等待线程终止,可以将其标记为**detached 状态,这意味着线程终止后,其资源会自动被系统回收。**

Demo示例
objectivec 复制代码
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void *task(void *arg)
{
    printf("Thread started\n");
    sleep(2); // 模拟线程工作
    printf("Thread finished\n");
    return NULL;
}

int main()
{
    pthread_t tid;

    // 创建线程
    pthread_create(&tid, NULL, task, NULL);

    // 使用 pthread_detach 让线程自动回收资源
    pthread_detach(tid);

    // 主线程继续工作
    printf("Main thread continues\n");
    sleep(3); // 需要注意的是,pthread_detach不会等待子线程结束,如果在后者执行完毕之前主线程退出,则整个进程退出,子线程被强制终止,因此需要等待足够的时间确保子线程完成自己的任务
    printf("Main thread ending\n");

    return 0;
}

✅ 一句话总结

pthread_detach 是给那些"不需要等待、不需要结果"的线程使用的,让它执行完后自动"消失",避免僵尸线程。

四. 线程取消机制

有时我们需要在运行时强制终止某个线程,POSIX 提供了线程取消机制。

4.1 pthread_cancel:请求取消线程

函数原型
objectivec 复制代码
#include <pthread.h>

int pthread_cancel(pthread_t thread);

功能: 向目标线程发送取消请求。目标线程是否和何时响应取决于它的取消状态和类型。

参数:即目标线程的线程ID,这里的目标线程是需要被取消的线程。

返回值: 成功返回0,失败返回非零的错误码

取消状态(Cancelability State):

可以是 enabled(默认)或 disabled。如果取消状态为禁用,则取消请求会被挂起,直至线程启用取消功能。如果取消状态为启用,则线程的取消类型决定它何时取消。

取消类型(Cancelability Type):

可以是 asynchronous(异步)或deferred(被推迟,默认值)。

  • asynchronous(异步):意味着线程可能在任何时候被取消(通常立即被取消,但系统并不保证这一点)

  • **deferred(延迟):**被推迟意味着取消请求会被挂起,直至被取消的线程执行取消点(cancellation point)函数时才会真正执行线程的取消操作。

需要注意的是:取消操作和**pthread_cancel** **函数的调用是异步的,**这个函数的返回值只能告诉调用者取消请求是否成功发送。当线程被成功取消后,通过 pthread_join 和线程关联将会获得PTHREAD_CANCELED作为返回信息,这是判断取消是否完成的唯一方式。

4.2 pthread_testcancel:手动插入取消点

函数原型
objectivec 复制代码
#include <pthread.h>

void pthread_testcancel(void);

说明: 取消点线程获取取消请求自动终止的地方,这里 pthread_testcancel插入取消点是在 POSIX线程库中专门设计用于检查和处理取消请求的函数。当被取消的线程执行这些函数时,如果线程的取消状态是 enable 且类型是 deferred,则它会立即响应取消请求并终止执行。

4.3 pthread_setcancelstate:设置取消状态

前面在 pthread_cancel 函数中介绍过取消状态、取消类型,这个函数就是设置线程函数的取消状态而用。

函数原型
objectivec 复制代码
#include <pthread.h>

int pthread_setcancelstate(int state, int *oldstate);

参数:

  • state
    • PTHREAD_CANCEL_ENABLE:允许取消
    • PTHREAD_CANCEL_DISABLE:忽略取消请求
  • oldstate:保存之前的取消状态(可为 NULL)

4.4 pthread_setcanceltype:设置取消类型

函数原型
objectivec 复制代码
#include <pthread.h>

int pthread_setcanceltype(int type, int *oldtype);

参数:

  • type
    • PTHREAD_CANCEL_DEFERRED(默认):延迟取消,只在取消点响应
    • PTHREAD_CANCEL_ASYNCHRONOUS:异步取消,可随时终止(不安全,慎用)
  • oldtype:保存之前的类型

4.5 Demo示例

下面给出三种示例,分别从线程的不同取消类型、取消状态进行介绍

取消状态设置为允许、取消类型为延迟下:

objectivec 复制代码
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void *task(void *arg)
{
    printf("Thread started\n");

    // 默认取消类型为延迟,无需设置

    // 模拟工作
    printf("Working...\n");
    sleep(1);             // 模拟工作
    pthread_testcancel(); // 取消点函数
    printf("After Cancelled\n");

    return NULL;
}

int main()
{
    pthread_t tid;
    void *res;

    // 创建线程
    pthread_create(&tid, NULL, task, NULL);

    // 取消子线程
    if (pthread_cancel(tid) != 0){
        perror("pthread_cancel");
    }

    // 等待子线程终止并获取其退出状态
    pthread_join(tid, &res);

    // 检查子线程是否被取消
    if (res == PTHREAD_CANCELED)
    {
        printf("Thread was canceled\n");
    }
    else
    {
        printf("Thread was not canceled, exit code: %ld\n", (long)res);
    }

    return 0;
}

在线程函数中我们手动设置了一个取消点函数,那么主线程向子线程发送了取消请求,子进程的取消类型设置为 deferred 后就会从该取消点函数终止,那么就打印子线程函数中的 After Cancelled

可以看到After Cancelled并未被打印,且主线程执行了取消成功分支,子线程被成功取消。

取消状态设置允许与取消类型设置为异步下:

objectivec 复制代码
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void *task(void *arg)
{
    printf("Thread started\n");

    // 设置取消类型为异步
    pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);

    // 模拟工作
    printf("Working...\n");
    sleep(1);             // 模拟工作
    printf("After Cancelled\n");

    return NULL;
}

int main()
{
    pthread_t tid;
    void *res;

    // 创建线程
    pthread_create(&tid, NULL, task, NULL);

    // 取消子线程
    if (pthread_cancel(tid) != 0)
    {
        perror("pthread_cancel");
    }

    // 等待子线程终止并获取其退出状态
    pthread_join(tid, &res);

    // 检查子线程是否被取消
    if (res == PTHREAD_CANCELED)
    {
        printf("Thread was canceled\n");
    }
    else
    {
        printf("Thread was not canceled, exit code: %ld\n", (long)res);
    }

    return 0;
}

我们在子线程函数中使用了sleep函数模拟了线程工作流程。

多次运行该文件,我们看到线程异步结果,也就是我们不知道线程什么时候结束,有时候会打印那个After Cancelled,而有时候不会。

当我们设置线程的取消状态为 disabled ,即不能取消下:

objectivec 复制代码
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void *task(void *arg)
{
    printf("Thread started\n");

    // 禁用取消响应
    pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
    printf("Thread CancelState is disabled\n");

    // 设置取消类型为异步
    pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);

    // 模拟工作
    printf("Working...\n");
    sleep(1);             // 模拟工作
    printf("After Cancelled\n");

    return NULL;
}

int main()
{
    pthread_t tid;
    void *res;

    // 创建线程
    pthread_create(&tid, NULL, task, NULL);

    // 取消子线程
    if (pthread_cancel(tid) != 0)
    {
        perror("pthread_cancel");
    }

    // 等待子线程终止并获取其退出状态
    pthread_join(tid, &res);

    // 检查子线程是否被取消
    if (res == PTHREAD_CANCELED)
    {
        printf("Thread was canceled\n");
    }
    else
    {
        printf("Thread was not canceled, exit code: %ld\n", (long)res);
    }

    return 0;
}

可以看到我们没有成功通过 join 函数获取线程取消的结果。

结语

掌握线程的创建、控制与生命周期管理是 Linux 系统编程的基石。通过本文的梳理,相信你对 pthread 系列 API 有了更系统的理解。

💬 欢迎在评论区交流学习心得,指出文中不足!

📌 觉得有帮助?别忘了点赞 + 收藏 + 关注!

相关推荐
jiaway3 小时前
【C语言】第一课 环境配置
c语言·开发语言
云的牧人3 小时前
Ubuntu 22 redis集群搭建
linux·运维·ubuntu
yzx9910133 小时前
图像去雾:从暗通道先验到可学习融合——一份可跑的 PyTorch 教程
人工智能·pytorch·学习
siriuuus3 小时前
Linux 磁盘扩容及分区相关操作实践
linux·运维·服务器
Qiang_san3 小时前
GNU Make | C/C++项目自动构建入门
c语言·c++·gnu
To_再飞行3 小时前
K8s 存储配置资源
linux·云原生·容器·kubernetes
源代码•宸4 小时前
Leetcode—2749. 得到整数零需要执行的最少操作数【中等】(__builtin_popcountl)
c++·经验分享·算法·leetcode·位运算
小猪写代码4 小时前
Ubuntu 文件权限管理
linux·ubuntu
芒果敲代码4 小时前
单一职责原则(SRP)
c++·单一职责原则