当前位置: 首页 > news >正文

[Linux系统编程]多线程

多线程

  • 1. 线程
    • 1.1 线程的概念
    • 1.2 进程与线程对比
    • 1.3 轻量级进程
  • 2. Linux线程控制
    • 2.1 POSIX 线程(pthread)
    • 2.2 线程ID、pthread_t、和进程地址空间的关系
      • 2.2.1 pthread_self
      • 2.2.2 pthread_create
      • 2.2.3 pthread_join
      • 2.2.4 线程终止的三种方式
      • 2.2.5 pthread_t
    • 2.3 线程的两种状态


1. 线程

1.1 线程的概念

线程是什么?

在操作系统中,线程(Thread) 是程序执行的最小单位。简单来说,一个进程(Program)可以包含一个或多个线程。每个线程都是独立的执行流,它们共享进程的资源,比如内存、文件描述符等。

线程的定义:

线程是一个进程内部的执行路线,也就是说,一个进程可以有多个线程,它们在进程内部并发执行。线程在进程内部执行,即线程在进程的地址空间中运行。

每个线程有自己独立的执行栈(用于保存局部变量、函数调用等信息),但是它们共享进程的其他资源,比如内存、文件句柄等。

换句话说,线程是在进程地址空间内运行的,它们依赖进程的资源,但执行时是相互独立的。

为什么有线程?

进程通常需要做很多事情,而这些事情往往是可以并行执行的。为了更好地利用计算机的资源(特别是多核 CPU),操作系统引入了线程,使得同一个进程内部的多个任务可以同时执行


1.2 进程与线程对比

线程和进程的区别:

进程:是程序执行的基本单位 (进程是基本单位、线程是最小单位),每个进程都有独立的地址空间(内存)。一个进程可以包含多个线程。

线程:是进程内的执行单元,同一个进程内的多个线程共享该进程的内存和资源。线程是“轻量级”的,它们比进程更容易创建和销毁。

在 CPU 眼里,线程相对比进程来说更加轻量化,也就是说,线程的管理和切换消耗的资源更少。

举一个例子说明:

进程(Process) 就像是一个厨房,厨房里有许多厨房用具(锅、刀、调料等)和工作人员(厨师)。

线程(Thread) 就像是厨房中的每个厨师,每个厨师有自己的工作台(自己的执行栈),但是他们共用厨房(进程的资源)中的工具和食材(内存、文件句柄等)。此处所说的句柄:是一个用于标识资源的抽象引用。它并不直接代表资源本身,而是一个指向资源的“指针”或“索引”,可以用来操作、管理或访问该资源。文件句柄(File Handle)正是操作系统用来标识和访问文件的一种方式。

如果厨房(进程)只有一个厨师(线程),那只能做一件事。但是,如果厨房有多个厨师(多个线程),他们就可以同时做不同的任务,比如一个厨师切菜,一个厨师煮汤,另一个厨师炒菜,这样就能加快整个烹饪的速度。

线程在进程内部如何运行:
每个进程至少有一个线程,这个线程通常被称为主线程。主线程负责执行程序的主要任务。进程启动后,主线程会按顺序执行代码。然而,在复杂的应用程序中,通常会创建多个线程,以实现并发或并行的任务处理。

此时重新理解进程:站在内核角度,承担分配系统资源的基本单位,叫做进程。

曾经所学的进程都是只有一个task_struct; 之前所谈论的进程内部只有一个执行流。

例如:

网络服务程序:一个线程负责接收网络请求,另一个线程处理请求,第三个线程负责返回响应。这样多个任务就可以并行执行,提高程序效率。

多线程网页浏览器:一个线程负责渲染网页,另一个线程处理用户输入,还有线程处理文件下载。

综上所述,对比进程与线程:

进程就像是一个正在运行的应用程序。比如当你打开一个浏览器、文档编辑器或者游戏时,每一个应用程序都有一个自己的进程。进程是程序的实例,它拥有自己的内存空间、数据、资源,它就像一个独立的“工作单元”。

线程是进程中的执行单元,你可以理解为进程中的“工作者”。一个进程可以包含多个线程,这些线程共享进程的资源(如内存、文件等)。线程负责执行进程中的具体任务。每个线程都有自己的任务和执行路线,但它们都在同一个进程内工作,共享同一份“材料”(内存和其他资源)。

进程是资源分配的基本单位、线程是调度的基本单位

重点:

线程共享进程数据,但也拥有自己的一部分数据 , 线程共享进程数据时,实际上是在说多个线程之间会共享同一进程中的资源,这些资源包括内存、文件描述符、环境变量等。尽管每个线程有自己的执行栈、寄存器等独立资源,它们在同一个进程中共享大量的其他资源。

线程拥有自己的一部分数据如下:

线程ID(操作系统用来区分不同线程)

一组寄存器(寄存器中保存了线程的运行状态即上下文,上下文切换时,操作系统会保存当前线程的寄存器状态,并加载其他线程的状态,从而保证线程能从上次中断的位置继续执行)

(线程运行会产生临时数据,需要将数据进行压栈)

errno

信号屏蔽字:每个线程有自己的信号屏蔽字,用于管理该线程接收的信号。信号屏蔽字决定了线程在某一时刻能接收哪些信号。

调度优先级:每个线程都有一个调度优先级,用于决定线程被调度执行的顺序。高优先级的线程会被操作系统优先执行,而低优先级的线程可能需要等待。

进程与线程区别要答出线程具有寄存器和栈!

进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到。

除此之外,各线程还共享以下进程资源和环境:

文件描述符表
每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
当前工作目录
用户id和组id

线程在进程内部如何运行:

每个进程至少有一个线程,这个线程通常被称为主线程。主线程负责执行程序的主要任务。进程启动后,主线程会按顺序执行代码。然而,在复杂的应用程序中,通常会创建多个线程,以实现并发或并行的任务处理。

线程的好处:

提高效率:通过多线程并行执行任务,可以充分利用 CPU 资源,尤其是多核 CPU。

响应性:多线程程序可以在一个线程执行耗时任务时,继续处理其他任务,比如图形界面程序在后台加载数据时,界面依然可以响应用户操作。

