[Linux系统编程]多线程

多线程

  • [1. 线程](#1. 线程)
    • [1.1 线程的概念](#1.1 线程的概念)
    • [1.2 进程与线程对比](#1.2 进程与线程对比)
    • [1.3 轻量级进程](#1.3 轻量级进程)
  • [2. Linux线程控制](#2. Linux线程控制)
    • [2.1 POSIX 线程(pthread)](#2.1 POSIX 线程(pthread))
    • [2.2 线程ID、pthread_t、和进程地址空间的关系](#2.2 线程ID、pthread_t、和进程地址空间的关系)
      • [2.2.1 pthread_self](#2.2.1 pthread_self)
      • [2.2.2 pthread_create](#2.2.2 pthread_create)
      • [2.2.3 pthread_join](#2.2.3 pthread_join)
      • [2.2.4 线程终止的三种方式](#2.2.4 线程终止的三种方式)
      • [2.2.5 pthread_t](#2.2.5 pthread_t)
    • [2.3 线程的两种状态](#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中,提供创建轻量级进程的接口,即创建进程,共享空间。

sql 复制代码
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"选项

sql 复制代码
创建线程的核心函数 pthread_create:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void*), void *arg);

✅ 返回值:
成功:返回 0
失败:返回错误码(注意:不是 errno,而是直接返回一个错误代码!)

错误处理:

sql 复制代码
int ret = pthread_create(...);
if (ret != 0) {
    fprintf(stderr, "pthread_create : %s\n", strerror(ret));
}

与老式函数不同,pthread 系列函数不会设置全局变量 errno,而是直接通过返回值返回错误码。
使用 strerror(ret) 打印错误信息,是推荐做法。
sql 复制代码
#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;      // 声明线程 ID
    int 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);
    }
}

结果:

sql 复制代码
I'm main thread
I'm thread 1
I'm main thread
I'm thread 1
...

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

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

sql 复制代码
#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());
sql 复制代码
Main  : PID = 12345, PPID = 12200
Thread: PID = 12345, PPID = 12200

pid 和 ppid 在主线程和子线程中是一样的。

因为:

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

sql 复制代码
$ ps -aL
 ps       # 进程状态查看命令
 -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

sql 复制代码
这是 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

sql 复制代码
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 类型)返回给你。

👇 所以:

sql 复制代码
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);

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

但是在线程函数 thread_func 中,你是没办法直接用这个 tid 的,因为它是 main 函数传进去的。

这时候,如果想获取 我是谁?我是哪个线程? ------ 用 pthread_self() 就行了。

sql 复制代码
#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;
}
sql 复制代码
Hello from thread! My pthread_t ID is 139812356818688

🔍 和系统线程ID(TID)的区别?

pthread_self() 返回的是 pthread 库的线程 ID(pthread_t),不是内核线程 ID(TID)。

如果想获取系统层面的线程 ID(TID),用:

sql 复制代码
#include <sys/syscall.h>
#include <unistd.h>

pid_t tid = syscall(SYS_gettid);

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

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

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

sql 复制代码
#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;
}

输出:

sql 复制代码
Main thread sleeping...
Child thread starts running
Child thread ends
Main thread ends

虽然子线程1秒就结束了,但主线程活着,进程还没完事。

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

sql 复制代码
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() 是用于等待一个线程结束的函数。

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

sql 复制代码
int pthread_join(pthread_t thread, void **retval);

参数	    说明
thread	要等待的线程的 pthread_t ID
retval	用于接收线程退出时返回的值(也可以为 NULL)
sql 复制代码
#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;
}
sql 复制代码
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(),比如:

sql 复制代码
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:

sql 复制代码
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);

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

sql 复制代码
pthread_join(tid, NULL); // 主线程
pthread_join(tid, NULL); // 线程 B

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

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

因为 pthread_join 的作用是:

等待目标线程结束

清理该线程的资源

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

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

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

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

✅ 用同步机制代替:

比如:

条件变量(pthread_cond_wait):线程完成时广播信号

信号量

互斥锁 + 标志变量

线程池场景下用任务状态标识

2.2.4 线程终止的三种方式

方法1:线程函数中 return

sql 复制代码
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()

sql 复制代码
void pthread_exit(void *retval);

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

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

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

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

特点:

线程可以随时主动退出;

退出时可以返回一个指针(让 pthread_join() 拿到);

返回值通常是 malloc 或全局变量分配的空间,不能是局部变量!

❌ 错误示例:

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

retval 不能是局部变量的地址,因为线程退出后栈会销毁,指针就失效了。

通常用 malloc 动态分配,或者用全局变量。

✅ 方法3:调用 pthread_cancel() 取消另一个线程

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

sql 复制代码
int pthread_cancel(pthread_t thread);

thread:要被取消的线程 ID。
返回值为 0 表示成功,非 0 表示失败。
sql 复制代码
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,说明这个线程被取消了。

sql 复制代码
#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 类型",你会这么写:

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

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

sql 复制代码
// pthreadtypes.h 中
typedef unsigned long int pthread_t;

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

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

🧠 那它和 PID 有什么关系?

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

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

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

可以用下面命令查看:

sql 复制代码
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 系统调用)拿到它。

sql 复制代码
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>

void* thread_func(void* arg) {
    pthread_t tid = pthread_self();   // 线程库层面ID
    pid_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;
}

输出:

sql 复制代码
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 一个分离线程,会返回错误。

sql 复制代码
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)------如果没人收尸又不分离,那就造成资源泄漏!

相关推荐
汤姆yu9 分钟前
Spring 中的 @Cacheable 缓存注解
java·spring·缓存
liyongjun631611 分钟前
Zookeeper 命令返回数据的含义
linux·服务器·zookeeper
老码识土19 分钟前
kotlin 协程源代码泛读:引子
java
dessler20 分钟前
Kubernetes(k8s)-服务目录(ServiceCatalog)介绍(二)
linux·运维·kubernetes
小智疯狂敲代码27 分钟前
Spring AOP源码-JDK 与 CGLIB 动态代理的抉择与实现
java·面试
一介输生28 分钟前
Spring Boot 实现权限管理(下)
java·后端
-曾牛30 分钟前
Git完全指南:从入门到精通版本控制 ------- Git 工作区、暂存区和版本库(4)
java·git·学习·个人开发
forestsea32 分钟前
Java并发编程面试题:锁(17题)
java·开发语言
顾言35 分钟前
Java 线程中断 Interrupted
java·后端
我的Go语言之路果然有问题36 分钟前
案例速成linux手把手教学(个人笔记)
linux