线程

线程概念

线程实际上是应用层的概念,在Linux内核中,所有的调度实体都被称为任务(task),它们之间的区别是:有些任务自己拥有一套完整的资源,而有些任务彼此之间共享一套资源。

对比图
  • 左边是一个含有单个线程的进程,它拥有自己的一套完整的资源。
  • 右边是一个含有两条线程的进程,线程彼此间共享进程内的资源。

由此可见,线程是一种轻量级进程,提供一种高效的任务处理方式。

线程与进程的核心区别

对比维度 进程 线程
资源占有 拥有独立的资源空间(代码段、数据段、堆等) 共享所属进程的资源空间
调度单位 内核级调度实体(task) 应用层轻量级调度实体,依赖进程
切换开销 大(需切换资源空间) 小(仅切换执行上下文)
独立性 高(一个进程崩溃不影响其他进程) 低(一个线程崩溃可能导致整个进程崩溃)
通信方式 复杂(管道、消息队列等IPC机制) 简单(直接访问共享资源)

进程与线程的应用场景

进程的应用场景

适用于需要独立资源和隔离环境的场景,核心优势是稳定性和安全性:

  • 网络服务器、数据库系统:每个进程拥有独立地址空间和资源(文件描述符、内存等),进程间资源不共享,单个进程故障不会影响整体系统。
  • 需严格隔离的任务:通过进程间通信(IPC)机制实现数据交换,保证数据安全性。

线程的应用场景

适用于需要高效并发执行的场景,核心优势是资源共享和低开销:

  • 图形界面应用(GUI):主线程处理界面交互,子线程处理后台任务(如数据加载、计算),避免界面卡顿。
  • 多媒体处理、数据批量处理:线程共享进程内存,数据交换无需复杂IPC,提高执行效率。
  • 网络通信并发处理:单个服务进程通过多线程同时响应多个客户端请求,节省内存资源。

线程的内存资源

系统不会为线程分配新的内存空间,线程的所有资源均来自其所属的进程(共享进程的虚拟内存空间),包括代码段、数据段、堆、文件描述符等。线程仅拥有独立的栈空间(用于存储局部变量、函数调用上下文)。

示意图
  • 线程共享进程的代码段、数据段、堆、全局变量、文件描述符等资源。
  • 线程独占独立的栈空间,用于执行自身任务的局部数据存储。
  • 注意:线程结束后,需手动或自动回收其栈空间(避免内存泄漏)。

基本接口

线程的创建

创建一条POSIX线程需指定线程的执行函数,函数接口及细节如下:

#include <pthread.h>

// 返回值:成功返回0,失败返回非0错误码
int pthread_create(pthread_t *thread,
                   const pthread_attr_t *attr,
                   void *(*start_routine) (void *),
                   void *arg);

参数说明

  • thread:输出参数,存储新创建线程的TID(线程ID)
  • attr:线程属性,创建标准线程时设为NULL(使用默认属性)
  • start_routine:线程执行函数的地址(函数指针),格式固定为void* (*)(void*)
  • arg:传递给线程执行函数的参数,若无需参数设为NULL

线程参数传递

线程通过pthread_createarg参数传递数据,本质是传递内存地址,线程函数通过指针解析该地址的数据(共享进程内存,无需拷贝)。

示例代码

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 线程任务函数:接收并修改参数
void *task(void *arg) {
    printf("线程接收的参数地址:%p\n", arg);
    // 指针类型转换,访问地址数据
    int *p = (int *)arg;
    printf("线程接收的参数值:%d\n", *p);
    // 修改共享内存中的数据(主线程可见)
    *p = 10086;
}

int main() {
    int value = 10010; // 主线程中的变量(共享资源)
    printf("主线程中value的地址:%p\n", &value);

    // 创建线程,传递value的地址
    pthread_t tid;
    pthread_create(&tid, NULL, task, (void *)&value);

    sleep(1); // 确保线程先执行(避免主线程提前退出)
    printf("主线程中value修改后的值:%d\n", value); // 输出10086

    getchar();
    return 0;
}

线程的并发性

线程最重要的特性是并发执行:线程函数与主线程会同时运行,这是它与普通函数调用的根本区别。

核心注意点:竞态问题

由于线程共享进程资源,多个线程同时访问共享资源(如全局变量、堆内存等)时,会出现“竞态”(Race Condition),导致结果不可预期。

线程最重要的特性是并发,线程函数 doSomething() 会与主线程 main() 同时运行,这是它与普通函数调用的根本区别。需要特别提醒的是,由于线程函数的并发性,在线程中访问共享资源需要特别小心,因为这些共享资源会被多个线程争抢,形成“竞态”。最典型的共享资源是全局变量,比如以下代码:

示例代码(含竞态演示)

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>

// 标志位:false=资源可读(子线程操作),true=资源可写(主线程操作)
bool flag = true;

// 线程执行函数:判断共享变量的奇偶性
void *myTask(void *arg) {
    int *ptr = (int *)arg;
    while (1) {
        if (flag == false) {
            if (*ptr % 2) {
                printf("[%d]是一个奇数\n", *ptr);
            } else {
                printf("[%d]是一个偶数\n", *ptr);
            }
            flag = true; // 释放写权限
        }
    }
}

int main(int argc, char const *argv[]) {
    int Num = 0; // 共享资源(主线程修改,子线程读取)
    pthread_t tid;

    // 创建线程,传递共享变量地址
    pthread_create(&tid, NULL, myTask, (void *)&Num);

    srand(time(NULL)); // 设置随机种子
    while (1) {
        if (flag == true) {
            Num = rand() % 1000; // 修改共享资源
            flag = false; // 释放读权限
        }
    }
    return 0;
}

2.3 线程的退出

线程执行完毕后,通过以下接口退出,释放自身资源(不影响进程内其他线程):

#include <pthread.h>

void pthread_exit(void *retval);

参数说明

  • retval:线程的返回值,可被pthread_join获取;无需返回值时设为NULL

exit的区别

函数 作用范围
pthread_exit 仅退出当前线程
exit 退出整个进程(所有线程)

线程的取消

用于主动终止正在运行的线程,需配合取消状态、取消类型及清理函数使用,避免资源泄漏。

核心接口:发送取消请求

#include <pthread.h>

// 返回值:成功返回0,失败返回非0错误码
int pthread_cancel(pthread_t thread);
  • thread:需要取消的线程TID

获取当前线程TID

#include <pthread.h>

// 返回值:当前线程的TID
pthread_t pthread_self(void);

控制取消状态与类型

开启/关闭取消请求
#include <pthread.h>

// 返回值:成功返回0,失败返回非0错误码
int pthread_setcancelstate(int state, int *oldstate);
  • state:取消状态
    • PTHREAD_CANCEL_ENABLE:开启取消(默认)
    • PTHREAD_CANCEL_DISABLE:关闭取消(忽略取消请求)
  • oldstate:输出参数,存储修改前的旧状态
设置取消类型
#include <pthread.h>

// 返回值:成功返回0,失败返回非0错误码
int pthread_setcanceltype(int type, int *oldtype);
  • type:取消类型
    • PTHREAD_CANCEL_DEFERRED:延时取消(默认,线程执行到“取消点”才响应)
    • PTHREAD_CANCEL_ASYNCHRONOUS:立即取消(收到请求后立即终止)

取消清理函数

线程被取消时,自动执行的函数,用于释放资源(如打开的文件、堆内存、锁等)。

#include <pthread.h>

// 注册清理函数(必须与pthread_cleanup_pop成对使用,且在同一代码块中)
void pthread_cleanup_push(void (*routine)(void *), void *arg);

// 注销清理函数
void pthread_cleanup_pop(int execute);
  • routine:清理函数指针(格式:void (*)(void *)
  • arg:传递给清理函数的参数
  • execute
    • 0:线程正常结束时,不执行清理函数
    • 1:线程正常结束时,执行清理函数
    • 注意:无论execute值如何,线程被取消时都会执行清理函数

示例代码(线程取消与清理)

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>

FILE *fp = NULL; // 全局文件指针(需在取消时关闭)

// 取消清理函数:释放文件资源
void clear_task(void *arg) {
    printf("线程被取消,执行清理函数(关闭文件)\n");
    if (fp != NULL) {
        fclose(fp);
        fp = NULL;
    }
}

// 线程任务函数:循环写入文件
void *task(void *arg) {
    // 打开文件
    fp = fopen("my.txt", "w+");
    if (fp == NULL) {
        perror("文件打开失败");
        pthread_exit(NULL);
    }

    // 注册清理函数(必须与pop成对出现)
    pthread_cleanup_push(clear_task, NULL);

    // 设置取消状态为开启(默认开启,此处仅演示)
    int old_state;
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &old_state);

    // 设置取消类型为延时取消(默认)
    int old_type;
    pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, &old_type);

    int i = 0;
    while (1) {
        fputc('Q', fp); // 写入文件(模拟任务)
        printf("线程正在写入数据(%d次)\n", ++i);
        sleep(1);

        if (i == 10) {
            break; // 正常结束
        }
    }

    // 正常结束时不执行清理函数(execute=0),手动关闭文件
    pthread_cleanup_pop(0);
    fclose(fp);
    fp = NULL;
    printf("线程正常结束\n");

    return NULL;
}