资源共享:同一个进程的多个线程可以共享进程的内存和文件句柄等资源,减少了内存开销。

线程的缺点:

同步问题:多个线程同时访问共享数据时,如果没有适当的同步机制,可能会导致数据的不一致。

管理复杂性:多线程程序的设计和调试比单线程更复杂,因为需要考虑线程的创建、销毁、调度、同步等问题。

上下文切换开销:虽然线程比进程更轻量,但频繁的线程上下文切换仍然会带来一定的性能开销。


1.3 轻量级进程

线程的实现:线程是如何执行的?

线程的执行由操作系统调度,操作系统会将 CPU 时间片分配给不同的线程。在多核处理器的情况下,操作系统可以将不同的线程分配到不同的 CPU 核心上并行执行,这样可以极大地提高程序的运行效率。

在Linux中,站在CPU的角度,能否识别task_struct是进程还是线程?——不能,也不需要,CPU只关心一个一个的独立执行流。

实际上,Linux中并不存在真正的多线程。
如果支持真的线程,当线程足够多的时候,OS要不要管理线程?——要管理线程。(Windows等普通操作系统)
在Linux下,是用进程模拟的线程。这句话的意思是,在Linux操作系统中,虽然我们可以通过线程(例如使用 pthread 库等)来创建和管理多个线程,但在底层实现上,Linux 其实并没有真正的多线程(特别是在内核层面)。具体来说,Linux 的线程模型与传统的多线程系统存在一些区别,以下是详细解释。

在 Linux 中,操作系统本身并没有将线程视为与进程完全不同的实体。Linux 内核将线程视为进程,所以线程和进程在内核中并没有本质的区别。

线程的实现:在 Linux 中,线程只是特殊类型的进程,它们共享同一进程的地址空间(即共享内存、文件描述符等),但它们有自己的栈(stack)和程序计数器(PC)等执行状态。每个线程都有一个单独的调度实体,称为轻量级进程。

进程和线程的区别:虽然线程和进程在用户空间看起来有所区别,但在 Linux 内核中,每个线程都拥有一个独立的 PID(进程ID)。这意味着线程也会有进程调度、调度队列、上下文切换等管理特性。不同的线程拥有不同的线程ID,但它们是同一个进程的不同执行流。

Linux中的所有执行流,都叫做轻量级进程Linux 的多线程是通过“轻量级进程”来实现的,而不是严格意义上的“真正的线程”。

在内核层面,每个线程其实都是一个进程,它们共享同一进程的资源,但有各自的执行状态。所以,尽管我们在用户空间创建线程,但在内核中,它们被视为多个进程在进行调度,这也是为什么这句话说“Linux 中并不存在真正的多线程”。

Linux 的线程实现并没有像其他操作系统那样为线程提供独立的调度和管理机制,而是通过将线程作为进程来进行统一的管理和调度的。所以,虽然可以创建多个线程,它们实际上和进程在内核中没有实质性的区别。

既然Linux没有真正意义的线程,所以Linux也绝对没有真正意义上的线程相关的系统调用。在Linux中,提供创建轻量级进程的接口,即创建进程,共享空间。

vfork();
创建父子共享空间

在这里插入图片描述
上面所的是站在内核角度。站在用户角度,如何创建进程?——原生线程库的方案创建。基于轻量级进程的系统调用,在用户层模拟实现一套线程接口 pthread

轻量级进程(LWP,Lightweight Process) 是一种特殊的进程类型,通常用于表示那些在操作系统中共享部分资源但具有独立执行流的进程。在 Linux 和其他类 Unix 系统中,线程就可以被视为轻量级进程。简单来说,轻量级进程就是线程,但它们的管理方式更接近进程,而不像传统意义上的线程那样完全独立。

轻量级进程的特点:

1、共享资源:

轻量级进程共享父进程的资源,比如内存空间、文件描述符等。
但每个轻量级进程有自己独立的执行栈、程序计数器、寄存器等执行状态。
补充:轻量级进程(或线程)会共享父进程的资源(比如地址空间),但每个轻量级进程(线程)有独立的执行栈和 CPU 状态。

2、独立调度:

每个轻量级进程可以独立调度执行,操作系统调度器会给它们分配 CPU 时间片。轻量级进程可以并发执行,尽管它们共享内存和文件等资源。
因为它们是“轻量级”的,相对于传统的进程,创建和销毁的开销较小。

3、独立执行:

轻量级进程有独立的执行路径(线程),所以它们在执行时,虽然共享资源,但每个线程的执行是独立的。

轻量级进程的优势:

1、更低的开销:

相比传统的进程,轻量级进程的创建和销毁开销要小 得多,因为它们共享进程的资源。

2、高效的并发执行:

轻量级进程通过共享资源,可以更加高效地进行并发执行,特别是在多核 CPU 环境下,多个线程可以并行执行,提升程序的执行效率。

3、快速上下文切换:

线程之间的上下文切换比进程的上下文切换要快,因为它们共享同一块内存和资源,减少了内存切换和状态保存的开销。


2. Linux线程控制

2.1 POSIX 线程(pthread)

Linux 线程控制(使用 POSIX 线程库,也就是 pthread)
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
要使用这些函数库,要通过引入头文<pthread.h>
链接这些线程函数库时要使用编译器命令的“-lpthread”选项

创建线程的核心函数 pthread_create:int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void*), void *arg);✅ 返回值:
成功:返回 0
失败:返回错误码(注意:不是 errno,而是直接返回一个错误代码!)

在这里插入图片描述

在这里插入图片描述

错误处理:

int ret = pthread_create(...);
if (ret != 0) {fprintf(stderr, "pthread_create : %s\n", strerror(ret));
}与老式函数不同,pthread 系列函数不会设置全局变量 errno,而是直接通过返回值返回错误码。
使用 strerror(ret) 打印错误信息,是推荐做法。
#include <unistd.h>      // 提供 sleep 函数
#include <stdlib.h>      // 提供 exit 等函数
#include <stdio.h>       // 提供 printf、fprintf
#include <string.h>      // 提供 strerror
#include <pthread.h>     // pthread 线程库头文件void *rout(void *arg) {for( ; ; ) {printf("I'm thread 1\n");sleep(1); // 每秒打印一次}
}int main( void )
{pthread_t tid;      // 声明线程 IDint ret;// 创建新线程,执行 rout 函数if ( (ret = pthread_create(&tid, NULL, rout, NULL)) != 0 ) {fprintf(stderr, "pthread_create : %s\n", strerror(ret));exit(EXIT_FAILURE);}// 主线程也无限循环打印for( ; ; ) {printf("I'm main thread\n");sleep(1);}
}

