【Linux】线程的概念与控制

目录

[1. 整体学习思维导图](#1. 整体学习思维导图)

[2. 线程的概念](#2. 线程的概念)

[2.1 基础概念](#2.1 基础概念)

[2.2 Linux下的线程](#2.2 Linux下的线程)

初步理解:

[2. 分页式存储](#2. 分页式存储)

[3.1 页表](#3.1 页表)

[3.1.1 页框/页](#3.1.1 页框/页)

[3.1.2 页表机制](#3.1.2 页表机制)

[3.1.3 从虚拟地址到物理地址的转换](#3.1.3 从虚拟地址到物理地址的转换)

总结:

[3.2 TLB快表](#3.2 TLB快表)

[3.3 缺页异常(Page Fault):](#3.3 缺页异常(Page Fault):)

[4. 线程理解](#4. 线程理解)

[4.1 线程优点](#4.1 线程优点)

[4.2 线程缺点](#4.2 线程缺点)

[4.3 线程和进程](#4.3 线程和进程)

[4.3.1 线程的函数共享](#4.3.1 线程的函数共享)

[​编辑 4.3.2 线程共享全局变量](#编辑 4.3.2 线程共享全局变量)

[5. 线程控制](#5. 线程控制)

[POSIX 线程库](#POSIX 线程库)

[5.1 创建一个线程](#5.1 创建一个线程)

[见一见 C++11 的线程库](#见一见 C++11 的线程库)

[​编辑 Create + join + 获取返回值](#编辑 Create + join + 获取返回值)

[5.2 线程终止](#5.2 线程终止)

[5.3 线程分离](#5.3 线程分离)

[5.4 多线程练习](#5.4 多线程练习)

[6. 线程ID及进程地址空间布局](#6. 线程ID及进程地址空间布局)

[7. 线程封装](#7. 线程封装)


1. 整体学习思维导图

2. 线程的概念

2.1 基础概念

  • 教材上对于进程/线程的解释(概念角度):

    • 进程 = PCB + 数据代码(执行流)

    • 线程:是进程内部的一个执行分支(执行流)

  • 从内核和资源角度:

    • 进程:承担资源分配的基础实体

      • 为什么这么说?进程创建可能会让磁盘文件加载到物理内存,申请需要使用的空间资源,构建映射表等等,这一切都需要使用申请到内部的资源,进程管理使用这部分资源。
    • 线程:CPU调度的基本单位

      • 为什么?通过Linux下具体的线程设计来理解。

2.2 Linux下的线程

  1. 进程访问绝大多数资源时是通过地址空间进行访问的,地址空间就是访问资源的"窗口",有了这个"窗口"我们就可以通过其看到内部的资源数据,地址空间的核心是由虚拟地址到物理地址的映射(MMU负责-CPU内部)。

  2. 现在我们创建线程时,多个task_struct共享这个"窗口"(地址空间),并且将资源分配给他们,这样我们就通过进程模拟实现出线程了。

  3. 对于Linux系统来说,不论你是线程还是进程都是一律看做为执行流,只需执行处理对于的代码和数据即可,你的mm_struct由一个task_struct管理我们叫做进程,由多个管理我们叫做线程,因此执行流 <= 进程的,Linux中将一个个的task_struct(线程)我们叫做轻量级进程(LWP)。

    1. 单进程(传统进程):一个task_struct + 独立mm_struct。

    2. 线程组(多线程):多个task_struct + 共享同一个mm_struct

  4. 对于CPU来说,在其内部有一个运行队列run_queue,不断的计算处理一个个task_struct的代码和数据,这也就是为什么说线程是CPU的最小调度单位,CPU只需要处理这一个个轻量级进程(LWP)即可!

初步理解:

  • 结论一:Linux的"线程"可以采用进程进行模拟实现。

  • 结论二:对内存资源划分本质就是对虚拟地址的划分,因此虚拟地址就是内存资源的代表。

  • 结论三:代码区怎么划分,以函数划分,函数的入口地址就是虚拟地址的集合,让未来不同的线程执行ELF文件中的不同函数即可。

  • 结论四:在不同于Linux的平台,如Windows下对于线程的描述与管理重新设计了TCB,而一个进程中组织着需要使用到的TCB线程数组。Linux的选择是复用进程的代码与结构设计线程,因此Linux的线程就是轻量级进程(LWP),或者说由轻量级进程(LWP)实现。

  • 结论五:

    • 进程强调独占,部分资源共享(如通信)。

    • 线程强调共享,部分独占。

2. 分页式存储

分页式存储是操作系统管理内存的核心技术之一,旨在解决内存碎片化问题并提升资源利用率。

如果在没有虚拟内存和分页机制的情况下,每⼀个用户程序在物理内存上所对应的空间必须是连续的,如下图:

不同程序退出时间和内存占用大小的差异,会导致空闲内存被分割为不连续的小块(外部碎片化)。而当分配的内存块大于实际需求时,则会产生内部碎片化。

3.1 页表

3.1.1 页框/页

我们知道磁盘被划分为4KB为单位的数据块,磁盘的数据(可执行程序等等)需要加载到物理内存,这就需要物理内存块也划分为4KB的内存块方便IO以4KB为单位进行交换!而将物理内存以4KB划分后的每一块我们叫做页框!

把物理内存按照⼀个固定的长度的页框进行分割,有时叫做物理页。每个页框包含⼀个物理页(page)。一个页的大小等于页框的大小。大多数 32位 体系结构支持 4KB 的页,而 64位 体系结构⼀般会支持 8KB 的页。区分一页和一个页框是很重要的:

页框是⼀个存储区域;

而页是⼀个数据块,可以存放在任何页框或磁盘中

现在设定我们的物理内存大小为4GB, 4GB/4KB = 1048576个页框这么多页框都是由操作系统划分的,那么操作系统就需要对这些页框进行管理!先描述,在组织!

cpp 复制代码
struct page mem[1048576];
/* 每一个page都有下标!那么起始物理地址就知道了 */
/* 具体的物理地址 = 起始物理地址 + 页内(4KB)偏移 */
cpp 复制代码
struct page_info {
    unsigned short flags;
    unsigned short count;
    struct inode * inode;
    unsigned long offset;
    struct page_info * next_same_inode;
    struct page_info * prev_same_inode;
    struct page_info * next_hash;
    struct page_info * prev_hash;
    struct wait_queue *wait;
};

进程在申请物理内存的时候是在干什么?

  • 查数组,改page结构体中的内容!

  • 建立内核数据结构的对应关系!

3.1.2 页表机制

我们前面了解过页表是虚拟地址和物理地址的映射表,假设我们每一个地址都需要映射,在 32 位系统中,地址的长度是 4 个字节,那么页表中的每⼀个表项就是占用 4 个字节。所以页表占据的总空间大小就是: 1048576*4 = 4MB 的大小。也就是说映射表自己本身,就要占用 4MB / 4KB = 1024 个物理页, 这显然太多了,因此页表绝对不是一张单独的页表!

页目录- 页表 机制

  • 这里的每⼀个表,就是真正的页表,所以⼀共有 1024 个页表。⼀个页表自身占用 4KB ,那么1024 个页表⼀共就占用了 4MB 的物理内存空间,和之前没差别啊?

  • 从总数上看是这样,但是⼀个应用程序是不可能完全使用全部的4GB空间的,也许只要几十个页表就可以了。例如:⼀个用户程序的代码段、数据段、栈段,⼀共就需要 10 MB 的空间,那么使用 3 个页表就足够了。

  • 页目录结构管理着这1024个页表,所以操作系统在加载用户程序的时候,不仅仅需要为程序内容来分配物理内存,还需要为用来保存程序的页目录和页表分配物理内存 。

3.1.3 从虚拟地址到物理地址的转换

在32位机器下,4字节的虚拟地址共有32个比特位,前10位比特位用于表示页目录的起始地址,中间10比特位用于表示页表的起始地址,最后12位用于表示页框的偏移量:

  • 页目录索引(前 10 位):页目录是用来存放页表起始地址的一张表。前 10 位比特位能表示2^10个不同的索引,也就是可以指向2^10个不同的页表。每个页表的起始地址存于页目录的对应项里。

  • 页表索引(中间 10 位):页表是用来存放页框起始地址的一张表。中间 10 位比特位能够表示2^10个不同的索引,也就是可以指向2^10个不同的页框。每个页框的起始地址存于页表的对应项中。

  • 页内偏移(最后 12 位):最后 12 位比特位能够表示2^12个不同的偏移量。因为2^12=4096字节,也就是 4KB,所以通常页的大小是 4KB。借助页内偏移,可以在一个页框内定位到具体的字节位置。

MMU硬件转换自动完成的两个阶段分别是:

  1. 查找到虚拟地址对应的页框

  2. 根据虚拟地址低12位,作为页内偏移,访问具体字节!

总结:

  1. 细节1:申请内存-->查找struct page mem[1048576]数组-->找到没有被使用的page-->page index-->物理页框地址!

  2. 细节2:写时拷贝,缺页中断,内存申请等等,背后都存在着建立新页表和新映射关系的操作!

  3. 细节3:进程:一张页目录+n张页表构建的映射体系,虚拟地址就是索引,物理地址页框就是索引的目标。虚拟地址的低12位 + 页框地址 = 物理地址!

  • 虚拟地址与资源:虚拟地址是程序执行流看到的资源代表,资源划分本质是虚拟地址空间划分,资源共享即虚拟地址共享。

  • 数据结构:mm_struct 结合 vm_area_struct 用于管理虚拟地址空间,统计资源数据并呈现整体信息。

  • 页表功能:页表作为 "虚拟到物理的地图",实现虚拟地址到物理地址的映射,是内存管理的关键环节。

3.2 TLB快表

我们每次通过MMU进行一次页目录,页表到页框的查询就是一次IO操作,当我们大量去查询时就会带来效率问题,因此为了解决这个频繁的查询请求,我们将最近查询的结果在TLB中缓存起来,这样在查询之前MMU会去TLB查看是否存在,如果存在直接使用,如果没有再去查询后加入到TLB中。

3.3 缺页异常(Page Fault):

  • 定义:CPU 向 MMU 发送的虚拟地址,在 TLB 和页表中均无对应物理页时触发,是硬件中断触发、可通过软件逻辑纠正的错误。

  • 触发场景:目标内存页在物理内存中无对应物理页,或存在但无访问权限,导致 CPU 无法获取数据。

  • 处理流程:因 CPU 无数据无法计算,进程从用户态切换到内核态,缺页中断交由内核的 Page Fault Handler(缺页异常处理程序) 处理。

缺页中断处理程序(PageFaultHandler)处理的三种缺页类型:

  • Hard Page Fault(硬缺页错误 / 主要缺页错误):物理内存无对应物理页,需从磁盘读取数据到内存,再由 MMU 建立虚拟地址与物理地址的映射。

  • Soft Page Fault(软缺页错误 / 次要缺页错误):物理内存存在对应物理页(其他进程调入),仅需 MMU 建立映射,无需磁盘读取,常见于多进程共享内存区域场景。

  • Invalid Page Fault(无效缺页错误):因进程越界访问内存、解引用空指针等非法操作触发,会报segment fault错误并中断进程。

4. 线程理解

  • 线程资源划分:线程划分资源时,通过划分虚拟地址空间获取合法虚拟地址,而页表是虚拟地址到物理地址的映射载体,划分页表本质就是管理虚拟地址空间。

  • 线程资源共享:地址空间共享的核心是页表条目的共享。页表条目记录虚拟地址与物理资源(如内存页框)的映射关系,共享页表条目意味着多个线程可通过相同虚拟地址访问同一物理资源,实现资源共享。

4.1 线程优点

  1. 创建新的线程比进程的代价小,一个线程只需在原有进程的基础上创建一个PCB并且进行对应的资源划分即可。

  2. 线程与进程切换相比较:

    1. 虚拟内存映射机制:

      1. 线程属于同一进程,共享进程虚拟内存空间。切换时,因虚拟地址空间不变,无需重新构建页表等地址映射关系。

      2. 进程拥有独立虚拟地址空间。进程切换时,新进程的虚拟地址空间与原进程不同,必须重新加载页目录、重建页表映射等,开销更大。

    2. 上下文切换内容:

      1. 线程切换:主要保存 / 恢复线程独有的上下文,如寄存器值、栈指针、线程状态等,不涉及虚拟内存空间的重新初始化。

      2. 进程切换:除了保存 / 恢复通用寄存器、程序计数器等基础上下文,还需处理虚拟内存相关资源(如页表、内存管理数据结构),以及进程独有的资源(如文件描述符状态等),切换成本显著高于线程。进程切换会导致TLB和Cache失效,下次需要重新加载!

注:Cache 硬件是一种高速存储器。

  1. 线程占用的资源少于进程,并且线程可以充分发挥CPU并行的作用。

4.2 线程缺点

  1. 异常方面

    1. 如一个单个线程出现异常,如除0,访问野指针错误,那么其他线程和当前进程都会崩溃!
cpp 复制代码
/* 1. 线程异常 */

void *routinue1(void *args)
{
    int cnt = 3;
    while(cnt--)
    {
        std::cout << "我是新线程, 我的name:" << (char*)args << std::endl;
        sleep(1);
    }
    /* 野指针操作--> 异常 */
    int* p = nullptr;
    *p = 100;
    return nullptr;
}

void *routinue2(void *args)
{
    int cnt = 3;
    while(cnt--)
    {
        std::cout << "我是新线程, 我的name:" << (char*)args << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    /* 创建两个线程 */
    pthread_t tid1, tid2;

    pthread_create(&tid1, nullptr, routinue1, (void*)"thread-1");
    pthread_create(&tid2, nullptr, routinue2, (void*)"thread-2");

    while(true)
    {
        std::cout << "我是主线程, 我的pid:" << getpid() << std::endl;
        sleep(1);
    }

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    return 0;
}
  1. 资源方面

    1. 多个线程彼此是可以使用其他线程的资源的,只需要有一个可以看见其他线程资源的窗口,具有线程不安全性。
  2. 健壮性方面

    1. 线程之间缺乏保护机制,一个线程的错误可能会波及到其他线程,导致整个系统的健壮性下降。

4.3 线程和进程

  • 进程:作为资源分配基本单位,系统为进程分配内存、文件描述符等资源。

  • 线程:作为调度基本单位,线程切换开销低于进程。线程共享进程数据(如全局变量、内存空间),同时拥有私有数据:

    • 线程 ID:唯一标识线程。

    • 一组寄存器:保存线程运行时的寄存器状态,用于上下文切换。

    • 栈:线程独立的调用栈,存储局部变量等。

    • errno:记录线程错误信息。

    • 信号屏蔽字:线程对信号的屏蔽设置。

    • 调度优先级:决定线程调度顺序的优先级参数。

4.3.1 线程的函数共享

cpp 复制代码
/* 2. 线程函数共享 */
/* 将tid转换为16进制 */
std::string toHex(pthread_t tid)
{
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "0x%lx", tid);
    return buffer;
}

/* 可重入函数 */
void *routinue(void *args)
{
    std::string name = static_cast<char*>(args);
    while(true)
    {
        sleep(1);
        std::cout << "我是新线程, 我的name:" << name << ",我的tid:"  << toHex(pthread_self()) << std::endl;
    }
    return nullptr;
}

int main()
{
    /* 创建两个线程 */
    pthread_t tid1, tid2;

    pthread_create(&tid1, nullptr, routinue, (void*)"thread-1");
    pthread_create(&tid2, nullptr, routinue, (void*)"thread-2");

    while(true)
    {
        std::cout << "我是主线程, 我的pid:" << getpid() << std::endl;
        sleep(1);
    }

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

    return 0;
}

4.3.2 线程共享全局变量

cpp 复制代码
/* 3. 线程全局变量共享 */
/* 将tid转换为16进制 */
int val = 100;
std::string toHex(pthread_t tid)
{
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "0x%lx", tid);
    return buffer;
}

/* 可重入函数 */
void *routinue1(void *args)
{
    std::string name = static_cast<char*>(args);
    while(true)
    {
        sleep(1);
        std::cout << "我是新线程, 我的name:" << name << ",我的tid:"  << toHex(pthread_self()) << "全局变量(会修改):" << val << std::endl;
        ++val;
    }
    return nullptr;
}

/* 可重入函数 */
void *routinue2(void *args)
{
    std::string name = static_cast<char*>(args);
    while(true)
    {
        sleep(1);
        std::cout << "我是新线程, 我的name:" << name << ",我的tid:"  << toHex(pthread_self()) << "全局变量(观察):" << val << std::endl;
    }
    return nullptr;
}

int main()
{
    /* 创建两个线程 */
    pthread_t tid1, tid2;

    pthread_create(&tid1, nullptr, routinue1, (void*)"thread-1");
    pthread_create(&tid2, nullptr, routinue2, (void*)"thread-2");

    while(true)
    {
        std::cout << "我是主线程, 我的pid:" << getpid() << std::endl;
        sleep(1);
    }

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

    return 0;
}
  • 刚刚上面演示了 一个全局变量被多线程共享,那么如果我们想让多个线程各自私有这一个变量,该怎么做呢? --> g++有一个编译选项 __thread

  • 用__thread修饰这个全局变量即可。运行后,主线程和新线程gval的地址也不一样了。这种情况叫线程的局部存储,原始代码里只看到一个gval,但是他们用的是各自的gval

  • 这种情况只在Linux中有效。__thread只能用来修饰内置类型。

cpp 复制代码
// 线程局部存储
// 编译型关键字,给每个线程来一份
// 虽然用的是同一份值和变量名,但是编译的时候把它地址编程不同
// 此时就叫做线程局部存储
// 注意:__thread 只能修饰内置类型
__thread int val = 100; // 此时主线程和新线程看到的地址不同

/* 将tid转换为16进制 */
std::string toHex(pthread_t tid)
{
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "0x%lx", tid);
    return buffer;
}

/* 可重入函数 */
void *routinue1(void *args)
{
    std::string name = static_cast<char*>(args);
    while(true)
    {
        sleep(1);
        std::cout << "我是新线程, 我的name:" << name << ",我的tid:"  << toHex(pthread_self()) << "全局变量(会修改):" << val << std::endl;
        ++val;
    }
    return nullptr;
}

/* 可重入函数 */
void *routinue2(void *args)
{
    std::string name = static_cast<char*>(args);
    while(true)
    {
        sleep(1);
        std::cout << "我是新线程, 我的name:" << name << ",我的tid:"  << toHex(pthread_self()) << "全局变量(观察):" << val << std::endl;
    }
    return nullptr;
}

int main()
{
    /* 创建两个线程 */
    pthread_t tid1, tid2;

    pthread_create(&tid1, nullptr, routinue1, (void*)"thread-1");
    pthread_create(&tid2, nullptr, routinue2, (void*)"thread-2");

    while(true)
    {
        std::cout << "我是主线程, 我的pid:" << getpid() << std::endl;
        sleep(1);
    }

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

    return 0;
}

5. 线程控制

POSIX 线程库

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以"pthread_"打头的

  • 要使用这些函数库,要通过引入头文件<pthread.h>

  • 链接这些线程函数库时要使用编译器命令的"-lpthread"选项

5.1 创建一个线程

cpp 复制代码
功能:创建⼀个新的线程
原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void*), void *arg);
参数: 
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数,这个参数可以是对象也可以是string等等
返回值:成功返回0;失败返回错误码  
cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>

void *ThreadRun(void *args)
{
    std::string name = (char*)args;
    while(true)
    {
        std::cout << "我是新线程,我们名称是" << name << std::endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRun, (void*)"Thread-1");

    while(true)
    {
        std::cout << "我是主线程" << std::endl;
        sleep(1);
    }
    return 0;
}

由于在Linux中没有线程的概念,只存在轻量级进程(LWP),因此需要封装一个线程库,这个线程库我们需要动态链接!

bash 复制代码
Thread:Thread.cc
    g++ -o $@ $^ -lpthread
.PHONY:clean
clean:
    rm -f Thread
  • 查看当前系统中存在的线程状态:
bash 复制代码
ps -aL

我们可以看见主线程和新线程的PID都是一致的,这说明他们属于同一个进程,LWP则是线程在CPU中调度识别的标识符,这边注意LWP和tid是不同的两个东西!

bash 复制代码
这个函数是获取线程ID的,这个ID是由封装库pthread维护的和底层内核CPU调用线程所用识别不同线程LWP是不一样的!

线程创建运行完之后有点像僵尸进程一样需要主线程进行等待,目的也是为了防止内存泄漏!

bash 复制代码
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数: thread:线程ID
retval:它指向⼀个指针,后者指向线程的返回值(二级指针是因为线程执行的函数返回值是void*, 防止形参是实参的拷贝需要传入二级指针获取)
返回值:
成功返回0;失败返回错误码  
  • 调用该函数的线程将挂起等待,直到 id 为 thread 的线程终止。thread 线程以不同的方法终止,通过 pthread join 得到的终止状态是不同的,总结如下:

  • 如果 thread 线程通过 return 返回,value ptr 所指向的单元里存放的是 thread 线程函数的返回值。

  • 如果 thread 线程被别的线程调用 pthread cancel 异常终掉,value_ptr 所指向的单元里存放的是常数PTHREAD CANCELED(void*(-1))

  • 如果 thread 线程是自己调用 pthread exit 终止的,value ptr 所指向的单元存放的是传给 pthread exit 的参数。

  • 如果对 thread 线程的终止状态不感兴趣,可以传 NULL 给 value_ptr 参数

见一见 C++11 的线程库

cpp 复制代码
/* c++11 线程库 */
void *routinue()
{
    while(true)
    {
        std::cout << "我是新线程,我的PID是 " << getpid() << std::endl;
        sleep(1);
    }
}

int main()
{
    std::thread t(routinue);
    while(true)
    {
        std::cout << "我是主线程" << std::endl;
        sleep(1);
    }
    t.join();

    return 0;
}  

c++11封装线程库是为了多平台的代码可移植性。

Create + join + 获取返回值

cpp 复制代码
void *routinue(void *args)
{
    std::string name = (const char*)args;
    int cnt = 3;
    while(cnt--)
    {
        std::cout << "我是一个新线程, 我的tid是:" << pthread_self() << std::endl;
        sleep(1);
    }
    return (void*)100;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, routinue, (void*)"thread-1");
    void* ret = nullptr;
    sleep(5);
    pthread_join(tid, &ret);
    std::cout << (long long)ret << std::endl;
    return 0;
}

5.2 线程终止

我们终止一个线程而不终止整个进程有以下三种方法:

  1. 线程函数的return

  2. 线程调用pthread_exit终止自己。

  3. 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。

bash 复制代码
功能:线程终止
原型:
    void pthread_exit(void *value_ptr);
参数:
    value_ptr:不要指向一个局部变量
返回值:
    无返回值,和进程一样,一旦结束无非返回调用它的调用者(本身)。

需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

cpp 复制代码
/* 对象 */
class RetObject
{
public:
    RetObject(int a, int b)
    :_a(a)
    ,_b(b)
    {}

    int Excute()
    {
        return _a + _b;
    }

    ~RetObject()
    {}
private:
    int _a;
    int _b;
};

int ret = 0;

void *routine(void *args)
{
    RetObject* ro = static_cast<RetObject*>(args);
    ret = ro->Excute();
    std::cout << ret << std::endl;
    pthread_exit((void*)ret);
    // return (void*)ret;
}

int main()
{
    pthread_t tid;
    RetObject* ro = new RetObject(10, 20);
    pthread_create(&tid, nullptr, routine, (void*)ro);
    void *ret = nullptr;
    pthread_join(tid, &ret);
    std::cout << (long long)ret << std::endl;
    return 0;
}

bash 复制代码
功能:取消⼀个执行中的线程
原型:
int pthread_cancel(pthread_t thread);
参数: thread:线程ID
返回值:成功返回0;失败返回错误码  
cpp 复制代码
void *routine(void *args)
{
    while(true)
    {
        std::cout << "我是一个新线程, 我的tid是:" << pthread_self() << std::endl;
        sleep(1);
    }
    return (void*)123;
}

int main()
{
    pthread_t tid;
    RetObject* ro = new RetObject(10, 20);
    pthread_create(&tid, nullptr, routine, (void*)ro);
    pthread_cancel(tid);
    void *ret = nullptr;
    pthread_join(tid, &ret);
    std::cout << (long long)ret << std::endl;
    return 0;
}
  • 线程取消一个就join一个。由上图可知,线程被取消后,线程的退出结果是 -1

  • -1对应pthread库中的一个宏 #define PTHREAD_CANCELD ((void*)-1)

5.3 线程分离

  • 默认情况下新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,回收释放资源,防止系统内存泄漏。

  • 但是如果我们并不关心线程执行的返回值,我们能否直接让线程退出时自己释放自己的资源,如果要实现这个我们就需要分离线程。

  • 分离和joinable是对立的属性,一个线程只能是其中一种。

  • 如果一个线程被分离了,它就不需要join,但是线程依旧属于进程内部!

cpp 复制代码
int pthread_detach(pthread_t thread);
int pthread_detach(pthread_self());
// 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离
cpp 复制代码
/* 线程分离 */
void *routine(void *args)
{
    std::cout << "我是新线程, 我的tid:" << pthread_self() << std::endl;
    std::cout << "我要开始分离了!" << std::endl;
    sleep(1);
    pthread_detach(pthread_self());
    return 0;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, routine, (void*)"thread-1");

    int n = pthread_join(tid, nullptr);
    std::cout << n << std::endl;
    return 0;
}
  • 线程自己把线程自己分离,主线程进入join 失败了然后退出,因此没有阻塞

  • 现在让主线程把线程分离,此时主线程也会退出,说明线程一旦被分离,就不能 Join 了

  • 主线程不会卡在 Join,而是会继续往后走,主线程结束了,整个进程就结束了。这样的话,假如我们还有其他线程,可能它们还没起来就死亡了

  • 所以分离线程后,主线程就可以做自己的事了,不用管新线程,即使新线程分离,只要分离的线程异常了,还是会影响整个进程。

  • 注意:在多执行流情况下,主执行流是最后退出的

5.4 多线程练习

cpp 复制代码
/* 创建多线程 */
// /* 将tid转换为16进制 */
std::string toHex(pthread_t tid)
{
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "0x%lx", tid);
    return buffer;
}

/* 可重入函数 */
void *routinue(void *args)
{
    std::string name = static_cast<char*>(args);
    sleep(1);
    std::cout << "我是新线程, 我的name:" << name << ",我的tid:"  << toHex(pthread_self())  << std::endl;
    return nullptr;
}

int main()
{
    /* vector 管理起来线程tid */
    std::vector<pthread_t> v;
    for(int i = 0; i < 10; ++i)
    {
        pthread_t tid;
        // char buffer[64]; // 会导致线程安全问题,多个线程访问的是同一资源,资源不一致
        char* buffer = new char[64];
        snprintf(buffer, 64, "thread-%d", i);
        pthread_create(&tid, nullptr, routinue, (void*)buffer);
        v.push_back(tid);
    }

    for(auto& e : v)
    {
        int n = pthread_join(e, nullptr);
        if(n == 0)
            std::cout << "join success!" << std::endl;
    }
    return 0;
}

6. 线程ID及进程地址空间布局

Linux没有真正的线程,由轻量级进程模拟实现,OS提供的接口不易直接提供线程接口,而是封装轻量级进程形成用户需要使用的原生线程库!

我们的进程是可执行程序,并且是ELF格式的,pthread.so是动态库,我们的可执行程序加载,形成进程,动态链接和动态地址重定向,并且要将动态库加载到内存,映射到当前进程的地址空间上!进程自己的代码区可以访问到pthread库内部的函数或者数据!

创建线程要去调用映射的方法,那么线程的概念就是在库中维护的,在库中就一定存在多个被创建好的线程,库需要管理这些线程!先描述,在组织!

cpp 复制代码
pthread_create();
|
|
struct tcb
{
    // 线程的属性
    // 线程的状态  joinoable
    // 线程id
    // 线程的独立栈结构
    // 线程栈的大小
    // ...
};

我们每创建一个线程,库内部会创建一个管理的部分tcb。

  • TID: 并不是LWP,而是线程库中当前线程管理模块的起始地址!struct_pthread中存在一个void* ret用于接收调用函数的返回值,因此我们需要使用pthread_join去获取ret并且释放这块管理模块的空间!pthread_join通过传入的参数tid获取该管理模块的地址!线程调用结束底层内核的LWP会被释放,但是动态库的管理模块还存在着!

  • 线程返回值:线程执行完将返回值写入到线程的控制管理模块,主线程使用pthread_join进行获取

  • 线程分离:在线程控制块中的线程状态设置一下!

  • Linux的所有线程都在线程库中。

  • 线程局部存储的使用:我们前面使用到的__pthread int a = 0;

  • 用户线程 Vs LWP 如何联动

    • 每个线程都有自己独立的栈空间,栈空间存在于动态库的管理模块内部!

    • pthread_create

      • 在库中创建对应线程的管理模块

      • 在内核中创建对应的轻量级线程,调用系统调用clone(),将对应的栈和调用的函数传入,线程在CPU调用就可以执行自己的代码和使用自己的数据了!

  • Linux用户级线程 :内核级线程 = 1 :1

7. 线程封装

基于我们上面所了解的内容,我们是否也可以实现一个和C++11类似的线程对象呢,我们想要实现的效果如下:

cpp 复制代码
#ifndef __MYPTHREAD__HPP
#define __MYPTHREAD__HPP

#include <iostream>
#include <pthread.h>
#include <string>
#include <functional>
#include <cstring>
#include <unistd.h>
#include <vector>

namespace ThreadSpace
{
    /* 用于标识线程的name */
    static int num = 1;

    /* 用于标识当前线程的状态 */
    enum class STATUS
    {
        NEW,    /* 新创建 */
        RUNING, /* 运行中 */
        STOP    /* 暂停中 */    
    };
    /* 包装器:包装回调函数 */
    using func_t = std::function<void()>;

    /* Thread类 */
    class Thread
    {
        /* 设置分离状态 */
        void EnableJoin()
        {
            _is_detachable = true;
        }

    public:
        Thread(func_t routine, bool is_detach = false) /* 缺省参数:回调函数, 初始是否分离状态 */
            : _tid(0)
            , _pid(getpid())
            , _name("thread-" + num++)
            , _is_detachable(is_detach) /* 默认不分离 */
            , _routine(routine)
            , _ret(nullptr)
            , _status(STATUS::NEW)
        {
        }

        /* 函数回调接口 */
        static void *Routine(void *args) /* 必须使用静态成员函数,否则带有this指针 */
        {
            Thread *t = static_cast<Thread *>(args);
            t->_routine(); /* 回调函数 */
            /* 自然结束更新状态 */
            t->_status = STATUS::STOP;
            /* 返回值 */
            return nullptr;
        }

        /* 创建线程 */
        bool Start()
        {
            /* 如果线程已经处于运行状态不要重新运行 */
            if(_status == STATUS::RUNING)
            {
                /* 属于重复运行情况 */
                std::cout << "Runing again!" << std::endl;
                return false;
            }

            /* 创建一个线程 */
            int n = pthread_create(&_tid, nullptr, Routine, static_cast<void *>(this));
            if (n != 0)
            {
                std::cout << "pthread_create fail!" << strerror(n) << std::endl;
                return false;
            }
            /* 更改状态为Runing */
            _status = STATUS::RUNING;
            std::cout << "pthread_create success!" << std::endl;
    
            /* 查看是否初始为分离状态,如果是进行分离 */
            if(_is_detachable == true)
                pthread_detach(_tid);
            return true; 
        }

        /* 分离线程 */
        bool Detach()
        {
            if(_is_detachable == true)
            {
                /* 如果线程已经分离,不需要重新分离 */
                std::cout << "Detach again!" << std::endl;
                return false;
            }

            /* 进行分离 */
            /* 1. 分离 */
            int n = pthread_detach(_tid);
            if(n != 0)
            {
                std::cout << "pthread_detach fail" << strerror(n) << std::endl;
                return false;
            }
            /* 2. 设置分离状态 */
            EnableJoin();
            std::cout << "pthread_detach success!" << std::endl;
            return true;
        }

        bool Join()
        {
            /* 如果线程已经分离,就不要join */
            if (_is_detachable == true)
            {
                std::cout << "pthread is detach, not need join!" << std::endl;
                return false;
            }

            int n = pthread_join(_tid, &_ret);
            if (n != 0)
            {
                std::cout << "pthread_join fail!" << strerror(n) << std::endl;
                return false;
            }
            /* 调整进程状态 */
            _status = STATUS::STOP;
            std::cout << "pthread_join success!" << std::endl;
            return true;
        }

        bool Cancel()
        {
            /* 取消的进程状态必须属于执行中的进程 */
            if(_status != STATUS::RUNING)
            {
                std::cout << "you cancel thread is not runing!" << std::endl;
                return false;
            }

            int n = pthread_cancel(_tid);
            if (n != 0)
            {
                std::cout << "pthread_cancel fail" << strerror(n) << std::endl;
                return false;
            }
            /* 调整进程状态 */
            _status = STATUS::STOP;
            std::cout << "pthread_cancel success!" << std::endl;
            return true;
        }

        void* GetRet()
        {
            return _ret;
        }

        ~Thread()
        {
        }

    private:
        pthread_t _tid;    /* 线程tid */
        pid_t _pid;        /* 进程pid */
        std::string _name; /* 线程name */
        bool _is_detachable;  /* 线程是否分离 */
        func_t _routine;   /* 线程执行的函数 */
        void *_ret;        /* 用于获取返回值 */
        STATUS _status;    /* 线程状态 */
    };
}

namespace ThreadSpace_T
{
    /* 用于标识线程的name */
    static int num = 1;

    /* 用于标识当前线程的状态 */
    enum class STATUS
    {
        NEW,    /* 新创建 */
        RUNING, /* 运行中 */
        STOP    /* 暂停中 */    
    };

    /* Thread类 */
    template<class T>
    class Thread
    {
        /* 包装器:包装回调函数 */
        using func_t = std::function<void(T)>;
        /* 设置分离状态 */
        void EnableJoin()
        {
            _is_detachable = true;
        }

    public:
        Thread(func_t routine, T data, bool is_detach = false) /* 缺省参数:回调函数, 初始是否分离状态 */
            : _tid(0)
            , _pid(getpid())
            , _name("thread-" + num++)
            , _is_detachable(is_detach) /* 默认不分离 */
            , _routine(routine)
            , _ret(nullptr)
            , _status(STATUS::NEW)
            , _data(data)
        {
        }

        /* 函数回调接口 */
        static void *Routine(void *args) /* 必须使用静态成员函数,否则带有this指针 */
        {
            Thread<T> *t = static_cast<Thread<T> *>(args);
            t->_routine(t->_data); /* 回调函数 */
            /* 自然结束更新状态 */
            t->_status = STATUS::STOP;
            /* 返回值 */
            return nullptr;
        }

        /* 创建线程 */
        bool Start()
        {
            /* 如果线程已经处于运行状态不要重新运行 */
            if(_status == STATUS::RUNING)
            {
                /* 属于重复运行情况 */
                std::cout << "Runing again!" << std::endl;
                return false;
            }

            /* 创建一个线程 */
            int n = pthread_create(&_tid, nullptr, Routine, this);
            if (n != 0)
            {
                std::cout << "pthread_create fail!" << strerror(n) << std::endl;
                return false;
            }
            /* 更改状态为Runing */
            _status = STATUS::RUNING;
            std::cout << "pthread_create success!" << std::endl;
    
            /* 查看是否初始为分离状态,如果是进行分离 */
            if(_is_detachable == true)
                pthread_detach(_tid);
            return true; 
        }

        /* 分离线程 */
        bool Detach()
        {
            if(_is_detachable == true)
            {
                /* 如果线程已经分离,不需要重新分离 */
                std::cout << "Detach again!" << std::endl;
                return false;
            }

            /* 进行分离 */
            /* 1. 分离 */
            int n = pthread_detach(_tid);
            if(n != 0)
            {
                std::cout << "pthread_detach fail" << strerror(n) << std::endl;
                return false;
            }
            /* 2. 设置分离状态 */
            EnableJoin();
            std::cout << "pthread_detach success!" << std::endl;
            return true;
        }

        bool Join()
        {
            /* 如果线程已经分离,就不要join */
            if (_is_detachable == true)
            {
                std::cout << "pthread is detach, not need join!" << std::endl;
                return false;
            }

            int n = pthread_join(_tid, &_ret);
            if (n != 0)
            {
                std::cout << "pthread_join fail!" << strerror(n) << std::endl;
                return false;
            }
            /* 调整进程状态 */
            _status = STATUS::STOP;
            std::cout << "pthread_join success!" << std::endl;
            return true;
        }

        bool Cancel()
        {
            /* 取消的进程状态必须属于执行中的进程 */
            if(_status != STATUS::RUNING)
            {
                std::cout << "you cancel thread is not runing!" << std::endl;
                return false;
            }

            int n = pthread_cancel(_tid);
            if (n != 0)
            {
                std::cout << "pthread_cancel fail" << strerror(n) << std::endl;
                return false;
            }
            /* 调整进程状态 */
            _status = STATUS::STOP;
            std::cout << "pthread_cancel success!" << std::endl;
            return true;
        }

        void* GetRet()
        {
            return _ret;
        }

        ~Thread()
        {
        }

    private:
        pthread_t _tid;    /* 线程tid */
        pid_t _pid;        /* 进程pid */
        std::string _name; /* 线程name */
        bool _is_detachable;  /* 线程是否分离 */
        func_t _routine;   /* 线程执行的函数 */
        void *_ret;        /* 用于获取返回值 */
        STATUS _status;    /* 线程状态 */
        T _data;           /* 对象类型 */
    };
}

#endif
相关推荐
2035去旅行9 小时前
bootcmd 详细讲解
linux·嵌入式·arm·soc
陈希1239 小时前
Linux 防火墙配置实战:firewalld、iptables 与云安全组
linux·运维·服务器
女神下凡10 小时前
芯参谋(95):嵌入式文件系统_开发指南
linux·服务器·arm开发·驱动开发·设计规范
且从容.13 小时前
Ubuntu安装flume
linux·ubuntu·flume
承渊政道14 小时前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
且从容.17 小时前
ubuntu安装flink
linux·ubuntu·flink
爱和冰阔落18 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨18 小时前
WSL2的安装与使用
linux·wsl
微小的鱼19 小时前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng19 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器