大家好!

提到并发编程,线程一定是重中之重。相比于笨重的进程,线程共享进程地址空间、创建开销更小、切换效率更高,也是现代操作系统主流的并发实现方案。

但在 Linux 环境下,线程的实现并没有想象中简单:我们代码中使用的 pthread_t 用户线程、内核调度的 LWP 轻量级进程,都是极易混淆的知识点。

本文从零梳理 Linux 线程核心原理,厘清用户级线程与内核级线程的区别,详解 Linux 经典的 1:1 线程映射机制,搭配代码 + 图片展示,零基础也能轻松看懂线程底层逻辑。

目录

1.线程概念

1.Linux中线程该如何理解?

2.重新定义线程和进程

3.重谈地址空间

4.Linux线程周边的概念

5.线程的优点

2.线程创建

pthread库

pthread_create

LWP

线程简单使用展示

线程出错,进程停止

3.线程控制

pthread_t tid

pthread_t tid和 LWP的区别

线程等待

pthread_join

pthread_join第二参数

线程退出

pthread_exit

pthread_cancel

重谈线程参数和返回值

c++11支持多线程

线程tid

pthread_self

1:1 模型(Linux 用的)

验证

线程栈区独立

全局变量所有线程共享

线程局部存储

分离线程 pthread_detach

结语


1.线程概念

线程:是进程内的一个执行分支。线程的执行粒度,要比进程要细。

1.Linux中线程该如何理解?

进程访问内存时,需要通过地址空间和页表机制在物理内存中定位代码、数据和堆空间。

地址空间是进程访问资源的视图窗口,它定义了进程可见的所有资源范围。

理论上,通过让一个task_struct指向原进程的地址空间并共享部分代码数据及页表映射,可以创建多个这样的进程。这些新进程能够共享原始进程的地址空间,并将代码段划分为多个部分分别执行,从而实现内存资源的共享。与传统进程相比,这些新进程具有更细粒度的执行单元。

我们将这种机制实现的进程称为线程。

结论(Linux实现方案):

1.在Linux中,线程在进程“内部”执行,线程在进程的地址空间内运行(为什么?)

任何执行流执行,都要有资源!地址空间是进程的资源窗口,线程在进程的地址空间运行是不过分的

2.在Linux中,线程的执行粒度要比进程更细?线程执行进程代码的一部分。

2.重新定义线程和进程

1.什么是线程?

我们认为,线程是操作系统进行调度的基本单位!

2.什么是进程?

进程 = 内核数据结构(task_struct) +  代码和数据

重新理解进程

内核观点:进程是承担分配系统资源的基本实体

进程 = (一大堆)内核数据结构(task_struct) + 代码和数据

执行流是资源吗? --- 当然是

线程就是进程内部的执行流资源

如何理解我们以前的进程呢??

操作系统以进程为单位,给我们分配资源,只不过我们当前的进程内部只有一个执行流!

Linux 通过进程数据结构和管理算法来实现线程功能,具体体现在 struct task_struct 的设计上。由于 Linux 并未采用真正的线程概念,而是巧妙地使用进程内核数据结构来模拟线程。 --- 这种设计既简化了系统架构,又降低了维护成本,同时减少了出错概率。这一设计理念体现了卓越的系统工程智慧。

从CPU的视角来看,它无法区分线程和进程的区别,CPU所识别的执行单元始终是小于或等于进程级别的。具体关系可以表示为:线程 ≤ 执行流 ≤ 进程。

Linux中的执行流:轻量级进程

3.重谈地址空间

如何理解资源分配给线程?

虚拟地址是如何转换到物理地址的?32为虚拟地址为例

虚拟地址是32位

32位的虚拟地址不是一个整体,32位的虚拟地址是将它转换为10 + 10 + 12

页表也不是一整块儿的,如果是一整块的会怎么样呢?

页表的一行保存虚拟地址,物理地址和标志位,假设为4 + 4 + 2 = 10字节

若要计算页表的行数,需考虑整个虚拟地址空间的映射情况。假设采用32位地址空间(0x00000000到0xFFFFFFFF)且全部虚拟地址都建立映射,那么页表需要包含2^32个条目。

如果有2^32个字节的大小就要存4GB的内容,更不要说乘以10了,单独一个页表就存40GB那是不可能的

页表是被拆成两级的,第一级,只有1024(2^10)个条目