结果:

I'm main thread
I'm thread 1
I'm main thread
I'm thread 1
...

这说明程序有两个线程在并发运行!

我们再来看看在多线程程序中打印 pid 和 ppid 是否一样。

#include <sys/types.h>
#include <unistd.h>// 在 rout 函数中:
printf("Thread: PID = %d, PPID = %d\n", getpid(), getppid());// 在 main 函数中:
printf("Main  : PID = %d, PPID = %d\n", getpid(), getppid());
Main  : PID = 12345, PPID = 12200
Thread: PID = 12345, PPID = 12200pid 和 ppid 在主线程和子线程中是一样的。

因为:

线程(Thread)不是独立的进程,而是进程内部的执行流;
所有线程共享同一个进程号(PID);
所有线程的 父进程号(PPID) 也一样;
创建线程用的是 pthread_create(),不是 fork(),不会创建新进程。

$ ps -aLps       # 进程状态查看命令-a      # 显示所有终端下的进程(包括其他用户的)-L      # 显示所有线程(轻量级进程)PID   LWP TTY          TIME CMD
12345 12345 pts/0    00:00:00 my_app
12345 12346 pts/0    00:00:00 my_app
12345 12347 pts/0    00:00:00 my_app同一个进程(PID 相同),有多个 LWP(线程 ID 不同)
每一行代表一个线程

因此OS调度的基本单位是轻量级进程 -> LWP 因为PID可能相同

在线程模型下:

每个线程都像是一个“缩小版”的进程:
有自己的 线程ID(LWP ID)
有自己的 栈、寄存器、调度信息
但它们共享所属进程的资源(内存、打开的文件、全局变量等)
所以说线程是 轻量级进程,而一个真正的进程是 重量级的(Heavyweight Process)。

在这里插入图片描述

Linux中,应用层的线程与内核的LWP是1:1的。

创建一批线程:
在这里插入图片描述


2.2 线程ID、pthread_t、和进程地址空间的关系

2.2.1 pthread_self

这是 POSIX 线程库提供的一个函数,用来获取当前线程的标识符。
此处pthread_self代表的是用户级原生线程库的线程ID,与LWP的值是不等的pthread_t pthread_self(void);

📌 它的作用:
返回调用它的当前线程自身的pthread_t ID。
和 pthread_create() 创建线程时生成的 pthread_t 是一样的类型。
通常我们用它来在当前线程中获取自己的 ID。

🧵 为什么需要它?
你创建线程时,系统会给你一个 pthread_t 类型的变量,作为线程标识符:

2.2.2 pthread_create

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);pthread_t *thread:是一个 输出参数,pthread_create 通过这个参数把 新线程的 ID(pthread_t 类型)返回给你。

👇 所以:

pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);定义了一个 tid 变量
把它的地址 &tid 传给 pthread_create
pthread_create 内部会 填充 tid 变量的值,让它代表这个新创建的线程

但是在线程函数 thread_func 中,你是没办法直接用这个 tid 的,因为它是 main 函数传进去的。
这时候,如果想获取 我是谁?我是哪个线程? —— 用 pthread_self() 就行了。

#include <pthread.h>
#include <stdio.h>void* thread_func(void* arg) {pthread_t self_id = pthread_self();printf("Hello from thread! My pthread_t ID is %lu\n", (unsigned long)self_id);return NULL;
}int main() {pthread_t tid;pthread_create(&tid, NULL, thread_func, NULL);pthread_join(tid, NULL);return 0;
}
Hello from thread! My pthread_t ID is 139812356818688

🔍 和系统线程ID(TID)的区别?
pthread_self() 返回的是 pthread 库的线程 ID(pthread_t),不是内核线程 ID(TID)。
如果想获取系统层面的线程 ID(TID),用:

#include <sys/syscall.h>
#include <unistd.h>pid_t tid = syscall(SYS_gettid);

主线程不退出,新创建的线程跑完了,进程会怎么样?

✅ 进程不会退出,直到所有线程都结束(包括主线程)。

在 Linux 中,一个进程由所有线程共同构成,只有最后一个线程(通常是主线程)退出或整个进程被 exit() 结束,这个进程才真正消失。

在这里插入图片描述

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>void* thread_func(void* arg) {printf("Child thread starts running\n");sleep(1); // 模拟耗时操作printf("Child thread ends\n");return NULL;
}int main() {pthread_t tid;pthread_create(&tid, NULL, thread_func, NULL);// 主线程不退出,但也不管子线程printf("Main thread sleeping...\n");sleep(5);printf("Main thread ends\n");return 0;
}

输出:

Main thread sleeping...
Child thread starts running
Child thread ends
Main thread ends虽然子线程1秒就结束了,但主线程活着,进程还没完事。

💥 那如果主线程用 return 或 exit() 直接退出呢?

int main() {pthread_t tid;pthread_create(&tid, NULL, thread_func, NULL);// 主线程直接 return 0;return 0;
}

2.2.3 pthread_join

❌ 子线程可能还没跑完,进程就直接终结了。

所以更安全的做法是:
用 pthread_join() 等待子线程。
或者主线程用 pthread_exit(NULL); 退出自身而不是整个进程。

🔧 pthread_join() 是什么?
pthread_join() 是用于等待一个线程结束的函数。

主线程(或者其他线程)可以通过它等待 指定的子线程 结束,再继续执行后续操作。

