Linux线程属性详细解析

一、什么是线程属性?

在用 pthread_create 创建线程的时候,我们平时大多这么写:

pthread_t tid;
pthread_create(&tid, NULL, thread_func, arg);

第二个参数传的是 NULL,意思是“用默认属性创建线程”。

但是 POSIX 线程库其实提供了一个专门用来“定制线程”的东西:pthread_attr_t,可以理解为“线程配置对象”。你可以通过它控制很多东西,例如:

  • 线程是不是可 pthread_join(joinable / detached)
  • 线程栈有多大、栈放在哪
  • 栈有没有保护区(防止栈溢出)
  • 调度策略(普通、实时 FIFO、实时 RR)
  • 线程优先级
  • 线程是否继承创建者的调度属性

用法大致是这一套流程:

pthread_attr_t attr;
pthread_attr_init(&attr);          // 初始化属性对象

// 中间各种 pthread_attr_setXXX(&attr, ...);

pthread_t tid;
pthread_create(&tid, &attr, thread_func, arg);  // 用属性创建线程

pthread_attr_destroy(&attr);       // 用完销毁

如果你传的是 NULL,就等于完全用默认配置。


二、线程的“分离状态”:joinable 和 detached

这是线程属性里最常用、也最容易理解的一块。

默认情况下,线程是“可连接”的(joinable):

pthread_t tid;
pthread_create(&tid, NULL, thread_func, arg);

// 将来可以
pthread_join(tid, &retval);

线程结束后,它的“线程控制块”和返回值之类的资源不会立刻完全释放,必须调用 pthread_join 接一下,系统才回收那部分资源。
如果你既不 join,也不设置为 detached,线程结束后就会变成“线程僵尸”,长期占着一点内核资源(进程结束时才会统一回收)。

另一种方式是“分离线程”(detached):

  • 线程结束之后,资源由系统自动回收
  • 不能再对它调用 pthread_join
  • 更适合那种“我只让你去干活,不关心你什么时候结束”的后台线程

如何通过属性设置分离状态

pthread_attr_setdetachstate

pthread_attr_t attr;
pthread_attr_init(&attr);

pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// 或者:pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); // 默认值

pthread_t tid;
pthread_create(&tid, &attr, thread_func, arg);

pthread_attr_destroy(&attr);

需要注意的是:你设置了 attr,但如果 pthread_create 第二个参数传的是 NULL,那前面的设置相当于白干。这一点很多人第一次都会踩坑:

pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

// 这样是错的:属性完全没用上
pthread_create(&tid, NULL, func, arg);    

要记得传 &attr 才能生效。

同样的效果,也可以在线程创建之后用 pthread_detach 来做:

pthread_t tid;
pthread_create(&tid, NULL, func, arg);  // 先用默认属性创建

pthread_detach(tid);                    // 再把它设置成 detached

或者在线程内部:

void* func(void* arg) {
    pthread_detach(pthread_self());  // 自己把自己分离
    ...
}

一般来说:

  • 需要拿返回值、或者需要精确等待线程结束 → 用 joinable + pthread_join
  • 只是丢出去让它干事,不在乎何时结束 → 用 detached(属性或者 pthread_detach

三、线程栈相关的属性

每个线程都有自己的栈,用来存函数调用、局部变量等。
Linux 上默认线程栈一般是几 MB(常见 8MB),对普通程序很好用,但在这几种情况里你可能想改一改:

  • 你要创建很多很多线程(几千、几万),默认栈太大,内存顶不住
  • 你的线程里递归很深,需要更大的栈空间
  • 特殊环境(比如嵌入式)、调试时想自己控制栈的位置

线程栈相关的属性主要有三个:

  • 栈大小:pthread_attr_setstacksize
  • 栈位置 + 大小:pthread_attr_setstack
  • 栈保护区大小:pthread_attr_setguardsize

设置栈大小

最常用的是调整栈大小:

pthread_attr_t attr;
pthread_attr_init(&attr);

// 设置栈大小为 1MB
pthread_attr_setstacksize(&attr, 1024 * 1024);

pthread_t tid;
pthread_create(&tid, &attr, thread_func, arg);

pthread_attr_destroy(&attr);

注意几点:

  • 栈大小不能小于 PTHREAD_STACK_MIN,否则会返回 EINVAL
  • 栈太小会导致栈溢出(尤其是有递归、局部大数组的时候)
  • 栈太大则会浪费内存,创建大量线程时风险更明显

自己指定栈的位置

更极端一点,你可以手动指定“栈放在哪一块内存里”:

pthread_attr_t attr;
pthread_attr_init(&attr);

size_t stack_size = 1024 * 1024;
void* stack = malloc(stack_size);

pthread_attr_setstack(&attr, stack, stack_size);

pthread_t tid;
pthread_create(&tid, &attr, thread_func, arg);

pthread_attr_destroy(&attr);
// 注意:当线程退出后,pthread 不会帮你 free 这块 stack,要你自己管理

这个一般只在特殊场景用得到,比如你想把线程栈放在共享内存或者特定区域里。

栈保护区(guard size)

guard 区的作用是防止栈溢出时“悄悄地”覆盖别的东西。
通常实现方式是在栈的末尾映射一段不可读写的内存,一旦栈撞到那里,就会触发段错误,让你知道栈用爆了。

设置方式:

size_t guard_size = 4096;  // 一页
pthread_attr_setguardsize(&attr, guard_size);

很多时候用默认的就行,除非你自己有精细控制的需求。


四、调度策略和优先级

这一块和操作系统的调度器有关,现实中用得没有“分离状态”和“栈大小”那么频繁,但在实时系统、音频处理、高优先级任务中很有用。

主要有几组相关的属性:

  • 调度策略:pthread_attr_setschedpolicy
  • 优先级参数:pthread_attr_setschedparam
  • 是否继承创建者的调度属性:pthread_attr_setinheritsched

常见调度策略

在 Linux 上,常见的策略有:

  • SCHED_OTHER:默认的普通分时调度(CFS),一般用户线程用这个
  • SCHED_FIFO:实时调度,先来先服务(队列),同优先级下不会被别的同级线程抢占
  • SCHED_RR:实时调度,时间片轮转,同优先级线程之间轮流执行

注意:
要想成功设置为 SCHED_FIFOSCHED_RR通常需要 root 权限,否则 pthread_create 会返回 EPERM

配置调度策略和优先级的标准流程

在属性上设置策略和优先级时,有一个容易被忽略的点:
pthread 属性有一个“是否继承父线程调度属性”的开关,如果你不关掉继承,你自己设的 policy / 优先级会被忽略。

完整流程大致如下:

pthread_attr_t attr;
pthread_attr_init(&attr);

// 1. 告诉 pthread:不要继承父线程调度策略,用我显式设置的
pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);

