【Linux C | 多线程编程】线程的连接、分离,资源销毁情况

😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀

🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C++、数据结构、音视频🍭

⏰发布时间⏰:2024-04-01 14:52:46

本文未经允许,不得转发!!!

目录

  • 🎄一、概述
  • 🎄二、线程的连接 pthread_join
  • 🎄三、线程的分离 pthread_detach
  • 🎄四、必须连接线程 或 分离线程
  • 🎄五、总结

    🎄一、概述

    记住一句话,“创建线程后,要么连接该线程,要么使该线程分离,否则可能导致资源无法释放”。

    怎样连接一个线程,连接线程是注意什么?

    为什么要连接线程?

    怎样分离一个线程,分离线程是注意什么?

    本文将围绕线程的连接、分离操作去召开,让读者可以清楚上面几个问题的答案。


    🎄二、线程的连接 pthread_join

    线程连接(joining):使用 pthread_join 函数,用来等待某线程的退出并接收它的返回值。

    线程连接可以用来等待一个线程执行,也可以用来获取线程的返回值,也可以即等待线程的结束又获取线程的返回值。这个等待有点类似于进程等待子进程退出的wait操作,但是有两点区别:

    • 1、进程间的等待只能是父进程等待子进程,而线程则没有这样的说法,只要是在一个线程组(进程)内,就可以对另外一个线程执行连接(join)操作;
    • 2、进程可以等待任一子进程的退出。而线程没有这样的操作,需要明确指定要连接的线程ID。这样的设计可以避免库函数的线程被连接了而导致库函数无法连接自己的线程。

    pthread_join函数原型:

    #include int pthread_join(pthread_t thread, void **retval);
    Compile and link with -pthread.
    
    • 函数描述:pthread_join可以等待某线程的退出并接收它的返回值。根据等待的线程是否退出, 可得到如下两种情况:
      • 等待的线程尚未退出, 那么pthread_join的调用线程就会陷入阻塞。
      • 等待的线程已经退出, 那么pthread_join函数会将线程的退出值(void*类型) 存放到retval指针指向的位置。
      • 函数参数:
        • thread:传入参数,要连接的线程ID;
        • retval:传出参数,用来接收线程返回值。
        • 函数返回值:成功返回 0,调用失败,和pthread_create函数一样, errno作为返回值返回。
          • EDEADLK:死锁,如自己连接自己,或者A连接B,B又连接A;
          • EINVAL:线程不是一个可连接(joinable)的线程;
          • EINVAL:已经有其他线程捷足先登,连接目标线程;
          • ESRCH:传入的线程ID不存在,查无此线程。

            可能产生连接死锁的两个情况:

            1、线程A连接线程A(自己连接自己);

            2、线程A连接线程B,线程B连接线程A。

            🌰举例子:

            // 07_pthread_join.c
            // gcc 07_pthread_join.c -l pthread
            #include #include int ret = -1;		// 全局变量记录线程返回值
            void *func(void *arg)
            {int *parg = arg;
            	printf("this thread arg is %d, my threadID is %lx \n", *parg, (unsigned long)pthread_self());
            	ret=*parg+1;
            	return (void*)&ret;
            }
            int main()
            {int arg=10;
            	pthread_t threadId;
            	pthread_create(&threadId, NULL, func, &arg);
            	int *pRet = NULL;
            	pthread_join(threadId, (void**)&pRet);
            	printf("pthread_join end, ret=%d\n", *pRet);
            	return 0;
            }
            

            🎄三、线程的分离 pthread_detach

            默认情况下, 新创建的线程处于可连接(Joinable)的状态, 可连接状态的线程退出后, 需要对其执行连接操作, 否则线程资源无法释放,从而造成资源泄漏。

            有时,我们并不关心该线程的返回值,也不想阻塞等待,但不执行连接又会资源泄露,那怎么办?线程库考虑到这种使用场景,提供了 pthread_detach 函数可将线程设置成已分离(detached)状态。处于已分离(detached)状态的线程退出时,由系统负责回收该线程的资源。

            pthread_detach函数原型:

            #include int pthread_detach(pthread_t thread);
            Compile and link with -pthread.
            
            • 函数描述:pthread_detach函数将thread指定的线程标记为已分离。当一个分离的线程终止时,它的资源会自动释放回系统,而不需要另一个线程与终止的线程连接。试图分离已分离的线程会导致未指定的行为。
            • 函数参数:thread,要分离的线程ID。
            • 函数返回值:成功返回 0,调用失败,和pthread_create函数一样, errno作为返回值返回。
              • EINVAL:线程不是一个可连接(joinable)的线程,已经处于已分离状态;
              • ESRCH:传入的线程ID不存在,查无此线程。

                其他相关内容:

                • 1、将线程的属性设定为已分离的第2种方式,使用 pthread_attr_setdetachstate 函数,这个比较少用可以了解一下。
                  #include int pthread_attr_setdetachstate(pthread_attr_t *attr,int detachstate);
                  int pthread_attr_getdetachstate(pthread_attr_t *attr,int *detachstate);
                  
                • 2、线程分离可由其他线程对其执行分离,也可以线程自己执行pthread_detach函数, 将自身设置成已分离的状态:
                  pthread_detach(pthread_self());
                  
                • 3、所谓已分离, 并不是指线程失去控制, 不归线程组管理, 而是指线程退出后, 系统会自动释放线程资源。

                🌰举例子:下面例子是主线程执行分离的,更常见的是,线程内部自己执行分离。

                // 07_pthread_detach.c
                // gcc 07_pthread_detach.c -l pthread
                #include #include void *func(void *arg)
                {int *parg = arg;
                	printf("this thread arg is %d, my threadID is %lx \n", *parg, (unsigned long)pthread_self());
                	return NULL;
                }
                int main()
                {int arg=10;
                	pthread_t threadId;
                	pthread_create(&threadId, NULL, func, &arg);
                	pthread_detach(threadId);	// 对该线程执行分离
                	printf("pthread_detach exec\n");
                	while(1); // 保持主线程不退出
                	return 0;
                }
                

                🎄四、必须连接线程 或 分离线程

                这一小节,我们了解为什么必须要 连接线程 或 分离线程 ?

                因为既不分离线程又不连接已经退出的线程,可能会导致资源无法释放。

                注意:已连接 或 已分离 的线程退出时,该线程的资源并没有立即调用munmap来释放掉,而是保留着被后面新建的线程复用。NPTL线程库的设计。

                释放线程资源的时候,NPTL认为进程可能再次创建线程,而频繁地munmap和mmap会影响性能,所以NTPL将该栈缓存起来,放到一个链表之中,如果有新的创建线程的请求,NPTL会首先在栈缓存链表中寻找空间合适的栈,有的话,直接将该栈分配给新创建的线程。

                下面通过一个例子来看看线程退出后的资源情况。

                // 07_pthread_join_detach_test.c
                // gcc 07_pthread_join_detach_test.c -l pthread -DNO_JOIN_DETACH	// 没有连接、分离
                // gcc 07_pthread_join_detach_test.c -l pthread -DTHREAD_JOIN		// 使用线程的连接
                // gcc 07_pthread_join_detach_test.c -l pthread -DTHREAD_DETACH		// 使用线程的分离
                #include #include #include void *func(void *arg)
                {#ifdef THREAD_DETACH
                	pthread_detach(pthread_self());
                #endif
                	printf("threadID = %lx, TID=%u \n", (unsigned long)pthread_self(), syscall(SYS_gettid));
                	
                	// 获取线程属性
                	pthread_attr_t gattr;
                	int s = pthread_getattr_np(pthread_self(), &gattr);
                	if (s != 0)
                	{printf("pthread_getattr_np error\n");
                		return NULL;
                	}
                	
                	// 获取线程栈地址和大小
                	void *stkaddr;
                	size_t v;
                	s = pthread_attr_getstack(&gattr, &stkaddr, &v);
                	if (s != 0)
                	{printf("pthread_attr_getstackaddr error\n");
                		return NULL;
                	}
                	printf("Stack address = %p, size=%luk btye\n", stkaddr, v/1024);
                	sleep(3);
                	printf("TID=%u EXIT\n", syscall(SYS_gettid));
                	return NULL;
                }
                int main()
                {#ifdef NO_JOIN_DETACH
                	printf("NO_JOIN_DETACH PID=%u\n",syscall(SYS_gettid));
                #endif
                #ifdef THREAD_JOIN
                	printf("THREAD_JOIN PID=%u\n",syscall(SYS_gettid));
                #endif
                #ifdef THREAD_DETACH
                	printf("THREAD_DETACH PID=%u\n",syscall(SYS_gettid));
                #endif
                	// 1、创建第一个线程
                	pthread_t threadId, threadId2;
                	pthread_create(&threadId, NULL, func, NULL);
                	// 2、等待上个线程结束
                #ifdef THREAD_JOIN
                	pthread_join(threadId, NULL);
                #else // 没有等待线程,就使用sleep等待上个线程结束
                	sleep(5);
                #endif
                	
                	// 3、创建第二个线程
                	pthread_create(&threadId2, NULL, func, NULL);
                	pause(); // 保持主线程不退出
                	return 0;
                }
                

                下面代码演示了线程资源使用的三个情况,下面分别看看其运行结果:

                • 🌰1、没有连接、分离;

                  复制上面代码,运行gcc 07_pthread_join_detach_test.c -l pthread -DNO_JOIN_DETACH编译,可以看到各个线程的栈地址不一样:

                  运行pmap查看内存分布情况,也可以看到这两个地址,如下图:

                  可以得出一个结论:如果线程既不连接、又不分离的话,那么:

                  1) 已经退出的线程,其空间没有被释放,仍然在进程的地址空间之内。

                  2) 新创建的线程,没有复用刚才退出的线程的地址空间。

                • 🌰2、使用线程的连接;

                  接下来看看,使用了线程连接的情况,上面代码运行gcc 07_pthread_join_detach_test.c -l pthread -DTHREAD_JOIN编译,运行结果如下,可以看到两个线程的栈地址是一样的,也就是说,第一个线程退出后,其地址空间被后面新建的线程复用了:

                  运行pmap查看进程内存分布情况,只有一个线程栈地址,如下图:

                • 🌰3、使用线程的分离。

                  接下来看看,使用了线程分离的情况,上面代码运行gcc 07_pthread_join_detach_test.c -l pthread -DTHREAD_DETACH编译,运行结果如下,可以看到两个线程的栈地址是一样的,也就是说,第一个线程退出后,其地址空间被后面新建的线程复用了:

                  运行pmap查看进程内存分布情况,只有一个线程栈地址,如下图:


                  🎄五、总结

                  本文结束了Linux系统编程的线程的连接(pthread_join)、线程的分离(pthread_detach),以及介绍了为什么要使用线程的连接、分离。

                  如果文章有帮助的话,点赞👍、收藏⭐,支持一波,谢谢 😁😁😁

                  参考资料:

                  《Linux环境编程:从应用到内核》