3.线程等待
一般而言,一个线程被创建出来,就如同进程一般,也是需要被等待的。如果主线程不对新线程进行等待,那么这个新线程的资源也是不会被回收的。所以线程需要被等待,如果不等待会产生类似于“僵尸进程”的问题,也就是内存泄漏。
线程等待的函数: pthread_join()
int pthread_join(pthread_t thread, void **retval);
参数说明:
thread:被等待线程的ID
retval:它是一个输出型参数,用来获取新线程退出的时候,函数的返回值;新线程函数的返回值是void*,所以要获取一级指针的值,就需要二级指针,也就是void**;
返回值
成功返回0
失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
如果thread线程被别的线程调用pthread_ cancel异常终掉,retval所指向的单元里存放的是常数PTHREAD_ CANCELED。
如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。
#include <stdio.h> #include <pthread.h> #include <unistd.h> void* thread_run(void* args) { int num = *(int*)args; while(1){ printf("I am new thread [%d], I creat thread ID: %lu\n", num, pthread_self()); sleep(3); break; } return (void*)111; } int main() { pthread_t tid[NUM]; for(int i = 0; i < NUM; i++){ pthread_create(tid + i, NULL, thread_run, (void*)&i); sleep(1); } void* status = NULL; pthread_join(tid[0], &status); printf("ret: %d\n", (int)status); return 0; }
pthread_join函数默认是以阻塞的方式进行线程等待的。它只有等待线程退出后才可以拿到退出码;
我们知道进程退出时有三种状态:
1.代码跑完,结果正确
2.代码跑完,结果错误
3.代码异常终止
那么线性也是一样的,这里就存在一个问题,刚刚上面的代码,是获取线程的退出码的,那么代码异常终止,线程需要获取吗?
答案:不需要;pthread_join函数无法获取到线程异常退出时的信息。因为线程是进程内的一个执行流,如果进程中的某个线程崩溃了,那么整个进程也会因此而崩溃,此时我们根本没办法执行pthread_join函数,因为整个进程已经退出了;例如,我们在线程的执行例程当中制造一个野指针问题,当某一个线程执行到此处时就会崩溃,进而导致整个进程崩溃。
#include <stdio.h> #include <pthread.h> #include <unistd.h> void* thread_run(void* args) { int num = *(int*)args; while(1){ printf("I am new thread [%d], I creat thread ID: %lu\n", num, pthread_self()); sleep(1); //设置一个野指针问题 if(num == 3){ printf("thread number: %d\n quit", num); int* p = NULL; *p = 100; } } } int main() { pthread_t tid[5]; for(int i = 0; i < 5; i++){ pthread_create(tid + i, NULL, thread_run, (void*)&i); } void* status = NULL; for(int i = 0; i < 5; i++) { pthread_join(tid[i], &status); printf("tid[%d]: %d\n", i, (int)status); } return 0; }
运行代码,可以看到一旦某个线程崩溃了,整个进程也就跟着挂掉了,此时主线程连等待新线程的机会都没有,这也说明了多线程的健壮性不太强。所以pthread_join函数只能获取到线程正常退出时的退出码,用于判断线程的运行结果是否正确并不能获取异常情况。
4.线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
线程可以调用pthread_ exit函数终止自己。
一个线程可以调用pthread_ cancel函数终止同一进程中的另一个线程。线程终止的函数:pthread_exit(让自己终止)
void pthread_exit(void *retval);
retval:线程退出时的退出码信息
该函数无返回值,跟进程一样,线程结束的时候无法返回它的调用者(自身)。
pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时,线程函数已经退出了。
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> #define NUM 5 void* thread_run(void* args) { int num = *(int*)args; while(1){ printf("I am new thread [%d], I creat thread ID: %lu\n", num, pthread_self()); sleep(3); break; } //exit(111); //1 pthread_exit((void*)111); //2 } int main() { pthread_t tid[NUM]; for(int i = 0; i < NUM; i++){ pthread_create(tid + i, NULL, thread_run, (void*)&i); sleep(1); } void* status = NULL; for(int i = 0; i < NUM; i++){ pthread_join(tid[i], &status); printf("I am thread[%d], I am code: %d\n",i ,(int)status); } while(1){ printf("I am main thread\n"); sleep(1); } return 0; }
当我们在新线程中调用pthread_exit函数时,只会将新线程终止,不会影响到主线程;
当我们在新线程中调用exit函数时,直接将进程退出了;
线程终止的函数:pthread_ cancel(让别人终止)
int pthread_cancel(pthread_t thread);
参数说明:
- thread:被取消线程的ID。
返回值说明:
- 线程取消成功返回0,失败返回错误码。
线程是可以取消自己的,取消成功的线程的退出码一般是-1;
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> void* thread_run(void* args) { while(1){ printf("I am new thread [%s], I creat thread ID: %lu\n", (const char*)args, pthread_self()); sleep(1); } } int main() { pthread_t tid; pthread_create(&tid, NULL, thread_run, (void*)"thread 1"); sleep(3); printf("wait new thread...\n");//主线程休眠3秒后,提示在等待线程 sleep(10); printf("cancel wait new thread...\n");//主线等待10秒之后,提示取消等待 pthread_cancel(tid); //调用函数,取消新线程 void* status = NULL; pthread_join(tid, &status); //和获取新线程退出时的退出码 printf("I am thread: %d, I am code: %d\n",tid ,(int)status); return 0; }
通过运行结果发现,当主线程取消新线程后,新线程终止,返回的退出码是-1;当线程被取消的时候,如果是-1,就表明它是合法的,这里的-1具体是什么呢?
被取消的线程,退出值为常数PTHREAD_CANCELED的值是-1。可在头文件pthread.h中找到它的定义
grep -ER "PTHREAD_CANCELED" /usr/include/pthread.h
当然也可以通过新线程去取消主线程,这时候主线程就处于僵尸状态了;
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> pthread_t g_tid; void* thread_run(void* args) { while(1){ printf("I am new thread [%s], I creat thread ID: %lu\n", (const char*)args, pthread_self()); sleep(2); pthread_cancel(tid); } } int main() { g_tid = pthread_self(); pthread_t tid; pthread_create(&tid, NULL, thread_run, (void*)"thread 1"); sleep(30); void* status = NULL; pthread_join(tid, &status); //和获取新线程退出时的退出码 printf("I am thread: %d, I am code: %d\n",tid ,(int)status); return 0; }
我们一般都是用主线程去控制新线程,这才符合我们对线程控制的基本逻辑,虽然实验结果表明新线程可以取消主线程,但是并不推荐该做法。
5.分离线程
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
int pthread_detach(pthread_t thread); // thread:要分离的线程ID;
例如:我们让新线程分离
void* thread_run(void* args) { pthread_detach(pthread_self());//让新线程分离 while(1){ printf("I am new thread [%s], I creat thread ID: %lu\n", (const char*)args, pthread_self()); sleep(2); break; } return (void*)111; } int main() { pthread_t tid; int ret = 0; pthread_create(&tid, NULL, thread_run, (void*)"thread 1"); sleep(1); void* status = NULL; ret = pthread_join(tid, &status); printf("ret: %d, status: %d\n",ret ,(int)status); sleep(3); return 0; }
从运行结果以及进程监控脚本来看,新线程在分离后的2秒中后,自动退出了,主线程在等待新线程,并且想要获取到它的退出码"111",但是结果却是0;所以新线程在设置分离后,主线程就不能再去join,会失败,它的资源会被自动回收;
一般线程分离的场景是主线程不退出,新线程完成在对某项任务处理完毕后,自行退出;
6.线程ID及进程地址空间布局
pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和内核中的LWP不是一回事。前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID;
pthread_t 到底是什么类型呢?
它取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质
就是一个进程地址空间上的一个地址。
Linux中没有真正意义上的线程,它是用进程来模拟实现的,内核提供LWP,只需要管理LWP,用户使用的线程要由线程库自己来管理。如何管理?先描述,在组织,所以就在线程库中管理。
我们可以通过ldd来查看线程依赖的库:
我们知道动态库是当进程运行时被加载到共享区,此时该进程内的所有线程都可以看到这个动态库。
动态库加载后,要把动态库本身全部信息映射到主线程堆、栈之间的共享区(mmap)还有我们之前学习的共享内存;动态库里除了有代码,还有维护线程创建的数据结构;
每个线程运行时都要有自己的临时数据,意味着就要有私有栈结构;在地址空间中只有一个栈,这个栈是用来给主线程用的,不可能说创建多个线程之后和主线程公用一个栈;
动态库本身还承担了线程的组织、管理工作,即每一个线程地址空间有 struct_pthread(线程结构体) 、线程局部存储和线程栈,实现“先描述、再组织”,可见每一个线程都有自己的私有栈。所有新线程所使用的栈是在库当中的,由库来维护它的栈结构;
每一个新线程在共享区都有这样一块区域对其进行描述,因此我们要找到一个用户级线程只需要找到该线程内存块的起始地址,然后就可以获取到该线程的各种信息。
在内核中的LWP和struct_pthread(线程结构体)是1:1的;在用户层如果存在多个这样的结构体,为了和内核的LWP一一对应,那么在这些线程结构体中一定是包含了LWP。