虚拟地址的前10位数直接转换成对应的十进制数作为一级页表的下标,找到二级页表。

在用中间的10位数转成十进制数,找到二级页表,其中存放的是物理内存中页框的起始地址

第一级页表叫做页目录,里面的内容叫做页目录表项,存放的是许多的二级页表的地址。

第二级页表里面的内容叫做页表表项,里面存放着对应页框的起始地址和一些权限的字段。

剩余的12个比特位表示的范围是[0, 2^12-1],用于指定从页框内访问物理内存的相对偏移量。由于一个页框的大小正好是4KB(即2^12字节),这个偏移量范围与页框容量完全匹配。

像这样的页表设计大小会是多大呢???

一个二级页表中内容是4个字节,页框的地址也并不需要2的32次方(4字节),20个比特位,2的20次方也就可以存放页框地址了。

对于1个二级页表,4字节大小乘以1024就是4kb的大小,可以放在一个页框。

页目录自己存放一个4kb大小的页框就够了。

页目录其中1024个表项,计算下来就是:4kb * 1024 = 4mb

对于一个进程,一般情况下都是不会将空间用光,对于二级页表大部分时间都是用不完的。

一个进程的页表算下来也不是特别大。

虽然大小很小了,但是对于创建一个进程也还是很一件很“重”的工作!!


对于CR3寄存器来讲,它是直接指向页目录的,任何一个进程,二级页表可以没有或者残缺,但是页目录是必须要存在的。

CR2寄存器,保存的是一些原因引起缺页中断或者异常的地址,保存在CR2寄存器里面,等把其他工作做好了,再用这个地址继续访问。

线程目前分配资源,本质就是分配地址空间的范围。

4.Linux线程周边的概念

线程 VS 进程 

进程是自愿分配的基本单位

线程是调度的基本单位

线程共享进程数据,但也有自己的一部分数据:

        线程ID、一组寄存器、栈、errno、信号屏蔽字、调度优先级        

进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调度,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享一下进程资源和环境:

        文件描述符表、每种信号的处理方式、当前工作目录、用户id和组id

线程比进程要更轻量化,为什么?

a.创建和释放更加轻量化(生死)

b.切换更加轻量化(运行)

线程在进行切换的时候,页表和地址空间都不需要切换,所以线程在切换的时候更加高效一些

但是就只是多几个寄存器,少几个寄存器的问题,这就提高效率了吗?

线程的执行本质就是进程在执行,毕竟线程是进程的执行分支

CPU中除了寄存器,还有一个缓存叫cache,缓存的热数据,将高频的数据缓存到里面。

线程在切换的时候,上下文不变化,数据缓存的热数据就不需要保存了。

CPU调度的基本单位是线程,并不代表进程不需要调度。操作系统也要调度进程。

进程在切换的时候,数据要保存,但是cache直接被舍弃了,新的cache需要从冷到热。

线程的cache不需要由冷到热重新缓存。

缓存的大小是比寄存器大很多的,单位都是kb的

其实一个线程在被创建的时候,也有自己的时间片,它的时间片也是来自于对应的进程的。时间片也是资源,所以创建每一个执行流线程,不能给线程重新申请时间片,必须把整个进程的时间片平均划分给线程

5.线程的优点

创建一个新线程的代价要比创建一个新进程的代价小得多

与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

线程占用的资源比进程少很多

能充分利用多处理器的可并行数量

在等待慢速I/O操作结束的同时,程序可以执行其他的计算任务

I/O密集型应用,为了提高性能,将I/O操作重叠,线程可以同时等待不同的I/O操作

2.线程创建

pthread库

Linux中是通过进程模拟线程,所以在Linux内核中有没有很明确的线程概念呢?没有,只有轻量级进程的概念。注定了不会直接给我们提供线程的系统调用,只会给我们提供轻量级进程的系统调用。

我们用户,需要线程的接口!

所以就有了程序员开发出来了pthread线程库 --- 应用层 --- 轻量级进程接口进行封装,为用户提供直接线程的接口。

几乎所有的Linux平台,都是默认自带这个库的!

Linux中编写多线程代码 需要使用第三方pthread库!!

pthread_create

pthread_create 创建一个新的线程

头文件:pthread.h

