😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀
🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C++、数据结构、音视频🍭
🤣本文内容🤣:🍭介绍线程的基础知识 🍭
😎金句分享😎:🍭你不能选择最好的,但最好的会来选择你——泰戈尔🍭
⏰发布时间⏰:2024-03-16 13:03:47
本文未经允许,不得转发!!!
目录
- 🎄一、什么是线程
- 🎄二、多线程的优缺点
- ✨2.1 多线程的优点
- ✨2.2 多线程的缺点
- 🎄三、线程ID
- ✨3.1 gettid
- ✨3.2 syscall
- ✨3.3 查看线程ID
- 🎄四、/proc/PID/task/ 目录
- 🎄五、总结
🎄一、什么是线程
在了解线程之前,先看看程序和进程是什么?
程序
:程序或可执行文件是静态的静态的实体,只是一组指令的集合,没有执行的意义。
进程
:是运行之后的程序,是一个动态的实体, 有自己的生命周期。
线程
:线程是一个进程内部的一个控制序列,是操作系统进程调度器可以调度的最小执行单元。一个进程运行之后,就至少会有一个线程。只有一个线程的进程叫单线程进程
。
一个进程可能包含多个线程, 传统意义上的进程, 不过是多线程的一种特例, 即该进程只包含一个线程。在Linux系统开发中,也经常会使用到多线程编程,把进程设计成在同一时刻能够做多件事,每个线程处理各自独立的任务。
🎄二、多线程的优缺点
✨2.1 多线程的优点
- 同一个进程的线程会共享内存地址空间。同一个进程的多个线程共享一份全局内存区域, 包括初始化数据段、 未初始化数据段和动态分配的堆内存段。这使得创建或终止线程的时间要少于进程,共享数据比进程简单。
- 发挥多核优势, 充分利用CPU资源。如果存在多个相同的任务, 彼此之间并行不悖, 互不依赖(或者依赖性很小) , 那么启动多个线程并发处理, 是一个不错的选择。通过为每种事件类型的处理分配单独的线程,能够简化处理异步时间的代码。
- 有些问题可以通过将其分解从而改善整个程序的吞吐量。
- 交互的程序可以通过使用多线程实现相应时间的改善,多线程可以把程序中处理用户输入输出的部分与其它部分分开。
✨2.2 多线程的缺点
- 多线程的进程, 因地址空间的共享让该进程变得更加脆弱。多个线程之中, 只要有一个线程不够健壮存在bug,就可能导致整个进程崩溃。
- 线程模型作为一种并发的编程模型, 效率并没有想象的那么高, 会出现复杂度高、 易出错、 难以测试和定位的问题。
多线程编程很难将全部任务均等地分给每个进程;
多线程之间可能存在依赖关系,一个线程未完成某些操作之前,其他线程不应该运行。 - 多线程编程存在四个陷进:死锁(Dead Lock)、饿死(Starvation)、活锁(Live Lock)、竞态条件(Race Condition)
🎄三、线程ID
在Linux中, 目前的线程实现是Native POSIX Thread Library,简称NPTL。在这种实现下,线程又被称为轻量级进程(Light Weighted Process),每一个用户态的线程,在内核之中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体) 。
对于单线程进程来说,一个进程对应内核里的一个进程描述符, 对应一个进程ID。
多线程的进程, 又被称为线程组, 线程组内的每一个线程在内核之中都存在一个进程描述符(task_struct) 与之对应。struct task_struct {... pid_t pid; pid_t tgid; ... struct task_struct *group_leader; ... struct list_head thread_group; ... }
内核的
struct task_struct
结构体中的两个ID字段:
pid
:pthread ID,表示线程ID;
tgid
:意思是 Thread Group ID,表示线程组ID,对应的是进程ID。
✨3.1 gettid
Linux中提供系统调用 gettid
来获取调用者的线程ID。但是,这个系统调用没有glibc封装,使用glibc的编译器时只能使用 syscall
函数来调用 gettid
系统调用;
gettid
函数原型:
#include <sys/types.h>
pid_t gettid(void);
Note: There is no glibc wrapper for this system call; see NOTES.
函数描述:gettid() 返回调用方的线程ID(TID)。在单线程进程中,线程ID等于进程ID(PID,由getpid()返回)。在多线程进程中,所有线程都有相同的PID,但每个线程都有一个唯一的TID。
✨3.2 syscall
因为在 glibc 的编译器没有gettid
函数,所以只能使用 syscall
函数来获取线程ID,syscall
函数原型如下:
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <unistd.h>
#include <sys/syscall.h> /* For SYS_xxx definitions */
int syscall(int number, ...);
函数描述:syscall()是一个小型库函数,用于系统调用,该系统调用的汇编语言接口具有指定的数字和指定的参数。例如,当调用C库中没有包装函数的系统调用时,使用syscall()非常有用。
下面是一个例子,创建4个线程,分别获取自己的线程ID,例子中用到一些陌生的函数如:pthread_create,会在后面介绍线程的文章讲解。这个例子可以使用命令gcc gettid.c -lpthread
进行编译,-lpthread
表示要链接线程库。
// gettid.c
#include <stdio.h>
#include <pthread.h>
#include <sys/syscall.h>
#include <unistd.h>
void *func(void *arg)
{
int *pI = arg;
pid_t tid = /*getpid()*/syscall(SYS_gettid);
printf("this thread is num %d , tid=%d\n", *pI, tid);
while(1); // 避免线程退出
return NULL;
}
int main()
{
int i=0;
int args[4] = {0,};
for(i=0; i<4; i++)
{
pthread_t threadId;
args[i] = i;
pthread_create(&threadId, NULL, func, &args[i]);
}
while(1); // 避免线程退出
return 0;
}
运行结果:
✨3.3 查看线程ID
在Linux下,使用
ps
命令中的-L
选项,会显示出线程的如下信息。
- LWP: 线程ID, 即gettid() 系统调用的返回值。
- NLWP: 线程组内线程的个数
使用ps
命令查看上面程序的线程,因为ps命令输出太多了,这里使用grep过滤一下,使用命令ps -eLf | grep -e "PID" -e "a.out"
,只显示带有PID
、a.out
关键字的行,结果如下:
可以看到a.out有5个线程,主线程是PID和LWP列一样的线程,即113178
那一行。
ps命令还可以使用-T
选项来显示SPID
列,表示线程ID。如果想知道更多的ps命令,可以使用ps --help
查看。
🎄四、/proc/PID/task/ 目录
procfs在task下会给进程的每个线程建立一个子目录, 目录名为线程ID。
已知某进程的进程ID,我们就可以通过/proc/PID/task/目录下的子目录来查看该进程的线程个数和线程ID。
以上个小节进程ID为113178
的进程举例,也是可以看到对应目录下有5个由线程ID命令的子目录:
🎄五、总结
👉本文介绍线程的基础知识,包括线程优缺点、线程ID、/proc/PID/task/ 目录等。
需要强调的一点是,线程和进程不一样, 进程有父进程的概念, 但在线程组里面, 所有的线程都是对等的关系。
- 并不是只有主线程才能创建线程, 被创建出来的线程同样可以创建线程。
- 不存在类似于fork函数那样的父子关系, 大家都归属于同一个线程组, 进程ID都相等, group_leader都指向主线程, 而且各有各的线程ID。
- 并非只有主线程才能调用pthread_join连接其他线程, 同一线程组内的任意线程都可以对某线程执行pthread_join函数。
- 并非只有主线程才能调用pthread_detach函数, 其实任意线程都可以对同一线程组内的线程执行分离操作
如果文章有帮助的话,点赞👍、收藏⭐,支持一波,谢谢 😁😁😁
参考资料:
《Linux环境编程:从应用到内核》