humid1ch blogs

本篇文章

手机用户建议
PC模式 或 横屏
阅读


Linux系统 2023 年 4 月 15 日

[Linux] 如何理解线程ID? 什么是线程局部存储?

在Linux中, 使用 pthread_create() 创建线程的时候, 第一个参数就是用来接收线程ID的
前面的几篇文章, 介绍了线程的概念与控制的一些基本的内容.
虽然已经介绍了很多了, 但是 一直都没有详细的介绍一个重要的东西: 线程ID
那么, 本篇文章 就来着重介绍一下, 如何理解线程ID

Linux线程ID

Linux中, 使用pthread_create()创建线程的时候, 第一个参数就是用来接收线程ID的

此线程ID, pthread库维护线程时所使用的唯一标识符

而不是Linux系统内核中 对于表示线程的PCB的线程ID

Linux内核中的线程ID, 就是PCBpid, 是LWP

Linux内核中的LWPpthread库维护的线程ID, 是 1对1的关系.

pthread创建一个线程, 操作系统就会对新的PCB分配一个LWP, pthread库也会分配一个线程ID作为库维护线程的唯一标识符

那么线程ID是什么? 有什么意义呢?

什么是线程ID

进程有进程ID, 即PID, 是进程在操作系统中的唯一标识符
而线程也有自己的ID, 通常叫做TID, 是线程在操作系统中的唯一标识符
一般为无符号的长整型pthread_t
TID, 一般都很大:
#include <iostream>
#include <pthread.h>
using std::cout;
using std::endl;

int main() {
    pthread_t tid1;
    pthread_create(&tid1, nullptr, nullptr, nullptr);
    cout << "tid1 = " << tid1 << endl;

    return 0;
}
输出一个线程ID
输出一个线程ID
它为什么这么长呢?
虽然线程ID是一个无符号的长整型, 但实际上线程ID表示的是一个地址
如果将获取到的TID以16进制输出:
线程ID表示一个地址
线程ID表示一个地址

如何理解线程ID **

通过一定方法获取的线程ID, 除了表示线程在操作系统中的唯一标识之外, 实际还表示一个地址
这个地址是什么呢?
通过pthread_create()创建的线程, 在运行时 一定会产生一些临时数据: 临时变量、函数调用等
所以, 其实线程也有自己的栈结构, 新线程的栈是独立与主线程(进程)的
既然线程存在一个独立的栈结构, 那么这个栈结构是谁创建的呢? 又在什么地方呢?

线程的管理 **

使用过pthread库的接口, 编译生成的可执行程序. 运行时肯定是需要libpthread.so动态库的
使用的pthread库, 是用户级的线程库
程序运行调用接口时, 会被加载到内存中, 再映射到进程地址空间的共享区
当线程需要调用pthread库中的接口时, 操作系统就会将磁盘中的动态库加载到内存中, 然后线程就会跳到共享区去找内存加载的动态库代码

进程中的代码一定包含三部分:

  1. 程序编写的代码
  2. 动态库代码
  3. 系统内核代码

pthread线程库代码会被加载到内存中, 并被映射到进程地址空间的共享区
那么, 其实就可以将一个简单的调用了pthread线程库的进程抽象为这样:

再来思考一个问题, Linux线程是操作系统内核来管理的吗?
Linux内核层面, 实际上没有线程的概念, 只有轻量级进程
所以PCB是操作系统内核管理的没错
线程并不是Linux内核管理的, Linux 内核只有轻量级进程的概念、执行流的概念
即使可以模拟出线程, 但是线程也不是Linux内核中实际存在的东西
操作系统为用户提供了 创建 子进程、共用进程地址空间进程的接口, 并没有提供直接创建、管理线程的接口
我们使用的线程创建、控制等, 其实是pthread库, 封装了系统关于创建子进程相关接口而成的库接口
pthread库中, 帮我们实现了从轻量级进程到线程的过程: 创建线程栈、分配任务, 以及线程的控制等相关接口
那么, 说到这里其实可以明白了, 线程不是由操作系统内核代码管理的, 而是由pthread库代码管理的
操作系统内核为线程的模拟提供了轻量级进程的概念, 而**pthread库则通过轻量级进程和操作系统提供的接口实现了我们理解的线程**
创建进程, 是操作系统内核代码创建的, 操作系统进行对进程的管理, 实现了PCB
而创建线程, 可以说是pthread库代码创建的
那么, 为了方便线程的管理pthread库代码中也实现了有关描述线程属性的结构体
假如, pthread库中实现的描述线程属性的结构体 是struct thread_struct{}, 那么此结构体的成员就可以抽象为:
struct thread_struct {
    pthread_t tid; 			// 线程ID
    void* stack;			// 线程栈
    ……
}
即, pthread库维护有线程的栈、线程的分配等结构. 此结构体也是库维护的:
描述线程属性的结构体, 会由 pthread 库创建并维护