对于第三个参数,函数指针:线程在创建的时候,每一个执行流执行代码的一部分,自己编程写代码的时候,想让线程执行哪部分,就传入对应的函数指针。main函数是主线程执行的入口函数,这里就是新线程执行的入口函数

返回值,0代表成功,非0代表错误,通过返回值的方式告诉错误码是几

由于这是一个库方法,在编译的时候,需要加上-lpthread才能正常的链接对应的库,使用这个方法。

LWP

线程是调度的基本单位,每一个线程都是在同一个地址空间当中,所有的线程都属于同一个进程,所有线程去调用getpid打印出来的是同一个pid:

#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;

// new thread
void* threadRoutine(void* args)
{
    while(true)
    {
        cout << "new thread, pid: " << getpid() << endl;
        sleep(2);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr);// 不是系统调用
    // 库方法,
    while(true)
    {
        cout << "main thread, pid: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

可是操作系统选择线程的时候,它怎么知道哪个线程是主线程,每个线程都是调度的基本单元,每个线程是不是要有自己的id值呢?

通过ps -aL查看(-L)大L选项,查看当前用户启动的所有轻量级进程

对应的LWP是什么?

这就是相应的轻量级进程PID,这些轻量级进程的调度正是基于这个PID来实现的。

通过LWP和PID是否相等就可以决定是否是主线程!!

线程简单使用展示

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>

using namespace std;

// 可以被多个执行流同时执行,show函数被重入了!!
void show(const string& name)
{
    cout << name << "say* " << "hello thread" << endl;
}
// new thread
void* threadRoutine(void* args)
{
    while(true)
    {
        // cout << "new thread, pid: " << getpid() << endl;
        show("[new thread]");
        sleep(2);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr);// 不是系统调用
    // 库方法,
    while(true)
    {
        // cout << "main thread, pid: " << getpid() << endl;
        show("[main thread]");
        sleep(1);
    }
    return 0;
}


这里的show函数被多个执行流重入了(有线程安全)

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>

using namespace std;

int g_val = 100;

// 可以被多个执行流同时执行,show函数被重入了!!
void show(const string& name)
{
    cout << name << "say* " << "hello thread" << endl;
}
// new thread
void* threadRoutine(void* args)
{
    while(true)
    {
        printf("new thread, pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);
        // cout << "new thread, pid: " << getpid() << endl;
        // show("[new thread]");
        sleep(2);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr);// 不是系统调用
    // 库方法,
    while(true)
    {
        printf("main thread, pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);
        // cout << "main thread, pid: " << getpid() << endl;
        // show("[main thread]");
        sleep(1);
        g_val++;
    }
    return 0;
}

未初始化,已初始化的全局变量在所有的线程中都是共享的!

线程中通信就非常方便

线程出错,进程停止

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>

using namespace std;

int g_val = 100;

// 可以被多个执行流同时执行,show函数被重入了!!
void show(const string& name)
{
    cout << name << "say* " << "hello thread" << endl;
}
// new thread
void* threadRoutine(void* args)
{
    while(true)
    {
        printf("new thread, pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);
        // cout << "new thread, pid: " << getpid() << endl;
        // show("[new thread]");
        sleep(5);
        int a = 10;
        a /= 0;
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr);// 不是系统调用
    // 库方法,
    while(true)
    {
        printf("main thread, pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);
        // cout << "main thread, pid: " << getpid() << endl;
        // show("[main thread]");
        sleep(1);
        g_val++;
    }
    return 0;
}

可以看到只要线程发生意外终止,整个进程就会终止,所有线程就都要停止,监视窗口也直接打印不出来了

3.线程控制

pthread_t tid

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>

using namespace std;

int g_val = 100;

// 可以被多个执行流同时执行,show函数被重入了!!
void show(const string &name)
{
    cout << name << "say* " << "hello thread" << endl;
}
// new thread
void *threadRoutine(void *args)
{
    while (true)
    {
        printf("new thread, pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);

        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr); // 不是系统调用
    // 库方法,
    while (true)
    {
        printf("main thread, pid: %d, g_val: %d, &g_val: 0x%p, create new thread tid : % ul\n ",
               getpid(),
               g_val, &g_val, tid);
        sleep(1);
        g_val++;
    }
    return 0;
}

可以看到tid和前面说的那个轻量级进程pid好像不一样?

这个tid无符号整数究竟是什么呢?

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>

using namespace std;

int g_val = 100;

// 可以被多个执行流同时执行,show函数被重入了!!
void show(const string &name)
{
    cout << name << "say* " << "hello thread" << endl;
}
// new thread
void *threadRoutine(void *args)
{
    while (true)
    {
        printf("new thread, pid: %d, g_val: %d, &g_val: 0x%p\n", getpid(), g_val, &g_val);

        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, nullptr); // 不是系统调用
    // 库方法,
    while (true)
    {
        printf("main thread, pid: %d, g_val: %d, &g_val: %p, create new thread tid : %p\n ",
               getpid(),
               g_val, &g_val, tid);
        sleep(1);
        g_val++;
    }
    return 0;
}

我们使用%p打印,可以看出这个tid是一个地址!

  • 本质进程虚拟地址空间中线程控制块(TCB)的内存地址(通常是 unsigned long 类型)
  • 作用:在当前进程内部唯一标识一个线程,用于 pthread_joinpthread_cancel 等库函数操作

pthread_t tid和 LWP的区别

  • pthread_t tid (用户态)

    • 全称:POSIX 线程 ID(Thread ID)
    • 管理者pthread 线程库(glibc/NPTL)
    • 本质进程虚拟地址空间中线程控制块(TCB)的内存地址(通常是 unsigned long 类型)
    • 作用:在当前进程内部唯一标识一个线程,用于 pthread_joinpthread_cancel 等库函数操作
  • LWP (Light Weight Process,内核态)

    • 全称:轻量级进程 ID(也常直接称为 TID, Thread ID
    • 管理者:Linux 内核
    • 本质:内核为每个可调度任务(task_struct)分配的全局唯一整数
    • 作用:内核调度 CPU、分配资源、跟踪任务的唯一标识

线程等待

一个新线程创建出来了是主线程先跑还是新线程先跑??? --- 不确定

谁最后退出呢??? --- 主线程

创建线程的本质就是对线程的管理,要保证主线程最后退出。

新线程退出,默认也会导致类似僵尸进程的问题防止新线程内存泄漏,和获取退出的内容。所以需要线程等待

pthread_join

pthread_join:等待一个进程

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>

using namespace std;


int g_val = 100;

// 可以被多个执行流同时执行,show函数被重入了!!
void show(const string &name)
{
    cout << name << "say* " << "hello thread" << endl;
}
// new thread
void *threadRoutine(void *args)
{
    const char* name = (const char*) args;
    int cnt = 5;
    while (true)
    {
        printf("%s, pid: %d, g_val: %d, &g_val: %p\n", name, getpid(), g_val, &g_val);
        sleep(1);
        cnt--;
        if (cnt == 0) break;
    }
    return nullptr;// 运行到这里默认线程退出了!!
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1"); // 不是系统调用
    
    sleep(10);
    pthread_join(tid, nullptr);// main thread等待的时候,默认是阻塞等待的!

    cout << "main thread quit ..." << endl;
    
    return 0;
}

可以看到等待是必须的,新线程结束,主线程还是会有存在的。

pthread_join是阻塞等待

pthread_join第二参数

线程执行完,怎么得到线程执行函数的返回值呢?使用第二个参数void** retval即可实现需求~

我们只要再创建一个void* reval,传入它的&reval,就可以将返回值void*给拿出来放入reval中:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>

using namespace std;
int g_val = 100;

// 可以被多个执行流同时执行,show函数被重入了!!
void show(const string &name)
{
    cout << name << "say* " << "hello thread" << endl;
}
// new thread
void *threadRoutine(void *args)
{
    const char* name = (const char*) args;
    int cnt = 5;
    while (true)
    {
        printf("%s, pid: %d, g_val: %d, &g_val: %p\n", name, getpid(), g_val, &g_val);
        sleep(1);
        cnt--;
        if (cnt == 0) break;
    }
    return (void*)1;// 将整数1转成void*
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
    void* retval;
    pthread_join(tid, &retval);// 传入二级指针

    cout << "main thread quit ..., retval: " << (long long)retval << endl;
    
    return 0;
}

这里要使用long long(8字节)去强转回来打印,不能用int(4字节),因为测试过里面是64位环境,指针大小为8字节。不然会发生报错。

为什么我们在join这里的时候不考虑异常呢?

因为做不到,线程出异常了,代表整个进程就会异常。

线程退出

除了用return 返回退出线程,还有什么呢?--- exit函数可以吗?

如果通过线程来调用exit来退出呢?会怎么样?

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>

using namespace std;
int g_val = 100;

// 可以被多个执行流同时执行,show函数被重入了!!
void show(const string &name)
{
    cout << name << "say* " << "hello thread" << endl;
}
// new thread
void *threadRoutine(void *args)
{
    const char* name = (const char*) args;
    int cnt = 5;
    while (true)
    {
        printf("%s, pid: %d, g_val: %d, &g_val: %p\n", name, getpid(), g_val, &g_val);
        sleep(1);
        cnt--;
        if (cnt == 0) break;
    }
    exit(11);// 通过新线程调用exit?
    // return (void*)1;// 将整数1转成void*
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
    void* retval;
    pthread_join(tid, &retval);// 传入二级指针

    cout << "main thread quit ..., retval: " << (long long)retval << endl;
    
    return 0;
}

我们会发现进程直接就截止了?exit是用来终止进程的,不能用来终止线程。

pthread_exit

通过pthread_exit退出

void *threadRoutine(void *args)
{
    const char* name = (const char*) args;
    int cnt = 5;
    while (true)
    {
        printf("%s, pid: %d, g_val: %d, &g_val: %p\n", name, getpid(), g_val, &g_val);
        sleep(1);
        cnt--;
        if (cnt == 0) break;
    }
    pthread_exit((void*)100);
    // exit(11);// 通过新线程调用exit?
    // return (void*)1;// 将整数1转成void*
}

所以终止线程的第二种方法:pthread_exit

显然的如果是main函数这个主线程直接return退出,线程也将会直接退出...

pthread_cancel

还有一种退出线程的方法:

线程取消 pthread_cancel

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>

using namespace std;


int g_val = 100;

// 可以被多个执行流同时执行,show函数被重入了!!
void show(const string &name)
{
    cout << name << "say* " << "hello thread" << endl;
}
// new thread
void *threadRoutine(void *args)
{
    const char* name = (const char*) args;
    int cnt = 5;
    while (true)
    {
        printf("%s, pid: %d, g_val: %d, &g_val: %p\n", name, getpid(), g_val, &g_val);
        sleep(1);
        cnt--;
        if (cnt == 0) break;
    }
    pthread_exit((void*)100);
    // exit(11);// 通过新线程调用exit?
    // return (void*)1;// 将整数1转成void*
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
    sleep(1);// 为了保证新线程已经启动
    pthread_cancel(tid);// 取消目标线程

    void* retval;
    pthread_join(tid, &retval);// 传入二级指针
    cout << "main thread quit ..., retval: " << (long long)retval << endl;
    
    return 0;
}

会发现直接将线程取消了,它会存在一个宏PTHREAD_CANCEL,这个被设置返回。它的值就是(void*)-1

重谈线程参数和返回值

传入线程的参数可以更加丰富,比如传入类的对象进入

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>

using namespace std;

// 控制线程的时候,可以传更多的东西
class Requst
{
public:
    Requst(int start, int end, const string& threadname)
        : _start(start)
        , _end(end)
        , _threadName(threadname)
    {}

    
public:
    int _start;
    int _end;
    string _threadName;
};

class Response
{
public:
    Response(int result, int exitcode)
        : _result(result)
        , _exitcode(exitcode)
    {}
public:
    int _result;// 计算结果
    int _exitcode;// 计算结果是否可靠
};
void* sumCount(void*args)
{
    Requst* rq = static_cast<Requst*>(args);//相关类型的隐式类型转换 c++
    Response* rsp = new Response(0, 0);
    for (int i = rq->_start; i <= rq->_end; i++)
    {
        cout << rq->_threadName << " is running, calcing ..., " << i << endl;
        rsp->_result += i;
        usleep(100000);// 0.1秒
    }
    delete rq;
    return rsp;
}
int main()
{
    pthread_t tid;
    Requst* rq = new Requst(1, 100, "thread 1"); // 1到100求和
    pthread_create(&tid, nullptr, sumCount, rq);

    void* ret;
    pthread_join(tid, &ret);
    Response* rsp = static_cast<Response*>(ret);
    cout << "rsp->result: " << rsp->_result << 
    " , exitcode: " << rsp->_exitcode << endl;
    delete rsp;

    return 0;
}

可以发现线程的参数和返回值,不仅仅可以用来进行传递一般参数,也可以传递对象!

以后设计的时候,可以将任务拆分成若干子任务,然后分别让它们运行。

前面的代码,都是通过主线程在堆上创建空间来使用的,可以知道,堆空间也是被线程共享的。

c++11支持多线程

目前,我们的原生线程库,pthread库

不仅仅可以通过pthread_creat创建线程,

目前c++11语言本身也已经支持多线程了,它和原生线程库的关系?

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <thread>
using namespace std;

void threadRun()
{
    while(true)
    {
        cout << "I am a new thread for C++" << endl;
        sleep(1);
    }
}
int main()
{
    thread t1(threadRun);

    t1.join();

    return 0;
}

不仅要加thread头文件,还有编译选项的-lpthread也不能省略掉,不然就会报错

所以在c++11里面的多线程就是封装的原生线程库pthread

在windows的平台下,有自己的一套线程块。c++也就是用的windows那一套实现的。

c++就具有其可移植性,跨平台性。

线程tid

pthread_self

pthread_self获取自己当前线程tid

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <thread>
using namespace std;

string toHex(pthread_t tid)
{
    // 10进制转成16进制
    char hex[64];
    snprintf(hex, sizeof(hex), "%p", tid);
    return hex;
}
void* threadRoutine(void* args)
{
    int cnt = 5;
    while(true)
    {
        cout << "thread id: " << toHex(pthread_self()) << endl;
        sleep(1);
        cnt--;
        if (cnt == 0) break;
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
    cout << "main thread create thread done, new thread id: " << toHex(tid) << endl;
    pthread_join(tid, nullptr);

    return 0;
}

Linux中没有明确的线程概念,只有轻量级进程的概念 

是如何创建轻量级进程的呢?

除了主线程,所有其他线程的独立栈,都在共享区具体来讲是在pthread库中,tid指向的用户tcb中!

1:1 模型(Linux 用的)

用户线程:内核 LWP = 1 : 1

  • 优点:一个线程阻塞(sleep/IO),其他线程照常跑
  • 缺点:创建线程要陷内核、开销稍大
  • 就是你现在用的 pthread

验证

线程栈区独立

#include <iostream>
#include <pthread.h>
#include <vector>
#include <unistd.h>

using namespace std;
// 证明了线程的栈区是独立的

#define NUM 10
struct ThreadData
{
    string _threadname;
};
string toHex(pthread_t tid)
{
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "0x%x", tid);
    return buffer;
}
// 所有的线程,执行的都是这个函数!
void* threadRoutine(void* args)
{
    int test_i = 0;
    ThreadData* td = static_cast<ThreadData*>(args);
    int i = 0;
    while(i < 10)
    {
        cout << "pid: " << getpid() <<
         ", tid: " << toHex(pthread_self()) <<
         ", threadname: " << td->_threadname <<
         ", test_i: " << test_i << 
         ", &test_i: " << &test_i << endl;
        test_i++;
        i++;
    }
    delete td;
    return nullptr;
}

void InitThreadData(ThreadData* td, int number)
{
    td->_threadname = "thread-" + to_string(number);
}

int main()
{
    vector<pthread_t> tids;
    // 创建一批线程
    for (int i = 0; i < NUM; i++)
    {
        ThreadData* td = new ThreadData;
        pthread_t tid;
        InitThreadData(td, i);
        pthread_create(&tid, nullptr, threadRoutine, td);
        tids.push_back(tid);
        sleep(1);
    }
    // 等待
    for (int i = 0; i < tids.size(); i++)
    {
        pthread_join(tids[i], nullptr);
    }

    return 0;
}
#include <iostream>
#include <pthread.h>
#include <vector>
#include <unistd.h>

using namespace std;

// 主线程的栈在栈区里面,新线程的栈在库里面
// 但都是在地址空间里面,主线程想访问任何一个线程都是可以的,怎么验证呢
// 每一个线程都会有自己的独立栈结构
// 线程和线程之间几乎没有密码,线程的栈上的数据,也是可以被其他线程看到和访问的。

int* p = NULL;
#define NUM 10
struct ThreadData
{
    string _threadname;
};
string toHex(pthread_t tid)
{
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "0x%x", tid);
    return buffer;
}
// 所有的线程,执行的都是这个函数!
void* threadRoutine(void* args)
{
    int test_i = 0;
    ThreadData* td = static_cast<ThreadData*>(args);

    if (td->_threadname == "thread-2") p = &test_i;

    int i = 0;
    while(i < 10)
    {
        cout << "pid: " << getpid() <<
         ", tid: " << toHex(pthread_self()) <<
         ", threadname: " << td->_threadname <<
         ", test_i: " << test_i << 
         ", &test_i: " << &test_i << endl;
        test_i++;
        i++;
    }
    delete td;
    return nullptr;
}

void InitThreadData(ThreadData* td, int number)
{
    td->_threadname = "thread-" + to_string(number);
}

int main()
{
    vector<pthread_t> tids;
    // 创建一批线程
    for (int i = 0; i < NUM; i++)
    {
        ThreadData* td = new ThreadData;
        pthread_t tid;
        InitThreadData(td, i);
        pthread_create(&tid, nullptr, threadRoutine, td);
        tids.push_back(tid);
        sleep(1);
    }
    sleep(3);// 确保复制成功
    cout << "main thread get a thread local value, val: " << *p <<
     ", &val: " << p << endl;

    // 等待
    for (int i = 0; i < tids.size(); i++)
    {
        pthread_join(tids[i], nullptr);
    }

    return 0;
}

全局变量所有线程共享

#include <iostream>
#include <pthread.h>
#include <vector>
#include <unistd.h>

using namespace std;

#define NUM 10

int g_val = 100; // 全局变量是被所有的线程同时看到并访问的!
struct ThreadData
{
    string _threadname;
};
string toHex(pthread_t tid)
{
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "0x%x", tid);
    return buffer;
}
// 所有的线程,执行的都是这个函数!
void* threadRoutine(void* args)
{
    ThreadData* td = static_cast<ThreadData*>(args);

    int i = 0;
    while(i < 10)
    {
        cout << "pid: " << getpid()  <<
        ", threadname: " << td->_threadname <<
         ", tid: " << toHex(pthread_self()) <<
         ", g_val: " << g_val << 
         ", &g_val: " << &g_val << endl;
        i++;
        g_val++;
    }
    
    delete td;
    return nullptr;
}

void InitThreadData(ThreadData* td, int number)
{
    td->_threadname = "thread-" + to_string(number);
}

int main()
{
    vector<pthread_t> tids;
    // 创建一批线程
    for (int i = 0; i < NUM; i++)
    {
        ThreadData* td = new ThreadData;
        pthread_t tid;
        InitThreadData(td, i);
        pthread_create(&tid, nullptr, threadRoutine, td);
        tids.push_back(tid);
        sleep(1);
    }
    sleep(1);

    // 等待
    for (int i = 0; i < tids.size(); i++)
    {
        pthread_join(tids[i], nullptr);
    }

    return 0;
}

线程局部存储

如果想要私有的全局变量呢???

全局变量前面加上__thread

这种技术叫做线程的局部存储!!

__thread不是c/c++提供的,是编译器提供的编译选项。

发现打印出的地址都不在是全局区的地址了。

__thread定义线程的局部存储只能用于定义内置类型!不能用来修饰自定义类型

一般这个定义出来,将线程比较关心的一些系统调用的值先拿出来,就不用在大量循环中反复拿取。

那为什么不使用线程的栈去定义呢,然后去获取呢?它其实有些类似全局变量的作用那般,如果又调用了其他函数,不得再传入这个参数吗?所以是有必要有这个线程局部存储的。

分离线程 pthread_detach

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。

如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出后,自动释放线程资源

int pthread_detach(pthread_t thread);

如果分离之后,等待就不会等到了,就直接返回退出了,主线程直接退出来了,其他线程也会退出。所以要保证主线程不要退出,或者最后一个退出。

分离就是线程的一种属性状态,一个线程是否被分离,一定是在被记录下来的。本质分离就是在对应的原生线程库里面将对应的属性改变,表示是否能被join等待。

结语

很高兴大家能够看完这篇文章,其中的内容很多,但是也为我们揭开了线程神奇的面纱。相信看完这篇内容后各位能够收获满满的!有关后续的内容最近就会全部完善好一起发布的。

多多点赞 + 收藏 + 关注!!谢谢大家❤️

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