int pthread_join(pthread_t thread, void **retval);参数	    说明
thread	要等待的线程的 pthread_t ID
retval	用于接收线程退出时返回的值(也可以为 NULL
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>void* thread_func(void* arg) {printf("Child thread running\n");sleep(2);printf("Child thread exiting\n");return "Thread done";
}int main() {pthread_t tid;void* result;pthread_create(&tid, NULL, thread_func, NULL);printf("Main thread waiting for child...\n");pthread_join(tid, &result);  // 等待子线程结束printf("Child thread returned: %s\n", (char*)result);printf("Main thread ends\n");return 0;
}
Main thread waiting for child...
Child thread running
Child thread exiting
Child thread returned: Thread done
Main thread ends

✨ 为啥要用 pthread_join()?

✅ 等待线程执行完毕,实现线程同步
✅ 获取线程的返回值
✅ 防止产生“僵尸线程”(类似僵尸进程,线程结束但资源未释放)

🚫 不用 pthread_join() 会怎样?

1、主线程可能直接退出,导致子线程还没执行完就被强制结束。
2、子线程虽然结束了,但系统资源没有被回收,可能造成资源泄露(内存等)。

🔁 多个线程 join 的情况?

你可以对每个线程都 pthread_join(),比如:

pthread_t tid1, tid2;
pthread_create(&tid1, NULL, func1, NULL);
pthread_create(&tid2, NULL, func2, NULL);pthread_join(tid1, NULL);
pthread_join(tid2, NULL);

这两个 join 的顺序会影响主线程等待的顺序。

🧠 小知识:

一个线程只能被 pthread_join() 一次
如果两个线程都尝试 join 同一个线程,会导致未定义行为
如果你不需要返回值,可以传 NULL 给第二个参数

🌰 举个例子说明这个小知识:

创建一个线程 A:

pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);

然后有 两个线程,比如主线程 和 线程 B,都想 join A:

pthread_join(tid, NULL); // 主线程
pthread_join(tid, NULL); // 线程 B

❌ 这是不被允许的!会出现未定义行为:可能崩溃、卡死、返回错误、不确定结果……总之行为不可预测。

🧠 为什么不能多次 join 同一个线程?

因为 pthread_join 的作用是:

等待目标线程结束
清理该线程的资源

线程一旦 join 成功,系统就会认为这个线程 已经被“处理掉”了,资源释放完毕,再也不应该动它。

如果另一个线程再来 join 它,系统已经无法确定要等待什么、释放什么,就会造成混乱 —— 所以是 未定义行为。

🔧 如果确实想多个线程“等待”一个线程怎么办?

就不能用 pthread_join() 了!可以考虑:

✅ 用同步机制代替:

比如:
条件变量(pthread_cond_wait):线程完成时广播信号
信号量
互斥锁 + 标志变量
线程池场景下用任务状态标识

2.2.4 线程终止的三种方式

方法1:线程函数中 return

void* my_thread(void* arg) {printf("Thread running...\n");return "Thread return";  // ✅ 相当于 pthread_exit("Thread return")
}

实际上就是函数正常返回了;

系统会自动调用 pthread_exit();
注意:返回值是传递给 pthread_join() 使用的;

❌ 不能用于主线程:主线程 return 会导致整个进程结束(相当于调用 exit())。

方法2:调用 pthread_exit()

void pthread_exit(void *retval);retval:是线程返回给调用 pthread_join() 的线程的值(可以是字符串、结构体等)。
没有返回值,线程退出后不再运行。

pthread_exit() 用于让线程主动结束自己,并返回一个值(通常是一个指针),供其他线程(通常是主线程)通过 pthread_join() 获取。

这是线程主动退出的标准做法:

void* my_thread(void* arg) {printf("Doing stuff...\n");pthread_exit("I'm done!");
}

特点:

线程可以随时主动退出;
退出时可以返回一个指针(让 pthread_join() 拿到);
返回值通常是 malloc 或全局变量分配的空间,不能是局部变量!

❌ 错误示例:

void* my_thread(void* arg) {char result[20] = "Oops"; // 栈上分配pthread_exit(result);     // ❌ 线程退出后,这块内存就没了
}

retval 不能是局部变量的地址,因为线程退出后栈会销毁,指针就失效了。
通常用 malloc 动态分配,或者用全局变量。

✅ 方法3:调用 pthread_cancel() 取消另一个线程
一个线程可以取消另一个正在运行的线程:pthread_cancel() 用于请求取消另一个线程,等价于对那个线程发起“你可以结束了”的请求。

int pthread_cancel(pthread_t thread);thread:要被取消的线程 ID。
返回值为 0 表示成功,非 0 表示失败。
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
pthread_cancel(tid);  // 强行终止

注意事项:

被取消的线程必须允许响应取消请求(默认是允许的);
线程什么时候真正结束?取决于它何时遇到一个“取消点”;
比如 sleep(), read(), pthread_join() 等都是取消点;
被取消的线程会调用 pthread_exit(PTHREAD_CANCELED);
如果你在 pthread_join() 得到 PTHREAD_CANCELED,说明这个线程被取消了。

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>void* my_thread(void* arg) {while (1) {printf("Working...\n");sleep(1);  // sleep 是一个“取消点”}return NULL;
}int main() {pthread_t tid;pthread_create(&tid, NULL, my_thread, NULL);sleep(3);printf("Main thread cancelling the worker thread\n");pthread_cancel(tid);pthread_join(tid, NULL);printf("Worker thread canceled.\n");return 0;
}

两者对比总结:

在这里插入图片描述

2.2.5 pthread_t

从表面看,它只是一个“线程 ID 类型”,你会这么写:

pthread_t tid;
但它 具体是什么类型,其实是 实现相关的
也就是说它在不同的操作系统、库中可能不一样。

🔧 在 Linux 中(使用 NPTL 的 glibc 实现):

// pthreadtypes.h 中
typedef unsigned long int pthread_t;

也就是说在大多数 Linux 下,pthread_t 实际上就是一个无符号长整型(unsigned long int)。

但这只是具体实现而已,标准并没有强制它一定是整数,也可能是结构体、指针、甚至自定义类型。

🧠 那它和 PID 有什么关系?

这就牵扯到多线程在 Linux 中的实际实现了:

🧩 Linux 下的线程 ≈ 轻量级进程(LWP)

Linux 用 轻量级进程(LWP) 来实现线程,每个线程在内核中其实是一个独立的“调度单位”,都有自己的 PID(准确地说,是 TID,Thread ID)。

可以用下面命令查看:

ps -eLf | grep ./your_program

会看到同一个进程下有多个 LWP(线程),每个线程会有:
一个公共的 PID(属于整个进程)
一个独立的 LWP ID(= TID,线程在内核中的 ID)

🔗 pthread_t 和 TID(线程ID)不是一回事!

pthread_t 是 线程库层面的 ID,用于 pthread 系列函数之间的调用,比如 pthread_join(tid)
TID 是 内核层面的 ID,你可以通过 gettid()(Linux 系统调用)拿到它。

#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>void* thread_func(void* arg) {pthread_t tid = pthread_self();   // 线程库层面IDpid_t sys_tid = syscall(SYS_gettid);  // 内核层面线程ID(TID)printf("pthread_t: %lu\n", tid);printf("syscall gettid (TID): %d\n", sys_tid);return NULL;
}int main() {pthread_t t;pthread_create(&t, NULL, thread_func, NULL);pthread_join(t, NULL);return 0;
}

