- 作者: 雪山肥鱼
- 时间:20210706 22:41z
- 目的:多线程的生命周期
# 多线程 vs 多进程
# 多线程举例
# 多线程本质
## 更高的posix标准
# 线程的资源回收 pthread_join
# 线程的退出引发的问题
## 代码举例
## 关于pthread_cansle 与 pthread_cleanup_pop/push
多线程 vs 多进程
进程是资源封装的单位。所以一个进程是看不到另一个进程的资源的。在进程之间的通讯中,关注的是,数据的交互。
- 小量数据 socket
- 大量数据 走 共享内存
很明显多进程的好处是进程之间的隔离
在多线程中,线程只是调度单元,同步也好,互斥也好。并不涉及数据的交互,因为在多线程模型中,数据已共享,数据均可被所有线程共享。此时内存本来就是共享的。
多线程的通信开销,是远低于多进程的
那么多线程面对的问题是:
- 一个线程挂,则进程挂
- 一个线程造成的 memory leak, 可能死的是另一个线程。定位问题非常困难。
关系亲密的(数据层)用多线程模型,关系一般的用多进程
多线程举例
#define THREAD_NUMBER 2
int retval_hello = 2, retval_hello2 = 3;
void * hello1(void * arg) {
char * hello_str = (char*arg);
sleep(1);
printf("%s\n", hello_str);
//exit proccess exit
pthread_exit(&retval_hello1);
}
void * hello2(void * arg) {
char * hello_str = (char*arg);
sleep(1);
printf("%s\n", hello_str);
//exit proccess exit
pthread_exit(&retval_hello2);
}
int main(int argc, char ** argv) {
int i;
int ret_val;
int * retval;
int * retval_hello[2];
pthread_t pt[THREAD_NUMBER];
const char * arg[THREAD_NUMBER];
arg[0] = "hello world from thread1";
arg[1] = "hello world from thread2";
printf("Begin to create threads...\n");
ret_val = pthread_create(&pt[0], NULL, hello1, (void*)arg[0]);
if(ret_val != 0) {
printf("pthread_create error!\n");
exit(1);
}
ret_val = pthread_create(&pt[1], NULL, hello2, (void*)arg[1]);
if(ret_val != 0) {
printf("pthread_create error!\n");
exit(1);
}
printf("Now, the main thread returns\n");
printf("Begin to wait for threads..\n");
for(i =0; i < THREAD_NUMBER; i++) {
//类似 waitpid()
ret_val = pthread_join(pt[i], (void **)&retval_hello[i]);
}
return 0;
}
exit 是进程级别的api, 调用就整个进程退出了
多线程本质
pthread_create -> clone
资源共享.png
clone 与fork 的本质就是 p1 p2 资源共享,但是有两个不同的pid, 即两个不同的调度单元,tast_struct.
更高的 POSIX 标准
只是用上述从资源的共享角度上描述线程,是不够的。
- 查看进程列表的时候,相关的一组task_struct应当被展现为列表的一个节点
进程开了10个线程,用top命令查看 都只能查看到主线程,即只能看到一个人。tgid的概念。 - 发送给这个进程的信号(对应的kill 系统调用),将被对应的这一组task_struct所共享,并且被其中的任意一个线程处理
ctrl+c 进程的所有线程都退出 - 发送给某个线程的信号(对应的 pthread_kill), 将只被对应的一个task_struct接受,并且由它自己来处理
线程级别的信号,只能打断指定线程 - 当进程被停止或继续时(对应 SIGSTOP/SIGCONT) 信号,对应的这一组task_struct状态将改变
- 当进程收到一个致命信号(比如由于段错误收到的 SIGSEGV), 对应的这一组task_struct全部退出。同生死,共进退
NPTL模型
LINUX中使用 NPTL模型满足 POSIX标准
TGID
Top中看到的命令都是 TGID,为了让一个进程的所有线程看上去像是一个整体
图片.png
#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
#include <signal.h>
struct thread_param
{
char info;
int num;
};
#if 0
void segvSignal(int sig)
{
printf("thread pid:%d, tid:%lu\n",getpid(), pthread_self());
}
#endif
void sighandler1(int sig)
{
printf("%s got the signal...\n", __func__);
}
void sighandler2(int sig)
{
printf("%s got the signal...\n", __func__);
}
void* thread_fun(void* param)
{
struct thread_param* p;
p=(struct thread_param*)param;
int i;
if (signal(SIGINT,sighandler2) == SIG_ERR)
return -1;
printf("thread pid:%d, tid:%lu\n",getpid(), pthread_self());
for(i=0;i<p->num;i++){
#if 1 /* change to #if 1 for debugging high cpu-loading issues */
while(1);
#else
sleep(1);
#endif
printf("%i: %c\n",i,p->info);
}
return NULL;
}
int main(void)
{
pthread_t tid1,tid2;
struct thread_param info1;
struct thread_param info2;
int ret;
if (signal(SIGINT,sighandler1) == SIG_ERR)
return -1;
info1.info='T';
info1.num=2000000;
printf("main pid:%d, tid:%lu\n",getpid(), pthread_self());
ret=pthread_create(&tid1,NULL,thread_fun,&info1);
if(ret==-1){
perror("cannot create new thread");
return 1;
}
info2.info='S';
info2.num=300000;
ret=pthread_create(&tid2,NULL,thread_fun,&info2);
if(ret==-1){
perror("cannot create new thread");
return 1;
}
sleep(1);
//pthread_kill(pthread_self(), SIGINT);
if(pthread_join(tid1,NULL)!=0){
perror("call pthread_join function fail");
return 1;
}
if(pthread_join(tid2,NULL)!=0){
perror("call pthread_join function fail");
return 1;
}
return 0;
}
代码行为描述:
主进程开了2根线程,所以总共有3个线程
让两个子线程死循环,利用 top 和 htop 查看信息。
-
top 只有 一个pid 进程整体
top.png -
top -H 线程视角
top -H.png -
htop
htop.png
现象总结:
top 只会看到tgid 的信息。
我们去 /proc/1606/ 或者 /proc/1607/ 两个文件夹中进行操作
cat /proc/1606/status
status.png
pthread_self().png
这一大串数字是 libc中维护的数字,和内核没什么关系。
用户态的c库所维护的tid, 和内核没半毛钱关系,只是用户态用来标识线程。
线程级和进程级的信号
posix标准规定,线程和线程之间是可以互发信号的,所以每根线程会有自己的signalpending, 对于进程来说,会有进程级别的signalpending
- kill 是 进程级别的
- pthread_kill 是 线程级别的
但是 信号的行为 是进程级别的。比如
signal(SIGINT, func1);
signal(SIGINT, func2);
singal(SIGINT, func3);
对于所有线程来说,最终 SIGINT 的行为 是 func3
代码示例,依旧使用上述代码,打开 pthread_kill()
sleep(1);
return 0;
pthread_kill(pthread_self(), SIGINT);
if(pthread_join(tid1,NULL)!=0){
perror("call pthread_join function fail");
return 1;
}
if(pthread_join(tid2,NULL)!=0){
perror("call pthread_join function fail");
return 1;
}
return 0;
杀掉自己,但是 信号原来注册的 signal handler1, 但是在线程函数中 signal(SIGINT, sighandler2) 则会刷出来 signalhandler 2:
1s 后的结果.png 主线程状态.png
exit_group 的系统调用
- 依旧是上述代码, 在主线程中,开好线程后,sleep 1s, 紧跟着 exit( ).
ret=pthread_create(&tid2,NULL,thread_fun,&info2);
if(ret==-1){
perror("cannot create new thread");
return 1;
}
sleep(1);
exit(0);
exit_group(0).png
exit(0) 转化成了 exit_group(0)
整个进程退出,代表所有线程退出
- 当没有join子线程,直接在主线程中调用return 0; 效果和原理 同上
ret=pthread_create(&tid2,NULL,thread_fun,&info2);
if(ret==-1){
perror("cannot create new thread");
return 1;
}
sleep(1);
return 0;
#if 0
pthread_kill(pthread_self(), SIGINT);
if(pthread_join(tid1,NULL)!=0){
perror("call pthread_join function fail");
return 1;
}
if(pthread_join(tid2,NULL)!=0){
perror("call pthread_join function fail");
return 1;
}
return 0;
#endif
}
return 0.png
主线程的 main函数 返回,clib 会 主动调用一个 exit_grou(0) 所有线程均退出。
所以 main函数的返回 意味着整个进程的返回
-
那么尝试在子线程中调用 exit(0)
结果: 整个进程退出
exitd with 0.png -
那么尝试在子线程中调用 return 0
子线程退出。main函数继续 running
总结:
- 主线程中调用 return 0 (libc 根据返回值0,会调用 exit_group 0)和 exit(0) 最终都会走到 exit_group 0
- 在子线程中 调用 exit(0), 整个进程退出 不会调用 exit_group
- 在子线程中 调用 return 0 子线程退出。 2根线程 短暂的存留在
/proc/1939/task 下
- 只让某个线程退出
pthread_exit(0); 即可
最终走的是 exit(0) 自己退出 与他人无关
线程的资源回收 pthread_join
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
void* worker (void* unusued)
{
// pthread_detach(pthread_self());
// Do nothing
}
int main()
{
pthread_t thread_id;
for(int i=0; i < 200000; i++)
{
if (pthread_create(&thread_id, NULL, &worker, NULL) != 0) {
fprintf(stderr, "pthread_create failed at %d\n", i);
perror("pthread_create");
break;
}
}
sleep(1000);
return 0;
}
创建线程的极限,用如下命令编写,用32位编译,死的更快
死亡.png
pthread_join 会把 线程分配的tcb 等资源全部释放。
pthread_detach 不用手动调用pthread_join, 目的是让线程游离了。自己工作完全不care 其他线程。
比如 项目中 有一个debug的线程,把进程某些数据结构和状态周期性的通过socket 发给另外的syslog. 又不怎么和其他线程工作,完全游离状态,这个时候用detach.
释放的动作 都是 libc的行为。即ntpl中的行为,与内核关系不大。
工程中很少使用 detach。因为线程的进退,要我们自己搞清楚。
pthread_join 调用的是 user space 中 futex(了解就好,遇到再说)。
线程的退出可能会引起leak
不pthread_join 可能会引起leak。
众所周知,如果非detach的线程,在线程结束时,并不主动join,是会造成内存呢泄露。但我在线程中申请资源,却在线程结束时,不主动free,且主动join,依然存在mem leak。
线程死的时候,是不会释放其申请的内存资源的!因为 heap 是追随整个进程的
代码举例
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void * test() {
//char*p = (char*)malloc(1000*sizeof(char));
printf("ok\n");
//free(p);
return;
}
int main(int argc, char ** argv) {
pthread_t tid;
pthread_create(&tid, NULL, test, NULL);
//pthread_join(tid, NMU
}
用 valgrind 进行内存检查
gcc -g test.c -o test -lpthread
valgrind --tool=memcheck --leak-check=full ./test
未手动phread_join.png
手动join:
手动 pthread_join.png
手动join, 但未释放heap 空间:
手动join, 但未释放heap.png
手动join, 手动释放heap:
join+free.png
所以,pthread_join 只回收与线程相关的tcb,而申请的heap空间则不管 ,因为heap追随的是整个进程
关于pthread_cancel 与 pthread_cleanup_pop/push
void pthread_cleanup_push(void(*routine)(void*), void * arg);
void pthread_cleanup_pop(int execute)
pthread_cancel 这种api就非常不靠谱。
T1 看 T2 不顺眼,可以用pthread_cancel 把T2 停掉。但是T2 被干掉之前持有 mutex,sem 和malloc一段内存,打开一个文件等。这些都没来得及回收.
所以pthread_cancel 会与 pthread_cleanup_pop/push , 以及pthread_testcancel 联合使用:
解读 man pthread_cleanup_pop/push 函数
#include <pthread.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#define handle_error_en(en, msg) \
do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)
static int done = 0;
static int cleanup_pop_arg = 0;
static int cnt = 0;
static void
cleanup_handler1(void *arg)
{
printf("Called clean-up handler 1\n");
cnt = 0;
}
static void
cleanup_handler2(void *arg)
{
printf("Called clean-up handler 2\n");
cnt = 0;
}
static void *
thread_start(void *arg)
{
time_t start, curr;
printf("New thread started\n");
pthread_cleanup_push(cleanup_handler, NULL);
curr = start = time(NULL);
while (!done) {
pthread_testcancel(); /* A cancellation point */
if (curr < time(NULL)) {
curr = time(NULL);
printf("cnt = %d\n", cnt); /* A cancellation point */
cnt++;
//pthread_testcancel(); /* A cancellation point */
}
}
pthread_cleanup_pop(cleanup_pop_arg);
pthread_cleanup_push(cleanup_handler2);
pthread_testcancel();//可有可无
pthread_cleanup_push(1);
return NULL;
}
int
main(int argc, char *argv[])
{
pthread_t thr;
int s;
void *res;
s = pthread_create(&thr, NULL, thread_start, NULL);
if (s != 0)
handle_error_en(s, "pthread_create");
sleep(2); /* Allow new thread to run a while */
if (argc > 1) {
if (argc > 2)
cleanup_pop_arg = atoi(argv[2]);
done = 1;
} else {
printf("Canceling thread\n");
s = pthread_cancel(thr);
if (s != 0)
handle_error_en(s, "pthread_cancel");
}
s = pthread_join(thr, &res);
if (s != 0)
handle_error_en(s, "pthread_join");
if (res == PTHREAD_CANCELED)
printf("Thread was canceled; cnt = %d\n", cnt);
else
printf("Thread terminated normally; cnt = %d\n", cnt);
exit(EXIT_SUCCESS);
}
- 注意点:
- pthread_cleanup_pop
pthread_cleanup_push
成对出现,如果不成对出现,编译就报错。所以只要传入参数,pop执行的函数必定会全部弹出来。 - pthread_cleanup_pop(int execute)
如果execute 非零 则会弹出来一个栈(收集了所有push传入的函数)顶函数。
execute为 0 , 则不会弹出栈顶函数,即便栈中存了多个函数 - pthread_testcancel
设置取消点。pthread_cancel 只有在线程执行到 pthread_test_cancel 这个取消点,线程才会退出。有些c库函数天生有pthread_testcancel 线程取消点的
可以设置 pthread_testcancel 不同位置,来观察cnt的值
./a.out
./a.out 1
./a.out 1 0
./a.out 1 1
试试看结果如何?同时 栈里可以增加一个 cleanup_handler2 试一试
- pop 和 push 保护的是这两个函数之间的函数
退出线程,再释放资源,本来就不太合理,是有风险的。push 和pop google Andorid 已经不支持了。
- 可能会引起memory leak
- lock no release
- other issues
网友评论