图示的意思是, pthread库会创建并维护 线程属性的结构体, 而不是说此结构体会存储在库所在代码空间中, 更不是会存储在共享区

介绍到现在, 其实已经可以回答两个问题了:
  1. 线程的栈是由谁创建的?
使用pthread_create()创建线程, 那么线程的栈就是就是pthread库创建的
因为 pthread库在创建线程的时候, 会创建线程的属性结构体, 结构体内维护由线程栈的信息

这里介绍一个内容:

Linux若使用pthread库创建线程, 则进程地址空间内的栈区就是主线程的栈

其他新线程的栈区, 是在pthread库维护的一块空间内分配维护的

  1. 通过 pthread 库获取的线程ID, 表示的地址实际就是一个 描述线程属性的结构体的地址
pthread_t到底是什么类型呢?
取决于实现
对于Linux目前实现的NPTL实现而言, pthread_t是一个无符号长整型
可以用来表示pthread 库 维护线程时的唯一标识符
也可以用来表示进程地址空间中的一个地址, 此地址就是描述线程属性的结构体, 在进程中的地址

pthread库实现的线程是在Linux轻量级进程的基础上, 又维护了一些属性实现的

即, pthread库中描述线程属性的结构体应该是直接或间接维护有线程的PCB

对于Linux目前实现的NPTL实现而言, pthread_t 是一个 无符号长整型, 可以用来表示进程地址空间中的一个地址

但这种实现方式并不是POSIX标准规定的, 所以是不符合POSIX标准

所以最好不要通过 此线程ID操作线程的属性, 不然可能会影响代码的可移植性

线程局部存储

pthread库中定义的描述线程的属性的结构体中, 维护有一个特殊区域: 线程局部存储区域
这个区域的作用, 需要用代码来表现
我们知道, 进程中的数据, 对线程来说都是可以见的, 全局数据更是所有线程都可以修改
那么, 先来观察下面这段代码的执行结果:
#include <iostream>
#include <unistd.h>
#include <syscall.h>
#include <pthread.h>
using std::cout;
using std::endl;

int global_value = 100;

void* startRoutine(void* args) {
    const char* name = static_cast<const char*>(args);

    while (true) {
        printf("%s: %lu global_value: %d &global_value: %p Inc: %d lwp: %ld\n", 
                name, pthread_self(), global_value, &global_value, --global_value, ::syscall(SYS_gettid));

        sleep(1);
    }

    return nullptr;
}

int main() {
    pthread_t tid1, tid2, tid3;

    pthread_create(&tid1, nullptr, startRoutine, (void*)"thread1");
    pthread_create(&tid2, nullptr, startRoutine, (void*)"thread2");
    pthread_create(&tid3, nullptr, startRoutine, (void*)"thread3");

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);

    return 0;
}
执行这段代码:
所有线程都在修改全局变量
所有线程都在修改全局变量
这段代码的执行结果就是, 创建的新线程都在对同一个global_value执行--操作
并且可以看到, 不同线程访问的global_value地址都是相同的会互相影响
但是, 如果我们在全局变量的定义前, 加一个__thread:
__thread int global_value = 100;
然后执行代码:
可以看到一个明显的变化, 不同线程看到的是不同的地址, 实际看到的是不同的数据
这就是 线程局部存储, 只属于线程自己的局部存储数据

感谢阅读~
版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)

作者: 哈米d1ch 发表日期:2023 年 4 月 15 日