// 2. 设置调度策略,比如 SCHED_RR(需要 root)
pthread_attr_setschedpolicy(&attr, SCHED_RR);

// 3. 设置优先级
struct sched_param param;
int min = sched_get_priority_min(SCHED_RR);
int max = sched_get_priority_max(SCHED_RR);
param.sched_priority = max;  // 越大优先级越高
pthread_attr_setschedparam(&attr, &param);

// 4. 创建线程时一定要用这个 attr
pthread_t tid;
int ret = pthread_create(&tid, &attr, thread_func, arg);

pthread_attr_destroy(&attr);

如果你不调用 pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);,默认是 PTHREAD_INHERIT_SCHED(继承父线程),你自己设的 policy/priority 可能压根没生效。

这一点非常坑:看起来代码“写对了”,但其实完全没应用上。


五、线程竞争范围(scope)

还有一个相对比较冷门的属性:pthread_attr_setscope,用于设置线程竞争范围:

  • PTHREAD_SCOPE_SYSTEM:线程和系统中其他线程一起竞争 CPU(大多数实现只支持这个)
  • PTHREAD_SCOPE_PROCESS:线程只和本进程的线程竞争

在很多实际系统(包括常见的 Linux glibc)里,PTHREAD_SCOPE_PROCESS 要么不支持,要么效果有限,经常直接返回 ENOTSUP
除非你在研究操作系统/调度相关的东西,大部分应用可以忽略这一项。


六、结合一个例子把几个属性串起来

下面是一个相对综合一点的小示例:

  • 使用属性创建一个分离线程
  • 调整栈大小
  • 给它传一个用 malloc 分配的参数
  • 主线程不 join 它,自己做自己的事
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

void* thread_function(void* arg) {
    int count = *(int*)arg;
    free(arg);   // 参数来自 malloc,用完要释放

    printf("子线程开始运行,循环 %d 次\n", count);

    for (int i = 0; i < count; ++i) {
        printf("子线程:第 %d 次\n", i + 1);
        sleep(1);
    }

    printf("子线程结束!\n");
    pthread_exit(NULL);
}

int main(void) {
    pthread_attr_t attr;
    pthread_attr_init(&attr);

    // 设置为分离线程
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    // 设置栈大小 1MB(仅演示)
    pthread_attr_setstacksize(&attr, 1024 * 1024);

    pthread_t tid;

    int* p = (int*)malloc(sizeof(int));
    *p = 5;

    int ret = pthread_create(&tid, &attr, thread_function, p);
    if (ret != 0) {
        printf("pthread_create failed: %d\n", ret);
        exit(1);
    }

    pthread_attr_destroy(&attr);

    printf("主线程:不 join 子线程,自己干自己的事...\n");

    for (int i = 0; i < 10; ++i) {
        printf("主线程:tick %d\n", i + 1);
        sleep(1);
    }

    printf("主线程结束。\n");
    return 0;
}

这里有几个点可以顺便强调一下:

  • 子线程是 detached,所以不能也不需要 pthread_join
  • 参数用 malloc 分配,这样即使主线程先结束,子线程也还能安全访问那块内存
  • 子线程自己 free(arg),避免内存泄漏
  • 栈大小仅作为演示,在实际里按需要调

七、属性相关的一些常见坑整理一下

写到最后顺手列一份“踩坑清单”,以后回头翻这篇笔记的时候会很有用:

  1. 设置了属性却忘了传给 pthread_create
    pthread_create(&tid, NULL, ...) → 所有 pthread_attr_setXXX 都白设了。

  2. 线程被设置为 detached 还去 pthread_join

    • 规范上这是错误用法,很多实现会返回 EINVAL
    • 正确思路:要么 join 要么 detached,别混着搞
  3. 传栈上的局部变量给 detached 线程
    主线程栈一旦退出,那块内存就无效了,子线程再用就是野指针。
    解决办法:用 malloc 分配参数,在子线程里 free

  4. 以为调度策略/优先级设置成功了,其实没生效
    忘了调用 pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
    或者没有 root 权限去设置实时调度策略。

  5. 栈设太小
    递归、局部大数组,都可能直接打穿栈。调栈大小的时候最好心里有数,或者先用默认值,真有需要再缩小。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值