前言
随着计算机技术的不断发展,多线程编程已经成为了程序设计中的一种重要方式。在Linux系统中,线程控制是多线程编程的核心内容之一。线程是一种轻量级的执行单元,它能够提高程序的并发性和响应速度,同时也能够有效地利用系统资源。
在Linux系统中,线程的控制主要涉及到线程的创建、等待、唤醒和销毁等方面。本文将详细介绍Linux中的线程控制机制,包括线程的状态转换、线程间的同步与互斥、线程的优先级以及线程的调度等方面。通过本文的学习,读者可以深入理解Linux中的线程控制原理,掌握线程编程的基本技巧和方法,为编写高效稳定的多线程程序打下坚实的基础。
1 线程创建
这里需要用到之前讲过的线程创建的函数 pthread_create函数
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
返回值与参数说明:
- 线程创建成功返回0 失败返回错误码 在线程库中 几乎所有的返回值都是成功返回0 失败返回错误码
- thread:获取创建成功的线程ID 该参数是一个输出型参数
- attr: 用于设置创建线程的属性 如果我们传入NULL则设置为默认属性
- start_routine:这是一个函数地址 传入我们想要这个线程执行的函数
- arg: 传给线程例程的参数 (默认是void* 类型 记得类型强转不然会报警告)
代码演示:
下面代码的作用是创建一个子线程,这个线程不停的打印参数发过去的消息,同时我们的主线程不停的打印另外的消息 在子线程中还调用了一个pthread_self()函数 它的作用是返回当前线程的id
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> void* run_thread(void* args) { char* msg = (char*)args; while(1) { printf("I'm a new pthread my tid is: %lu\n" , pthread_self()); sleep(1); } } int main() { pthread_t tid; pthread_create(&tid ,NULL ,run_thread ,(void*)"thread 1"); while(1) { printf("im main thread i create the new pid is:%lu\n", tid); sleep(1); } return 0; }
运行结果可能出现下面的情况:
这是因为程序找不到库文件导致的,我需要在编译的命令后面加上-l pthread这样就可以让它正常运行了
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的。
- 要使用这些函数库,要通过引入头文<pthread.h>。
- 链接这些线程函数库时要使用编译器命令的“-lpthread”选项。
结果如下:
可以观察到主线程创建的子线程的pid就是新线程的pid
并且做到之前从没做到过的事
两个死循环
但是线程的健壮性不够强,只要有一个线程崩溃它就都会崩溃
演示一下,让新线程运行一段时间后出现一个野指针问题 观察实验现象
演示代码如下:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> void* run_thread(void* args) { char* msg = (char*)args; while(1) { printf("im a new pthread my tid is: %lu\n" , pthread_self()); sleep(3); int* p = NULL; *p = 20; } } int main() { pthread_t tid; pthread_create(&tid ,NULL ,run_thread ,(void*)"thread 1"); while(1) { printf("im main thread i create the new pid is:%lu\n", tid); sleep(1); } return 0; }
结果如下
可以清楚看到新线程奔溃后,另一个线程也崩溃了
2 线程等待
在学习进程控制的时候讲过进程根据需要会进行进程等待 不然可能会造成僵尸进程的问题 而线程同样是通过PCB来保存数据的 所以线程也需要进行线程等待
在Linux操作系统中 我们可以使用pthread_join函数来实现
int pthread_join(pthread_t thread, void **retval);
返回值和参数说明:
- 返回值:线程等待成功就返回0 失败返回错误码
- thread: 被等待线程的ID
- retval:线程退出时的退出码信息 因为线程退出的返回值是一个void*类型的数据 所以我们要使用一个void**类型的数据去接收它
调用该函数的线程将被挂起等待直到ID为thread的线程终止
演示代码:
下面的代码的行为是:先创建一个新线程 这个线程会打印自己的线程id 之后休眠3秒并结束
主线程使用pthread_join函数接收它的返回值并打印
这里要注意的是:我们不能直接打印status 而是要将它强制转换为intptr_t再强制转换为int
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> void* run_thread(void* args) { printf("im new thread my tid is:%lu\n", pthread_self()); sleep(3); return (void*)101; } int main() { pthread_t tid; pthread_create(&tid, NULL, run_thread, (void*)"thread 1"); void* status = NULL; printf("waiting ...\n"); pthread_join(tid, &status); printf("wait success, exit code is:%d\n", (int)(intptr_t)status); return 0; }
结果如下:
可以看到主线程收到了新线程的退出码101
也就知道了pthread_join函数确实会进行线程等待 而调用这个函数的线程要等到新线程结束才继续执行
如果有异常情况需要处理吗?
答案是不需要 因为当一个线程出现异常情况的时候 操作系统就会发信号给进程 从而杀死进程
这里还要注意的当我们创建了多个线程时 只能一个个等待
3 线程终止
在前面线程等待中看到 线程结束用的是return
return 包含两种情况
- 一个是主线程使用代表主线程和进程退出
- 还有一个就是其他线程return 只代表线程退出
3.1 pthread_exit 线程退出函数
线程退出除了使用return以外 还有专门的函数pthread_exit函数
void pthread_exit(void *retval);
返回值和参数说明:
- 返回值:void 这很好理解 当线程不存在了 返回值也就没有了意义
- retval:线程退出时退出码信息
演示代码:
下面代码使用这个函数来退出线程 返回值是520
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> void* run_thread(void* args) { printf("im new thread my tid is:%lu\n", pthread_self()); sleep(3); pthread_exit((void*)520); } int main() { pthread_t tid; pthread_create(&tid, NULL, run_thread, (void*)"thread 1"); void* status = NULL; printf("waiting ...\n"); pthread_join(tid, &status); printf("wait success, exit code is:%d\n", (int)(intptr_t)status); return 0; }
结果如下:
可以看到退出码为设置的520
3.2 pthread_cancel 取消线程函数
int pthread_cancel(pthread_t thread);
返回值和参数说明:
- 返回值:成功返回0 失败返回-1
- thread:被取消线程的ID
- 这个函数既可以让线程取消自己 也可以让新线程取消主线程 但是不建议这样做 只建议使用主线程取消新线程
演示代码:
下面代码的作用是创建线程以后马上取消并且查看退出码
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> void* run_thread(void* args) { printf("im new thread my tid is:%lu\n", pthread_self()); while(1) { // 检查取消状态 if (pthread_cancel(pthread_self()) != 0) { printf("Thread is being cancelled\n"); pthread_exit((void*)-1); } sleep(3); } } int main() { pthread_t tid; pthread_create(&tid, NULL, run_thread, (void*)"thread 1"); void* status = NULL; printf("waiting ...\n"); sleep(5); // 等待5秒钟 pthread_cancel(tid); // 发送取消请求 pthread_join(tid, &status); if (status == PTHREAD_CANCELED) { printf("Thread was successfully cancelled\n"); } else { printf("Thread was not cancelled\n"); } return 0; }
结果如下:
PTHREAD_CANCELED是什么?
其实它就是 -1 只不过它是宏定义而已 所以用他来检查退出是否正常
查看一下在系统中是怎么定义的:
可以看到它实际上是被定义为**((void*)-1)**
需要注意的是默认创建线程的时候 线程的属性是joinable属性 它会导致线程退出的时候需要别人来回收自己的退出资源 线程退出了 但是线程在共享区中的空间还没有释放
4 线程分离
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
- 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
什么是线程分离?
一个线程如果被设置为分离属性 那这个线程在退出后 不需要其他执行流来回收该资源 而是由操作系统进行回收 它既可以是被线程组内的其他线程分离 又可以是线程自己分离 就和线程取消一样 当这个线程被设置为线程分离 就不能被等待了 这是相冲突的 它分离后就是"孤家寡人"了
我们可以使用pthread_detach函数来进行线程分离
int pthread_detach(pthread_t thread);
虽然前面说它分离后就变成“孤家寡人” 但是如果这个线程崩溃之后 进程同样会崩溃 那线程分离的意义在哪里?
线程分离的主要目的在于允许线程独立于创建它的进程运行,从而避免资源泄漏和提高系统性能。虽然线程分离后确实会变成“孤家寡人”,不再受到主线程的管辖,但是这也意味着对于主线程来说,无需等待这些“孤家寡人”线程的结束,从而能够更快地继续执行自己的任务。
当一个线程被分离后,线程终止时它所占用的系统资源会被自动释放,无需其他线程或者主线程专门等待或者回收资源,进而降低了系统负担。此外,线程分离还可以简化线程的管理,尤其是在多线程程序中,当有大量的线程需要创建时,分离线程可以减少对线程的管理和资源回收的工作量。
演示代码:
在创建完这个线程之后就分离它 并在三秒之后终止主线程和分离的线程
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> void* run_thread(void* args) { printf("im new thread my tid is:%lu\n",pthread_self()); printf("hello im new thread\n"); sleep(3); } int main() { pthread_t tid; pthread_create(&tid, NULL, run_thread, (void*)"thread 1"); printf("waiting ...\n"); pthread_detach(tid); sleep(3); return 0; }
结果:
5 线程ID及进程地址空间布局
- pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
- 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
- pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
- 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID
演示代码:
让主线程打印自己的进程ID、线程ID和新线程的ID 让新线程打印自己的进程ID和线程ID
#include<iostream> #include<pthread.h> using namespace std; #include<sys/types.h> #include<unistd.h> void* thread_run(void* arg) { while(1) { cout<<"i am:"<<(char*)arg<<"pid:"<<getpid()<<" "<<"my thread id is:"<<pthread_self()<<endl; sleep(1); } } int main() { pthread_t tid; int ret=pthread_create(&tid,NULL,thread_run,(void*)"thread 1"); if(ret!=0) { return -1; } while(1) { cout<<"i am main thread id:"<<pthread_self()<<" "<<"new thread:"<<tid<<" "<<"pid:"<<getpid()<<endl; sleep(2); } return 0; }
结果如下:
线程ID 本质就是一个进程地址空间上的一个地址 也就是虚拟地址
我们可以通过ldd命令查看一下编译好的文件
先介绍一下ldd
ldd 是一个在 Linux 系统中用于显示共享库依赖关系的命令。它列出了程序运行时所需的动态链接库及其路径。使用方法如下:
ldd [选项] 可执行文件或库
例如,要查看名为 code 的可执行文件的依赖关系,可以使用以下命令:
ldd code
我们所链接的线程库实际上就是一个动态库 既然是库肯定就是一个文件
当这个文件被加载到物理内存之后会经过页表的映射加载到进程地址空间的共享区中
我们之前说每个进程都有自己的栈空间 实际上这个栈空间和我们所想的是不一样的
线程采用的栈是在共享区开辟的 除此以外线程还有自己的struct pthread 当中包含了对应线程的各种属性
每个线程还有自己的线程局部存储 私有数据 还有线程被切换时的上下文数据
每当我们增加一个线程 在共享区中就会增加一个对应的结构体
在上面我们所用的各种线程函数 它的本质就是在库内部对线程属性进行各种操作 最后要执行的代码交给对应的内核级线程(轻量级进程)去执行即可 可以说对线程的管理就是基于共享区的
而这些个LWP就存在一个个结构体中
我们所看到的tid就是一个个结构体的虚拟地址
pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。