输出:

pthread_t: 140145145444096
syscall gettid (TID): 20123

两个值不一样!一个是用户态线程库内部使用的“ID”,一个是内核调度线程用的“真实ID”。

📌 一句话记忆:

pthread_t 是用户态线程库中的“身份证”,而 TID 是 Linux 内核中的“真实编号”。


2.3 线程的两种状态

在 Linux 中,线程(Thread)其实是进程资源下的一部分。当我们创建一个线程并且让它运行时,系统会为它分配资源:比如线程栈空间、线程控制块(TCB)等。

如果你创建了一个线程,但在线程执行完后没有及时把它的资源释放掉,它就会变成一个“僵尸线程”,占用系统资源,这就叫 资源泄露。

所以线程运行完后,要么:

你来“收尸” —— 用 pthread_join(),显式地把它的资源收回来;
让它“自杀” —— 用 pthread_detach(),线程结束时自己把自己清理掉。

🔧 一、线程的两种状态:Joinable vs Detached

1、Joinable(可连接)线程:

默认状态
创建的线程执行完后,系统不会自动释放它的资源(如栈空间、线程描述符)。
需要用 pthread_join() 去“收尸”。
如果不 join,线程结束后,它的资源就不会释放,造成内存泄漏(资源泄漏)。

2、Detached(分离)线程:

设置为分离态后,线程一旦执行完毕,系统会自动回收资源。
不需要也不能用 pthread_join()。
如果你尝试 join 一个分离线程,会返回错误。