int main() {
    // 创建线程
    pthread_t tid;
    pthread_create(&tid, NULL, task, NULL);

    // 等待用户输入,触发取消
    printf("按任意键取消线程...\n");
    getchar();
    pthread_cancel(tid); // 发送取消请求

    // 回收线程资源
    pthread_join(tid, NULL);
    return 0;
}

线程的接合

线程退出后不会立即释放系统资源,会成为僵尸线程。通过pthread_join可回收僵尸线程资源,并获取其退出返回值,该操作称为“线程接合”。

核心接口

#include <pthread.h>

// 返回值:成功返回0,失败返回非0错误码
int pthread_join(pthread_t tid, void **val);

接口说明

  • 若指定tid的线程未退出,当前线程会阻塞等待
  • 无需获取退出值时,val设为NULL
  • 若线程处于分离状态或不存在,函数会出错返回

扩展接合接口(GNU扩展)

#define _GNU_SOURCE             /* 必须定义该宏才能使用 */
#include <pthread.h>

// 非阻塞接合:线程未退出时直接返回错误,不阻塞
int pthread_tryjoin_np(pthread_t thread, void **retval);

// 定时阻塞接合:超过指定时间未退出则返回错误
int pthread_timedjoin_np(pthread_t thread, void **retval,
                        const struct timespec *abstime);

示例代码(三种接合方式)

#define _GNU_SOURCE 
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>

// 选择接合方式:BLOCKING(阻塞)、NON_BLOCKING(非阻塞)、TIME_OUT(定时)
#define BLOCKING  1
#define NON_BLOCKING 0
#define TIME_OUT 0

void *MyTask(void *arg) {
    sleep(12); // 模拟线程执行耗时操作
    static int Num = 123; // 静态变量(退出后仍存在,可作为返回值)
    printf("myTask: Num的地址=%p\n", &Num);
    pthread_exit(&Num); // 退出线程并返回Num地址
}

int main(int argc, char const *argv[]) {
    pthread_t tid;
    pthread_create(&tid, NULL, MyTask, NULL);

    int *ret; // 存储线程返回值

#if BLOCKING
    // 1. 阻塞等待线程退出
    pthread_join(tid, (void **)&ret);
    printf("阻塞接合成功,返回值=%d\n", *ret);

#elif NON_BLOCKING
    // 2. 非阻塞接合
    errno = pthread_tryjoin_np(tid, (void **)&ret);
    perror("非阻塞接合结果"); // 线程未退出时会提示错误

#elif TIME_OUT
    // 3. 定时阻塞接合(等待2秒)
    struct timespec abstime;
    abstime.tv_sec = time(NULL) + 2; // 绝对时间(当前时间+2秒)
    abstime.tv_nsec = 0;
    errno = pthread_timedjoin_np(tid, (void **)&ret, &abstime);
    perror("定时接合结果"); // 2秒内未退出会提示超时错误

#endif

    printf("Main: 线程返回值地址=%p\n", ret);
    return 0;
}

编译注意事项

编译含线程函数的代码时,需手动链接线程库(系统不会默认链接),否则会报“未定义引用”错误:

# 错误编译(无链接线程库)
gcc 线程代码.c 
# 报错:undefined reference to `pthread_create'

# 正确编译(添加 -lpthread 链接线程库)
gcc 线程代码.c -lpthread

其他接口:检测文件权限

用于检测指定路径文件的权限(存在性、读写执行权限):

#include <unistd.h>

// 返回值:权限全部满足返回0,否则返回-1
int access(const char *pathname, int mode);

参数说明

  • pathname:目标文件的路径(绝对路径或相对路径)
  • mode:检测的权限类型(可组合使用,如R_OK | W_OK):
    • R_OK:是否可读
    • W_OK:是否可写
    • X_OK:是否可执行
    • F_OK:文件是否存在

示例用法

#include <stdio.h>
#include <unistd.h>

int main() {
    const char *file = "test.txt";
    // 检测文件是否存在且可读可写
    if (access(file, F_OK | R_OK | W_OK) == 0) {
        printf("文件存在,且拥有读写权限\n");
    } else {
        perror("权限检测失败");
    }
    return 0;
}

线程的属性

线程拥有多种可配置属性,可通过属性变量统一设置(创建线程时传入pthread_createattr参数)。

线程属性列表

