文章目录
一、什么是线程属性?
在用 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_FIFO 或 SCHED_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, ¶m);
// 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),避免内存泄漏 - 栈大小仅作为演示,在实际里按需要调
七、属性相关的一些常见坑整理一下
写到最后顺手列一份“踩坑清单”,以后回头翻这篇笔记的时候会很有用:
-
设置了属性却忘了传给
pthread_create
pthread_create(&tid, NULL, ...)→ 所有pthread_attr_setXXX都白设了。 -
线程被设置为 detached 还去 pthread_join
- 规范上这是错误用法,很多实现会返回
EINVAL - 正确思路:要么 join 要么 detached,别混着搞
- 规范上这是错误用法,很多实现会返回
-
传栈上的局部变量给 detached 线程
主线程栈一旦退出,那块内存就无效了,子线程再用就是野指针。
解决办法:用malloc分配参数,在子线程里free。 -
以为调度策略/优先级设置成功了,其实没生效
忘了调用pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
或者没有 root 权限去设置实时调度策略。 -
栈设太小
递归、局部大数组,都可能直接打穿栈。调栈大小的时候最好心里有数,或者先用默认值,真有需要再缩小。
6469

被折叠的 条评论
为什么被折叠?



