C语言pthread_create函数及参数含义相关技术咨询
pthread_create 函数参数设计逻辑解析
POSIX线程库中pthread_create的函数原型如下:
int pthread_create( pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
一、第一、第二个参数为什么设计为指针类型
两个参数用指针的设计逻辑完全不同,分开说明:
- 第一个参数
pthread_t *thread是输出型参数
函数本身的int返回值专门用来返回错误码(成功返回0,失败返回对应错误编号,如资源不足、属性非法等),无法再承载线程ID的返回。调用方传入一个pthread_t类型变量的地址,函数创建线程成功后,会把新线程的ID直接写入这个地址指向的内存,调用方后续执行pthread_join、pthread_cancel等操作时,直接用这个变量里存的线程ID即可。
另外这种设计也屏蔽了跨平台差异:Linux下pthread_t是无符号长整型,部分BSD系统下pthread_t是结构体指针,调用方不需要关心类型的具体实现,只要按要求传地址就行。 - 第二个参数
const pthread_attr_t *attr是输入型参数
首先pthread_attr_t是存储线程属性的结构体,内部包含栈大小、调度策略、分离状态等十余个字段,结构体体积不小,传指针只需要传递8字节(64位系统)的内存地址,避免传值时拷贝整个结构体带来的性能开销。其次指针天然支持空值语义,传NULL就可以明确告知库函数“使用默认属性”,不需要额外定义特殊的标记值。加const修饰是为了保证函数内部不会修改调用方传入的属性结构体。
二、为什么第二个参数绝大多数场景传NULL?支持哪些自定义配置?附示例
为什么常用NULL传参
NULL代表使用系统默认的线程属性,这套默认配置覆盖了90%以上的常规业务开发场景:默认栈大小通常为8MB、线程为可汇合状态(需要调用pthread_join回收资源)、调度策略和优先级继承创建者线程、栈地址由系统自动分配、栈溢出警戒区为4KB。
直接传NULL还省掉了手动调用pthread_attr_init初始化属性、pthread_attr_destroy销毁属性的步骤,不容易出现资源泄漏或者配置错误,除非有明确的特殊需求,否则没必要自定义属性。
支持的自定义属性项
你可以通过pthread_attr_setxxx系列接口配置以下常用属性:
- 线程分离状态:可配置为创建即分离(
PTHREAD_CREATE_DETACHED,线程退出后自动回收资源,无法被join),默认是可汇合状态 - 线程栈大小:自定义栈内存大小,比如跑深度递归逻辑时可以调大,嵌入式内存紧张场景可以调小
- 调度策略与优先级:可配置实时调度策略
SCHED_FIFO/SCHED_RR(需要root权限),或默认的分时调度策略SCHED_OTHER,以及对应策略下的优先级 - 调度继承性:配置新线程是继承创建线程的调度参数(默认),还是使用属性结构体里自定义的调度参数
- 栈警戒区大小:自定义栈顶预留的禁止访问内存段大小,用来检测栈溢出
- 栈起始地址:手动指定线程栈的内存位置,仅内核、嵌入式等特殊场景使用,常规用户态开发几乎不用
自定义属性配置示例
以下代码演示如何配置分离状态+16MB自定义栈大小的线程:
#include <pthread.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> void* thread_work(void* arg) { printf("自定义属性线程启动,收到参数:%s\n", (char*)arg); // 业务逻辑 return NULL; } int main() { pthread_t tid; pthread_attr_t attr; int ret; // 必须先初始化属性结构体,不能直接手动修改结构体内部字段 ret = pthread_attr_init(&attr); if (ret != 0) { fprintf(stderr, "属性初始化失败: %s\n", strerror(ret)); exit(EXIT_FAILURE); } // 配置1:设置线程为分离状态,无需后续join ret = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); if (ret != 0) { fprintf(stderr, "设置分离状态失败: %s\n", strerror(ret)); } // 配置2:设置线程栈大小为16MB size_t stack_size = 16 * 1024 * 1024; ret = pthread_attr_setstacksize(&attr, stack_size); if (ret != 0) { fprintf(stderr, "设置栈大小失败: %s\n", strerror(ret)); } // 用自定义属性创建线程 char* msg = "test custom attr"; ret = pthread_create(&tid, &attr, thread_work, msg); if (ret != 0) { fprintf(stderr, "线程创建失败: %s\n", strerror(ret)); exit(EXIT_FAILURE); } // 线程创建完成后即可销毁属性结构体,不需要等线程运行结束 pthread_attr_destroy(&attr); // 分离线程无法join,休眠1秒等待线程执行完输出 sleep(1); return 0; }
编译时需要加-lpthread链接线程库。
三、void*(*start_routine)(void*) 中的指针含义
这是C语言的函数指针声明,从内向外拆解非常好理解:
(*start_routine):说明start_routine是一个指针,指向的目标是函数- 末尾的
(void*):被指向的这个函数,入参是一个void*类型的通用指针——对应pthread_create的第四个参数arg,线程启动时会把arg原封不动传给入口函数当入参。void*可以承载任意类型的内存地址,不管你要给线程传int、结构体还是其他类型的数据,都可以把地址强转成void*传入,线程内部再强转回对应类型使用即可。 - 开头的
void*:被指向的这个函数,返回值也是一个void*类型的通用指针——线程退出时可以返回任意类型的内存地址,这个返回值会被pthread_join捕获,用来把线程的执行结果传回给等待它的线程。
这种设计是为了做通用接口:库函数不可能提前预知每个线程要传什么参数、返回什么结果,用通用指针就可以适配所有业务场景,不需要为不同类型的线程设计多套创建接口。
简单用法示例:
// 入参是两个整数的结构体,返回值是堆上分配的计算结果 typedef struct { int x; int y; } CalcArg; void* calc_add(void* arg) { CalcArg* para = (CalcArg*)arg; int* result = malloc(sizeof(int)); *result = para->x + para->y; return (void*)result; } // 调用方代码参考 // pthread_t tid; // CalcArg a = {.x=10, .y=20}; // pthread_create(&tid, NULL, calc_add, &a); // int* res; // pthread_join(tid, (void**)&res); // printf("计算结果:%d\n", *res); // free(res);
内容的提问来源于stack exchange,提问作者GDD
相关产品推荐
相关产品推荐