通过man pthread_attr_(连续按两下Tab)可查看所有属性相关函数,核心属性如下:

获取属性API 功能描述 设置属性API 功能描述
pthread_attr_getdetachstate 获取线程分离状态 pthread_attr_setdetachstate 设置线程分离状态
pthread_attr_getguardsize 获取栈警戒区大小(防止栈溢出) pthread_attr_setguardsize 设置栈警戒区大小
pthread_attr_getinheritsched 获取调度策略继承方式 pthread_attr_setinheritsched 设置调度策略继承方式
pthread_attr_getschedpolicy 获取线程调度策略(如RR、FIFO) pthread_attr_setschedpolicy 设置线程调度策略
pthread_attr_getschedparam 获取调度参数(如优先级) pthread_attr_setschedparam 设置调度参数
pthread_attr_getscope 获取线程竞争范围(进程内/系统级) pthread_attr_setscope 设置线程竞争范围
pthread_attr_getaffinity_np 获取CPU亲和度(绑定到指定CPU) pthread_attr_setaffinity_np 设置CPU亲和度
pthread_attr_getstack 获取栈基地址和栈大小 pthread_attr_setstack 设置栈基地址和栈大小
pthread_attr_getstacksize 获取栈大小 pthread_attr_setstacksize 设置栈大小

属性变量的使用步骤

线程属性需通过pthread_attr_t类型的变量统一管理,使用流程如下:

  1. 定义并初始化属性变量
  2. 为属性变量设置具体属性
  3. 传入pthread_create创建线程
  4. 销毁属性变量(释放资源)

核心接口(初始化与销毁)

#include <pthread.h>

// 初始化属性变量(设为默认属性)
int pthread_attr_init(pthread_attr_t *attr);

// 销毁属性变量(释放资源,不可再使用)
int pthread_attr_destroy(pthread_attr_t *attr);

示例代码(设置分离属性)

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

void* TaskFunc(void *arg) {
    sleep(2); // 模拟线程执行
    pthread_exit(NULL);
}

int main(int argc, char const *argv[]) {
    pthread_attr_t attr;
    pthread_attr_init(&attr); // 初始化属性变量

    // 1. 设置线程为分离状态(退出后自动释放资源,无需join)
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    // 2. 设置调度策略为时间片轮转(RR)
    pthread_attr_setschedpolicy(&attr, SCHED_RR);

    // 创建线程(传入属性变量)
    pthread_t tid;
    pthread_create(&tid, &attr, TaskFunc, NULL);

    // 尝试接合分离状态的线程(会失败)
    errno = pthread_join(tid, NULL);
    if (errno == 0) {
        printf("线程接合成功\n");
    } else {
        perror("join error"); // 输出错误:Invalid argument
    }

    // 销毁属性变量
    pthread_attr_destroy(&attr);

    while (1) pause(); // 让主线程不退出
    return 0;
}

拓展:动态分离线程

若线程创建时未设置分离属性,可通过以下函数在运行时强制分离:

#include <pthread.h>

// 返回值:成功返回0,失败返回非0错误码
int pthread_detach(pthread_t tid);

线程栈空间的设置

线程栈空间用于存储局部变量和函数调用上下文,默认大小由系统配置(可通过ulimit -a查看,通常为10240KB),可手动设置(需大于PTHREAD_STACK_MIN=16384字节)。

查看系统资源配置

ulimit -a  # 查看系统资源限制,重点关注 stack size

示例代码(设置栈大小)

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void *task(void *arg) {
    int i = 0;
    while (1) {
        printf("线程正在运行(%d次)\n", i++);
        sleep(1);
        if (i == 10) {
            pthread_exit(NULL); // 退出线程
        }
    }
}

int main() {
    // 初始化属性变量
    pthread_attr_t attr;
    pthread_attr_init(&attr);

    // 设置栈大小(2倍最小栈大小)
    size_t stack_size = PTHREAD_STACK_MIN * 2;
    pthread_attr_setstacksize(&attr, stack_size);

    // 创建线程(使用自定义栈大小)
    pthread_t tid;
    pthread_create(&tid, &attr, task, NULL);

    // 获取并打印当前栈大小
    size_t get_size = 0;
    pthread_attr_getstacksize(&attr, &get_size);
    printf("线程栈大小设置为:%ld 字节\n", get_size);

    // 回收线程资源
    pthread_join(tid, NULL);

    // 销毁属性变量
    pthread_attr_destroy(&attr);
    return 0;
}
posted @ 2025-12-02 08:23  Jaklin  阅读(22)  评论(0)    收藏  举报