void *thread_run( void * arg ) {pthread_detach(pthread_self()); // 让自己变成一个分离线程printf("%s\n", (char*)arg);     // 打印传入的字符串return NULL;                    // 线程结束
}int main(){pthread_t tid;if ( pthread_create(&tid, NULL, thread_run,"thread1 run...") != 0 ) {printf("create thread error\n");return 1;}sleep(1);  // 让线程有机会先分离自己}

这里的重点:

用 pthread_self() 获取当前线程的 ID;
用 pthread_detach() 把自己设置成分离状态;

意思是:我这个线程自己跑完后,不需要别人等我,系统直接帮我销毁我用过的内存等资源。

✅ 线程分离(pthread_detach())是干什么用的?

线程分离是为了让线程在结束时,系统能自动清理资源,而不用再让其他线程去 pthread_join() 它。

🔍 为什么要分离线程?

默认情况下,线程是 joinable 的:
如果你不 pthread_join() 它,它退出后会变成“僵尸线程”(资源没被系统清理);

如果很多线程都不被 join,会造成内存泄漏、线程资源枯竭,进程可能崩溃!

所以:
如果你根本不打算关心线程什么时候结束、返回了什么值,那就应当 分离这个线程,让系统自动处理。

分离线程不能被 join,否则系统会报错,返回错误码 EINVAL(非法参数)。

❗ 不能同时 join 和 detach:
一个线程,要么 join,要么 detach;
不能两个都来,否则就是“死两次”——系统崩溃 or 出错。

在这里插入图片描述

🔍 分离线程实际应用场景
服务端:来了一个客户端连接,就起一个线程处理这个连接,不需要再 join;
后台任务:比如记录日志、上传文件;
任务型线程:只负责一次性跑完某段逻辑,跑完就结束。

🧾 总结一句话:
Joinable线程跑完要有人“收尸”(join),分离线程跑完“自我销毁”(detach)——如果没人收尸又不分离,那就造成资源泄漏!

相关文章:

[Linux系统编程]多线程

多线程 1. 线程1.1 线程的概念1.2 进程与线程对比1.3 轻量级进程 2. Linux线程控制2.1 POSIX 线程&#xff08;pthread&#xff09;2.2 线程ID、pthread_t、和进程地址空间的关系2.2.1 pthread_self2.2.2 pthread_create2.2.3 pthread_join2.2.4 线程终止的三种方式2.2.5 pthre…...

进程状态(运行 阻塞 僵尸)及其场景分析

【Linux学习笔记】Linux基本指令及其分析 &#x1f525;个人主页&#xff1a;大白的编程日记 &#x1f525;专栏&#xff1a;Linux学习笔记 前言 哈喽&#xff0c;各位小伙伴大家好!上期我们讲了进程PCB 今天我们讲的是进程状态(运行 阻塞 僵尸)及其场景分析。话不多说&#…...

程序化广告行业(67/89):DMP系统标签制作与人群拓展深度解析

程序化广告行业&#xff08;67/89&#xff09;&#xff1a;DMP系统标签制作与人群拓展深度解析 大家好&#xff01;在之前的分享中&#xff0c;我们对程序化广告的多个关键环节进行了探讨。今天&#xff0c;咱们继续深入了解程序化广告中的DMP系统&#xff0c;聚焦于标签制作和…...

【QT】QPixmap QImage QBitmap QPicture

文章目录 **1. QPixmap****特点****典型应用场景****示例** **2. QImage****特点****典型应用场景****示例** **3. QBitmap****特点****示例** **4. 三者的主要区别****5. 如何选择&#xff1f;****使用 QPixmap 的情况****使用 QImage 的情况****使用 QBitmap 的情况** **6. 相…...

如何开通google Free Tier长期免费云服务器(1C/1G)

Google宣布的一项政策&#xff0c;为标准层级的网络提供每地域200G的免费流量。两项政策结合&#xff0c;于是便可以得到一台1核心、1G内存、30G磁盘、200G流量的小云服务器&#xff0c;可玩性大大提高。这篇文章就分享一下如何正确开机&#xff0c;避免产生额外的费用。 免费…...

Kaggle房价预测

实战 Kaggle 比赛&#xff1a;预测房价 这里李沐老师讲的比较的细致&#xff0c;我根据提供的代码汇总了一下&#xff1a; import hashlib import os import tarfile import zipfile import requests import numpy as np import pandas as pd import torch from matplotlib i…...

4.7学习总结 java集合进阶

集合进阶 泛型 //没有泛型的时候&#xff0c;集合如何存储数据 //结论: //如果我们没有给集合指定类型&#xff0c;默认认为所有的数据类型都是object类型 //此时可以往集合添加任意的数据类型。 //带来一个坏处:我们在获取数据的时候&#xff0c;无法使用他的特有行为。 //此…...

设计模式 - 代理模式Proxy

设计思想&#xff1a; 举个通俗的例子&#xff0c;你想找某局长帮你做一件事情&#xff0c;但局长官位显赫&#xff0c;你又不能轻易见着&#xff0c;你就想到了找他的秘书&#xff0c;通过她传话给局长&#xff0c;这样你就等于请他的秘书帮你办成了那件事。秘书为什么就可以…...

计算机网络体系结构(一)

1.计算机网络概述 1.1计算机网络的概念 计算机网络是由相互连接的计算机及其周边设备构成的系统&#xff0c;这些计算机和设备通过各种通信介质实现数据和资源的共享。计算机网络的主要目的是为了增强信息传递的效率、便利性和可靠性。以下是一些计算机网络的关键概念&#xf…...

数据结构与算法-数学-基础数学2(扩展欧几里得算法,组合数问题)

六&#xff1a;扩展欧几里得算法 同余&#xff1a; 若 a≡b(modm)&#xff0c;则 m 整除 a−b&#xff0c;即 abkm&#xff08;k 为整数&#xff09;。 扩展欧几里得算法 扩展欧几里得算法可用于求解 axbygcd(a,b) 的一组整数解。 #include <iostream> using namesp…...

【力扣hot100题】(072)柱状图中的最大矩阵

这绝对是我做过印象最深的算法题之一。&#xff08;还有是那道盛水最多的贪心题&#xff09; 当初不知道想了多少个日日夜夜&#xff0c;所幸这道题已经深深的烙印在了我的脑海里。 现在看来也没那么可怕&#xff08;&#xff09;不过初见确实非常难想到单调栈。 方法如下&a…...

T-SQL语言的压力测试

T-SQL语言的压力测试 随着数据驱动技术的发展&#xff0c;数据库在现代应用中的角色愈加重要。而在数据库管理系统中&#xff0c;微软的SQL Server凭借其强大的功能和易用性&#xff0c;广泛应用于各行业。在这一环境中&#xff0c;T-SQL&#xff08;Transact-SQL&#xff09;…...

debian 系统gnome怎么关闭触摸屏三指滑动

ubuntu如何限制三指手势操作_ubuntu 手势-CSDN博客 参考方案给上面了, kiosk模式 就是专用模式,类似于广告机、售货机那种。 方案 在 Debian 系统的 GNOME 桌面环境中,可以通过以下方法关闭触摸屏三指滑动功能: 安装 gnome-tweaks 工具:...

【9】搭建k8s集群系列(二进制部署)之安装work-node节点组件(kube-proxy)和网络组件calico

承接上一篇文章&#xff0c;继续安装工作节点的第二个组件&#xff1a;kube-proxy 一、创建配置文件 cat > /opt/kubernetes/cfg/kube-proxy.conf << EOF KUBE_PROXY_OPTS"--logtostderrfalse \\ --v2 \\ --log-dir/opt/kubernetes/logs \\ --config/opt/kubern…...

MongoDB及Yapi迁移数据

一、MongoDB安装及迁移 1、导入MongoDB GPG密钥 sudo rpm --import https://www.mongodb.org/static/pgp/server-5.0.asc 2、创建MongoDB 安装源配置文件 vi /etc/yum.repos.d/mongodb-org-5.0.repo&#xff0c;添加以下内容&#xff1a; [mongodb-org-5.0] nameMongoDB Repo…...

高效解读机器语言,profinet转ethernet ip网关烟草企业自动化升级案例分析

工业通信协议转换在烟草生产线的实践应用 某中型烟草生产企业为提高自动化水平&#xff0c;引进了西门子S7-1500系列PLC控制系统和防爆型科氏力质量流量计。但在系统集成阶段&#xff0c;技术人员发现PLC支持的PROFINET协议与流量计采用的EtherNet/IP协议存在互操作障碍&#x…...

使用Scade实现神经网络算法

在ERTS2022中&#xff0c;ANSYS 发表了使用Scade实现神经网络AI算法的相关工作。论文题目为《Programming Neural Networks Inference in a Safety-Critical Simulation-based Framework》 背景与挑战 神经网络在安全关键系统中的应用&#xff1a;随着嵌入式系统中自主性的引入…...

rom定制系列------小米10pro机型定制解锁固件 原生安卓15批量线刷固件 操作解析与界面预览

注意;固件用于自己机型忘记密码或者手机号注销等出现设备锁 过保修期 售后无视的机型&#xff0c;勿用于非法途径 目前有粉丝联系&#xff0c;自己的机型由于手机号注销导致手机更新系统后出现设备锁界面。另外也没有解锁bl。目前无法使用手机。经过询问是小米10pro机型。根据…...

2023年-全国大学生数学建模竞赛(CUMCM)试题速浏、分类及浅析

2023年-全国大学生数学建模竞赛(CUMCM)试题速浏、分类及浅析 全国大学生数学建模竞赛&#xff08;China Undergraduate Mathematical Contest in Modeling&#xff09;是国家教委高教司和中国工业与应用数学学会共同主办的面向全国大学生的群众性科技活动&#xff0c;目的在于激…...

2014年-全国大学生数学建模竞赛(CUMCM)试题速浏、分类及浅析

2014年-全国大学生数学建模竞赛(CUMCM)试题速浏、分类及浅析 全国大学生数学建模竞赛(China Undergraduate Mathematical Contest in Modeling)是国家教委高教司和中国工业与应用数学学会共同主办的面向全国大学生的群众性科技活动,目的在于激励学生学习数学的积极性,提高学…...

【Docker基础】--查阅笔记1

目录 Docker是什么Docker解决什么问题Docker的理念Docker基本组成镜像&#xff08;image&#xff09;容器&#xff08;container&#xff09;仓库&#xff08;registry&#xff09; Docker平台架构Docker基本实现原理 Docker常用命令总结 Docker是什么 Docker解决什么问题 统…...

算法(动态规划)

动态规划 基本思想 将问题分解为相互重叠的子问题 定义子问题&#xff1a;将原问题分解为若干个子问题。确定状态转移方程&#xff1a;找到子问题之间的递推关系。边界条件&#xff1a;确定初始状态的值。递推计算&#xff1a;根据状态转移方程和边界条件逐步计算子问题的解。…...

2025 年前端与后端开发方向的抉择与展望-优雅草卓伊凡

2025 年前端与后端开发方向的抉择与展望-优雅草卓伊凡 在 2025 年这个科技浪潮奔涌的时代&#xff0c;软件开发领域持续变革&#xff0c;前端与后端开发方向的抉择&#xff0c;成为众多从业者和爱好者亟待破解的关键命题。卓伊凡就频繁收到这样的疑问&#xff1a;“2025 年了&…...

指纹浏览器技术架构解析:高并发批量注册业务的工程化实践——基于分布式指纹引擎与防关联策略的深度实现

一、技术背景与行业痛点 在跨境电商、广告投放、问卷调查等场景中&#xff0c;批量注册与多账号矩阵运营已成为刚需。然而&#xff0c;主流平台&#xff08;如亚马逊、Facebook、Google&#xff09;的风控系统通过浏览器指纹追踪&#xff08;Canvas/WebGL/WebRTC等&#xff09…...

基于SpringBoot的“智慧医疗采购系统”的设计与实现(源码+数据库+文档+PPT)

基于SpringBoot的“智慧医疗采购系统”的设计与实现&#xff08;源码数据库文档PPT) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBoot 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 系统总体结构图 局部E-R图 系统首页界面 系统…...

codeforces B. Large Array and Segments

题目简述&#xff1a; 给定一个长度为n的数组&#xff0c;以及两个整数k和p&#xff0c;该数组可以通过复制在增加长度&#xff0c;可以复制k次&#xff0c;我们最后要找到保证后缀和至少为p的首元结点的数量 思路简述&#xff1a; 找到有多少个完整的原数组n&#xff0c;最…...

VS Code-i18n Ally国际化插件

前言 本文借鉴&#xff1a;i18n Ally 插件帮你轻松搞定国际化需求-按模块划分i18n Ally 是一款 VS Code 插件&#xff0c;它能通过可视 - 掘金本来是没有准备将I18n Ally插件单独写一个博客的&#xff0c;但是了解过后&#xff0c;功能强大&#xff0c;使用方便&#xff0c;解决…...

ResNet改进(21):基于ECA注意力机制的ResNet18网络实现

一、引言 在计算机视觉领域,ResNet(残差网络)一直是图像分类任务中的重要基准模型。今天我们要介绍的是一个改进版的ResNet18网络,它在传统ResNet结构的基础上加入了ECA(Efficient Channel Attention)注意力机制,能够在不显著增加计算量的情况下提升模型性能。 二、网络…...

[ERROR] Some problems were encountered while processing the POMs

记录一次maven的错误 问题复现&#xff1a; 我在ruoyi-vue-plus项目的ruoyi-modules中新建了一个子项目ruoyi-network-telphonem,然后某一次编译的时候提示SysTenantServiceImpl找不到无参的构造函数&#xff0c;检查了很久都没发现问题&#xff0c;于是我想着删掉本地maven仓…...

【网络协议】WebSocket讲解

目录 webSocket简介 连接原理解析: 客户端API 服务端API&#xff08;java&#xff09; 实战案例 &#xff08;1&#xff09;引入依赖 &#xff08;2&#xff09;编写服务端逻辑 &#xff08;3&#xff09;注册配置类 &#xff08;4&#xff09;前端连接 WebSocket 示例…...

什么是可靠性工程师?

一、什么是可靠性工程师&#xff1f; 可靠性工程师就是负责确保产品在使用过程中不出故障、不给客户添麻烦。 你可以理解为是那种“挑毛病的人”&#xff0c;但不是事后挑&#xff0c;是提前想清楚产品在哪些情况下可能会出问题&#xff0c;然后解决掉。 比如&#xff1a; …...

Next.js + SQLite 项目 Docker 生产环境部署方案

以下是完整的 Next.js SQLite 项目 Docker 生产环境部署方案&#xff1a; 1. 项目结构准备 your-project/ ├── prisma/ │ ├── schema.prisma │ └── migrations/ ├── app/ ├── lib/ ├── Dockerfile ├── docker-compose.yml ├── .dockerignore └…...

记录1---20250407

哈佛结构&#xff1a;指令和数据放在不同的存储器&#xff0c;因为在使用流水线时&#xff0c;方便数据和指令的读取和存入。 冯诺依曼结构&#xff1a;指令和数据放在同一个存储器。 处理器的存储结构&#xff1a;CPU内部采用hierarchy 存储结构&#xff0c;一般由 CPU内部…...

php调用大模型应用接口实现流式输出以及数据过滤

最近开发智能客服&#xff0c;需要用php调用已有的大模型应用接口流式输出vue前端调用打字机效果展示。这里整理了php调用大模型流式输出业务过滤等的核心实现部分&#xff0c;分享给大家。 前置条件&#xff1a;大模型应用接口已经打通&#xff08;最好是通过postman或者apip…...

数据结构:红黑树

为什么要以这个结构为题&#xff1f;那就要追溯到CSTL库中的两种map/set&#xff0c;分别基于红黑树与哈希表&#xff0c;我是根本分不清楚。为了搞清楚其区别&#xff0c;我会重点聊聊红黑树和哈希表。 当然也会简单介绍一下树的结构。 树 Tree 首先需要简单了解树这个数据结…...

nginx配置ssl证书,实现https安全访问.

前置条件: 名称 ip地址端口号nginx服务器192.168.59.3080/443server服务器190.168.59.318080/8081/8082 安装nginx服务: 参见: 编译安装nginx-CSDN博客 启动后端web服务器192.168.59.31: (#后端要被代理的web服务器要有docker服务并且配置相关的加速服务) 拉取tomcat容器…...

《当区块链穿上防弹衣:落盘加密技术全景拆解》

落盘加密是区块链技术中一项重要的数据安全机制,主要用于保护节点本地存储的敏感数据不被非法访问。本文将全面解析区块链落盘加密的技术原理、实施方法、应用价值,并与其他加密技术进行比较分析。 🔒 落盘加密的技术原理 落盘加密(Disk Encryption)是指对存储在物理磁盘…...

新HTML5

在新HTML5中&#xff0c;DOCTYPE声明以及字符编码声明都非常简单&#xff1a; <!DOCTYPE html> <html> <head><meta charset"UTF-8"><title>Document</title> </head> <body>内容 </body> </html>HTM…...

DeepSeek-MLA

MLA 结构 需要缓存 KV 向量共用的压缩隐特征K 向量多头共享的带位置编码的向量 为什么带有位置信息的 Q 向量来自于隐特征向量&#xff0c;而带有位置的 K 向量来自于 H 向量且共享呢&#xff1f; 最好的方法肯定是从H向量直接计算并且不共享&#xff0c;但是会大大增加显存使…...

SQL:数据类型(Data Types)

目录 数字数据类型&#xff08;Numeric data types) 非数据类型&#xff08;Non-numeric data types) 日期和时间类型&#xff08;Date and Time Types) NULL&#xff08;缺失或未知值&#xff09; 当你在数据库中创建表格时&#xff0c;你必须指明表中每一列可以保存的数据…...

AF3 OpenFoldBatchCollator类解读

AlphaFold3 data_modules 模块的 OpenFoldBatchCollator 类将一个蛋白质样本列表中的多个字典按键合并,并对每个键值进行 torch.stack 操作,打包成一个批次(batch)。通过定义 OpenFoldBatchCollator类的 __call__ 方法,可以将类的实例当作函数来调用,相当于自定义的批次打…...

手搓多模态-06 数据预处理

前情回顾 我们目前实现了视觉模型的编码器部分&#xff0c;然而&#xff0c;我们所做的是把一张图片编码嵌入成了许多个上下文相关的嵌入向量&#xff0c;然而我们期望的是一张图片用一个向量来表示&#xff0c;从而与文字的向量做点积形成相似度&#xff08;参考手搓多模态-01…...

[蓝桥杯] 求和

题目链接 P8772 [蓝桥杯 2022 省 A] 求和 - 洛谷 题目理解 这道题就是公式题&#xff0c;我们模拟出公式后&#xff0c;输出最终结果即可。 本题不难&#xff0c;相信很多同学第一次见到这道题都是直接暴力解题。 两个for循环&#xff0c;测试样例&#xff0c;直接拿下。 #in…...

INFINI Labs 产品更新 | Coco AI 0.3 发布 – 新增支持 Widget 外部站点集成

INFINI Labs 产品更新发布&#xff01;此次更新涵盖 Coco AI 、Easysearch 等产品多项重要升级&#xff0c;重点提升 AI 搜索能力、易用性及企业级优化。 Coco AI v0.3 作为 开源、跨平台的 AI 搜索工具&#xff0c;新增快捷键设置&#xff0c;支持多个聊天会话等功能。Coco A…...

vue3+element-plus多个多选下拉框并搜索

一、下拉框组件&#xff1a; <template> <div class"top-select"> <div class"first-select"> <div v-for"(group, index) in selectGroups" :key"index" class"item-select" > <div class&quo…...

吴恩达深度学习复盘(9)多类分类与SoftMax回归

多类分类 概念 对于分类问题&#xff0c;并非只有0或1两个标签&#xff0c;而是可以有两个以上的开放标签。以手写数字分类问题为例&#xff0c;之前只是区分0和1&#xff0c; 但在实际生活中&#xff0c;如读取信封上的数字或邮政编码&#xff0c;会涉及十个可能的数字识别&…...

【力扣hot100题】(068)有效的括号

犹记得第一次做这题的时候是怎样一番惨状&#xff0c;现在已经得心应手了。 class Solution { public:bool isValid(string s) {stack<char> zhan;for(int i0;i<s.size();i){if(s[i]{||s[i](||s[i][) zhan.push(s[i]);else{if(zhan.empty()) return 0;if(zhan.top(){…...

深度学习篇---Prophet时间序列预测工具

文章目录 前言一、什么是Prophet&#xff1f;易用性自动化灵活性鲁棒性快速拟合 二、Prophet的核心原理1. 趋势模型a. 分段线性模型&#xff08;默认&#xff09;b. 逻辑增长模型 2. 季节性模型3. 节假日效应 三、Prophet使用方法安装ProphetPython基本使用示例1. 准备数据2. 创…...

TDengine JAVA 语言连接器

简介 本节简介 TDengine 最重要且使用最多的连接器, 本节内容是以教科书式方式列出对外提供的接口及功能及使用过程中要注意的技术细节&#xff0c;大家可以收藏起来做为今后开发 TDengine 的参考资料。 taos-jdbcdriver 是 TDengine 的官方 Java 语言连接器&#xff0c;Java…...

vue3工程中使用vditor完成markdown渲染并防止xss攻击

vue3工程中使用vditor完成markdown渲染并防止xss攻击 背景环境解决方案引入依赖 组件封装实现效果 背景 做oj系统时&#xff0c;题目使用的时markdown语法字符串,前端查看时需要将markdown转html再渲染到页面上。 环境 vitevue3pnpm 解决方案 引入依赖 pnpm install vdit…...