【系统编程】线程控制原语
文章目录
- 一、获取线程 id
- 1.1 线程号 `pthread_t`
- 1.2 函数 `pthread_self`
- 1.3 函数 `pthread_equal`
- 二、创建线程
- 2.1 创建子线程
- 2.2 循环创建N个子线程
- 2.3 子线程传参地址错误
- 三、线程退出
- 3.1 线程退出方式比较
- 3.2 函数 `pthread_exit()`
- 四、线程资源回收
- 五、线程分离
- 5.1 线程结束与资源回收
- 5.2 函数`pthread_detach`
- 六、杀死(取消)线程
- 七、线程进程控制原语对比
一、获取线程 id
1.1 线程号 pthread_t
- 类型:
pthread_t
- 范围:只在所属进程中唯一
- 底层实现:
- Linux下一般是无符号长整数
- 有些系统可能是结构体,不能强行转换为整数来比较
1.2 函数 pthread_self
#include <pthread.h>
pthread_t pthread_self(void);
- 作用:获取线程 id,在进程内部去标识线程身份。
- 返回值:线程ID(类型为
pthread_t
)
1.3 函数 pthread_equal
int pthread_equal(pthread_t t1, pthread_t t2);
- 作用:判断
t1
和t2
是否是同一个线程 - 返回值:
- 相同:返回非 0
- 不同:返回 0
- 不能直接用 == 比较,为了可移植性必须使用该函数
二、创建线程
2.1 创建子线程
#include <pthread.h>int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine)(void *),void *arg
);
-
参数:
thread
:传出参数,新子线程的线程 idattr
:线程属性,默认传 NULL,表示使用默认属性start_routine
:函数指针,子线程的入口函数地址(回调函数)- 指向线程主函数(线程体),该函数运行结束,则线程结束。
pthread_create
调用成功,该函数会被自动调用起来
arg
:线程主体函数执行期间所使用的函数参数。(void*
类型)
-
返回值:
- 成功:0
- 失败:返回错误码(不是设置到
errno
)
-
错误处理:
- pthread_create 的错误码不保存在 errno 中
- 不能使用
perror()
打印错误信息 - 应该使用
strerror(ret)
将错误码转为字符串再打印 - 示例:
#include <string.h> char *strerror(int errnum); // 举例应用: int ret = pthread_create(&tid, NULL, thread_fun, (void *)&test); if (ret != 0) {printf("error number: %d\n", ret);fprintf(stderr, "error information: %s\n", strerror(ret)); }
-
创建线程示例:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>// 子线程的主体函数
void *tfn(void *arg)
{printf("tfn: pid = %d, pthread_id = %lu\n", getpid(), pthread_self());return NULL;
}int main(int argc, char *argv[])
{pthread_t tid;// 创建子线程int ret = pthread_create(&tid, NULL, tfn, NULL);if (ret != 0) {fprintf(stderr, "pthread_create err: %s\n", strerror(ret));}printf("main: pid = %d, pthread_id = %lu\n", getpid(), pthread_self());sleep(1); // 给子线程执行时间return 0; // 释放进程地址空间
}
2.2 循环创建N个子线程
- 每个子线程打印自己是第几个被创建出来的
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>void sys_err(const char *str)
{perror(str);exit(1);
}void *tfn(void *arg)
{int i = (int)arg;printf("this is %dth thread, pid: %d, tid: %lu\n", i+1, getpid(), pthread_self());return NULL;
}int main(int argc, char *argv[])
{int n = 5;if (argc == 2) {n = atoi(argv[1]);}int i, ret;pthread_t tid;for (i = 0; i < n; i++) {ret = pthread_create(&tid, NULL, tfn, (void *)i);if (ret != 0) {fprintf(stderr, "pthread_create err: %s\n", strerror(ret));}}usleep(1000);printf("this is main thread, pid: %d, tid: %lu\n", getpid(), pthread_self());return 0;
}
2.3 子线程传参地址错误
for (i = 0; i < n; i++) {ret = pthread_create(&tid, NULL, tfn, (void *)&i);if (ret != 0) {fprintf(stderr, "pthread_create err: %s\n", strerror(ret));}
}
void *tfn(void *arg)
{int i = *(int*)arg;printf("this is %dth thread, pid: %d, tid: %lu\n", i+1, getpid(), pthread_self());return NULL;
}
void *arg
与int i = *(int*)arg;
void *
是通用指针,可以指向任意类型(但不能直接解引用)。(int *)arg
是类型强制转换,把void *
转成int *
*(int *)arg
是取出这个地址指向的int
值
错误原因:
多个线程共享同一个变量的地址,从而造成数据竞争或混乱。
因为 &i
是主线程中的局部变量地址。多个线程在启动时都接收到这个相同的地址,而主线程在循环中每次 i++
,这个值在原地址上被修改,线程看到的是不断变化的结果。
- 线程启动是异步的,多个线程可能还没来得及执行,主线程已经把
i
改了。 - 所以每个线程读取的
i
值都不一定是自己那一轮的值,而是最后或中间某次循环的值。
正确:
方法 1:定义独立变量(如堆上申请)
for (int i = 0; i < 5; i++) {int *arg = malloc(sizeof(int));*arg = i;pthread_create(&tid[i], NULL, tfn, arg);
}
并在线程中释放:
void *tfn(void *arg)
{int i = *(int*)arg;printf("this is %dth thread\n", i+1);free(arg); // 避免内存泄漏return NULL;
}
方法 2:使用数组,保证每次传入的地址是独立的
int args[5];
for (int i = 0; i < 5; i++) {args[i] = i;pthread_create(&tid[i], NULL, tfn, (void *)&args[i]);
}
这种方式要确保 args
数组的生命周期足够长,在所有线程结束前不能销毁。
三、线程退出
3.1 线程退出方式比较
方式 | 作用对象 | 效果 | 建议使用场景 |
---|---|---|---|
return | 当前线程 | 返回到调用者那里 | 简单的线程函数使用 |
pthread_exit | 当前线程 | 正常退出当前线程 | 推荐,线程安全退出 |
exit | 当前进程 | 终止整个进程(所有线程终止) | 不要在线程中使用 |
exit()
终止整个进程,不仅仅是当前线程。会导致所有线程立刻终止。pthread_cancel()
可以杀死线程,但需要“保存点” —— 进内核即可得到。
3.2 函数 pthread_exit()
#include <pthread.h>
void pthread_exit(void *retval);
- 参数:返回给
pthread_join()
的退出值(可以是NULL
,也可以是指针) - 如果希望线程返回一个值,让别的线程(如主线程)获取它,就传一个指针进去
- 该指针不能指向线程函数内部的局部变量(栈内存)!
- 当线程退出后,栈帧销毁,指针失效。
- 主线程通过
pthread_join
获取到的是“野指针”。
四、线程资源回收
- 阻塞等待回收子线程
#include <pthread.h>
// int pthread_create(pthread_t *thread,...) 这里参1是指针
int pthread_join(pthread_t thread, void **retval);
-
参数:
pthread_t
:待回收的线程 idretval
:传出参数,保存待回收线程返回值的指针地址(void **
)
-
返回值:
- 成功:0
- 失败:错误号
-
示例:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>struct thrd {int var;char str[256];
};// 子线程的主体函数
void *tfn(void *arg)
{// struct thrd th;// th.var = 100;// strcpy(th.str, "hello thread")// pthread_exit((void *)&th);// 定义的局部变量,函数调用结束会被释放掉struct thrd *tval = (struct thrd *)arg;tval->var = 100;strcpy(tval->str, "hello thread");pthread_exit((void *)tval);// return (void *)tval; // 也可以
}int main(int argc, char *argv[])
{pthread_t tid;struct thrd arg, *retval;// 创建子线程int ret = pthread_create(&tid, NULL, tfn, (void *)&arg);if (ret != 0) {fprintf(stderr, "pthread_create err: %s\n", strerror(ret));}// 回收子线程退出值ret = pthread_join(tid, (void**)&retval);if (ret != 0) {fprintf(stderr, "pthread_join err: %s\n", strerror(ret));}printf("child thread exit with: var = %d, str = %s\n", retval->var, retval->str); // void* 指针是8字节pthread_exit((void *)0); // 退出主线程return 0; // 释放进程地址空间
}
五、线程分离
5.1 线程结束与资源回收
- 线程结束后,终止状态会一直保留,直到其他线程用
pthread_join
获取它的状态并回收资源。 - 如果没有
pthread_join
,资源不会释放,会产生“僵尸线程”现象(类似僵尸进程)。
5.2 函数pthread_detach
int pthread_detach(pthread_t thread);
- 参数
thread
:待设置为分离的线程 id - 作用:
- 把线程设置为“分离状态”。
- 分离状态的线程结束后,系统自动回收资源,不保留终止状态。
- 分离后不能再用
pthread_join
,否则返回EINVAL
错误。 - 已经分离的线程,不能再用
pthread_join
,不能获取线程退出值。
- 返回值:
- 成功:0
- 失败:错误号
- 特点:
- 调用后不阻塞。
- 常用于网络、多线程服务器,避免手动回收。
- 也可以在
pthread_create
时通过线程属性设置为分离状态。
六、杀死(取消)线程
int pthread_cancel(pthread_t thread);
- 参数:待杀死的线程 id
- 作用:
- 请求取消指定线程(类似进程的
kill
,但不是立即终止)。
- 请求取消指定线程(类似进程的
- 返回值:
- 成功:
0
- 失败:错误号
- 成功:
- 注意事项:
- 线程取消是延时的,需要线程运行到“取消点(保存点)”才能真正生效。
- 取消点(保存点):线程检查是否被取消的地方,通常是一些系统调用(如
read
、write
、open
、close
等)。 - 如果线程没有取消点,可在代码中调用
pthread_testcancel()
手动设置一个取消点。
- 被取消线程的退出值:
- 宏定义:
#define PTHREAD_CANCELED ((void *) -1)
- 被取消后,用
pthread_join
获取的退出值是(void *) -1
。
- 宏定义:
七、线程进程控制原语对比
功能 | 进程原语 | 线程原语 | 说明 |
---|---|---|---|
创建 | fork | pthread_create | 进程创建后资源独立,线程创建共享进程资源 |
退出 | exit | pthread_exit | 结束当前执行单元并释放资源 |
回收(等待结束) | wait/waitpid() | pthread_join | 等待子进程/线程结束并回收资源 |
终止(强制结束) | kill | pthread_cancel | 发送信号或请求终止执行 |
获取 ID | getpid | pthread_self | 获取当前进程/线程 ID |
返回 ID | getppid (父进程ID) | 无直接对应(需保存) | 线程父子关系不严格,对应的是同属一个进程 |
分离 | 无直接对应 | pthread_detach() | 线程设置为“分离状态”,分离状态的线程结束后,系统自动回收资源 |