Linux多线程【线程控制】

news2024/12/24 2:51:25

✨个人主页: 北 海
🎉所属专栏: Linux学习之旅
🎃操作环境: CentOS 7.6 阿里云远程服务器

成就一亿技术人


文章目录

  • 🌇前言
  • 🏙️正文
    • 1、线程知识补充
      • 1.2、线程私有资源
      • 1.3、线程共享资源
      • 1.4、原生线程库
    • 2、线程控制接口
      • 2.1、线程创建
        • 2.1.1、一批线程
      • 2.2、线程等待
      • 2.3、线程终止
      • 2.4、线程实战
      • 2.5、其他接口
        • 2.5.1、关闭线程
        • 2.5.2、获取线程ID
        • 2.5.3、线程分离
    • 3、深入理解线程
      • 3.1、理解线程库及线程 ID
      • 3.2、理解线程独立栈
      • 3.3、理解线程局部存储
  • 🌆总结


🌇前言

线程是进程内部的一个执行流,作为 CPU 运行的基本单位,对于线程的合理控制与任务的执行效率息息相关,因此掌握线程基本操作(线程控制)是很有必要的

图示


🏙️正文

1、线程知识补充

在正式介绍线程控制相关接口前,需要先补充一波线程相关知识

1.2、线程私有资源

在 Linux多线程【初识线程】 中我们得出了一个结论:Linux 中没有真线程,只有复用 PCB 设计思想的 TCB 结构

图示

因此 Linux 中的线程本质上就是 轻量级进程(LWP),一个进程内的多个线程看到的是同一个进程地址空间,所以所有的线程可能会共享进程的大部分资源

但是如果多个执行流(多个线程)都使用同一份资源,如何确保自己的相对独立性呢?

  • 相对独立性:线程各司其职,不至于乱成一锅粥

图示

显然,多线程虽然共同 “生活” 在一个进程中,但也需要有自己的 “隐私”,而这正是 线程私有资源

线程私有资源:

  1. 线程 ID:内核观点中的 LWP
  2. 一组寄存器: 线程切换时,当前线程的上下文数据需要被保存
  3. 线程独立栈: 线程在执行函数时,需要创建临时变量
  4. 错误码 errno: 线程因错误终止时,需要告知父进程
  5. 信号屏蔽字: 不同线程对于信号的屏蔽需求不同
  6. 调度优先级: 线程也是要被调度的,需要根据优先级进行合理调度

其中,线程 最重要 的资源是 一组寄存器(体现切换特性)和独立栈(体现临时运行特性)

这两个资源共同构成了最基本的线程

1.3、线程共享资源

除了上述提到的 线程私有资源 外,多线程还共享着进程中的部分资源

共享的定义:不需要太多的额外成本,就可以实现随时访问资源

基于 多线程看到的是同一块进程地址空间,理论上 凡是在进程地址空间中出现的资源,多线程都是可以看到的

但实际上为了确保线程调度、运行时的独立性,只能共享部分资源

这也就是线程中的栈区称作 “独立栈” 的原因:某块栈空间属于某个线程,其他线程是可以访问的,为了确保独立性,并不会这样做

进程地址空间 中,诸如 共享区、全局数据区等 这类天生自带共享属性的区域支持 多线程共享

图示

Linux 中,多线程共享资源如下

线程共享资源:

  1. 共享区、全局数据区、字符常量区、代码区: 常规资源共享区
  2. 文件描述符表: 进行 IO 操作时,无需再次打开文件
  3. 每种信号的处理方式: 多线程共同构成一个整体,信号的处理动作必须统一
  4. 当前工作目录: 即使是多线程,也是位于同一工作目录下
  5. 用户 ID 和 组 ID: 进程属于某个组中的某个用户,多线程也是如此

其中,线程 较重要 的共享资源是:文件描述符表

涉及 IO 操作时,多线程 多路转接 非常实用

进程和线程关系图示

图示

多个单线程进程单进程多线程 比较常用

1.4、原生线程库

在之前编译多线程相关代码时,我们必须带上一个选项:-lpthread,否则就无法使用多线程相关接口

图示
带上这个选项的目的很简单:使用 pthread 原生线程库

接下来对 原生线程库 进行一个系统性的理解

首先,在 Linux 中是没有真正意义上的线程的,有的只是通过进程模拟实现的线程(LWP

站在操作系统角度:并不会提供对线程控制的相关接口,最多提供轻量级进程操作的相关接口

但是对于用户来说,只认识线程,并不清楚轻量级进程

所以为了使用户能愉快的对线程进行操作,就需要对系统提供的轻量级进程操作相关接口进行封装:对下封装轻量级进程操作相关接口,对上给用户提供线程控制的相关接口

这里很好的体现了计算机界的哲学:通过添加一层软件层解决问题

图示

Linux 中,封装轻量级进程操作相关接口的库称为 pthread 库,即 原生线程库,这个库文件是所有 Linux 系统都必须预载的,用户使用多线程控制相关接口时,只需要指明使用 -lpthread 库,即可正常使用多线程控制相关接口


2、线程控制接口

有了前面知识的补充之后,接下来正式进入线程控制接口的学习

2.1、线程创建

要想控制线程,得先创建线程,对于 原生线程库 来说,创建线程使用的是 pthread_create 这个接口

#include <pthread.h>

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

先来认识一下函数中涉及的参数

参数1 pthread_t*线程 ID,用于标识线程,其实这玩意本质上就是一个 unsigned long int 类型

图示
注:pthread_t* 表明这是一个输出型参数,旨在创建线程后,获取新线程 ID

参数2 const pthread_attr_t*用于设置线程的属性,比如优先级、状态、私有栈大小,这个参数一般不考虑,直接传递 nullptr 使用默认设置即可

参数3 void *(*start_routine) (void *)这是一个很重要的参数,它是一个 返回值为 void* 参数也为 void* 的函数指针,线程启动时,会自动回调此函数(类似于 signal 函数中的参数2)

参数4 void*显然,这个类型与回调函数中的参数类型匹配上了,而这正是线程运行时,传递给回调函数的参数

返回值 int创建成功返回 0,失败返回 error number

明白创建线程函数的各个参数后,就可以尝试创建一个线程了

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

using namespace std;

void* threadRun(void *arg)
{
    while(true)
    {
        cout << "我是次线程,我正在运行..." << endl;
        sleep(1);
    }

    return nullptr;
}

int main()
{
    pthread_t t;
    pthread_create(&t, nullptr, threadRun, nullptr);

    while(true)
    {
        cout << "我是主线程 " << " 我创建了一个次线程 " << t << endl;
        sleep(1);
    }

    return 0;
}

非常简单的代码,此时如果直接编译会引发报错

图示

错误:未定义 pthread_create 这个函数

原因:没有指明使用 原生线程库,这是一个非常常见的问题

解决方法:编译时带上 -lpthread,指明使用 原生线程库

此时再编译就没有问题了

图示

可以通过 ps -aL 查看正在运行中的线程信息
图示

接下来解决一批衍生问题

1.如何验证 原生线程库 存在?
现在我们已经得到了一个链接 原生线程库 的可执行程序,可以通过 ldd 可执行程序 查看库的链接情况

ldd mythread

图示

可以看到,原生线程库路径: /lib64/libpthread.so.0

图示

足以证明原生线程库确确实实的存在于我们的系统中

2.为什么打印的次线程 ID 如此长?并且与 ps -aL 查出来的 LWP 不一致?

很长是因为它本质上是一个无符号长整型,至于为什么显示不一致的问题,需要到后面才能解答

3.程序运行时,主次线程的运行顺序?
线程的调度机制源于进程,而多进程运行时,谁先运行取决于调度器,因此主次线程运行的先后顺序不定,具体取决于调度器的调度

2.1.1、一批线程

接下来演示创建一批线程

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

using namespace std;

#define NUM 5

void* threadRun(void *name)
{
    while(true)
    {
        cout << "我是次线程 " << (char*)name << endl;
        sleep(1);
    }

    return nullptr;
}

int main()
{
    pthread_t pt[NUM];

    for(int i = 0; i < NUM; i++)
    {
        // 注册新线程的信息
        char name[64];
        snprintf(name, sizeof(name), "thread-%d", i + 1);
        pthread_create(pt + i, nullptr, threadRun, name);
    }

    while(true)
    {
        cout << "我是主线程,我正在运行" << endl;
        sleep(1);
    }

    return 0;
}

细节:传递 pthread_create 的参数1时,可以通过 起始地址+偏移量 的方式进行传递,传递的就是 pthread_t*

预期结果:打印 thread-1thread-2thread-3

实际结果:确实有五个次线程在运行,但打印的结果全是 thread-5

图示

原因:char name[64] 属于主线程中栈区之上的变量,多个线程实际指向的是同一块空间,最后一次覆盖后,所有线程都打印 thread-5

图示

这是由于多线程共享同一块区域引发的问题,解决方法就是在堆区动态匹配空间,使不同的线程读取不同的空间,这样就能确保各自信息的独立性

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

using namespace std;

#define NUM 5

void* threadRun(void *name)
{
    while(true)
    {
        cout << "我是次线程 " << (char*)name << endl;
        sleep(1);
    }
    delete[] (char*)name;

    return nullptr;
}

int main()
{
    pthread_t pt[NUM];

    for(int i = 0; i < NUM; i++)
    {
        // 注册新线程的信息
        char *name = new char[64];
        snprintf(name, 64, "thread-%d", i + 1);
        pthread_create(pt + i, nullptr, threadRun, name);
    }

    while(true)
    {
        cout << "我是主线程,我正在运行" << endl;
        sleep(1);
    }

    return 0;
}

现在程序能符合预期般运行了

图示
显然,线程每次的运行顺序取决于调度器

在上面的程序中,主线程也是在死循环式运行,假若主线程等待 3 秒后,再 return, 会发生什么呢?

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

using namespace std;

#define NUM 5

void* threadRun(void *name)
{
    while(true)
    {
        cout << "我是次线程 " << (char*)name << endl;
        sleep(1);
    }
    delete[] (char*)name;

    return nullptr;
}

int main()
{
    pthread_t pt[NUM];

    for(int i = 0; i < NUM; i++)
    {
        // 注册新线程的信息
        char *name = new char[64];
        snprintf(name, 64, "thread-%d", i + 1);
        pthread_create(pt + i, nullptr, threadRun, name);
    }

    // 等待 3 秒后 return
    sleep(3);
    return 0;
}

结果:程序运行 3 秒后,主线程退出,同时其他次线程也被强制结束了

图示

这是因为 主线程结束了,整个进程的资源都得被释放,次线程自然也就无法继续运行了

换句话说,次线程由主线程创建,主线程就得对他们负责,必须等待他们运行结束,类似于父子进程间的等待机制;如果不等待,就会引发僵尸进程问题,不过线程这里没有僵尸线程的概念,直接影响就是次线程也全部退出了

2.2、线程等待

主线程需要等待次线程,在 原生线程库 中刚好存在这样一个接口 pthread_join,用于等待次线程运行结束

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);

照例先来看看参数部分

参数1 pthread_t待等待的线程 ID,本质上就是一个无符号长整型类型;这里传递是数值,并非地址

参数2 void**这是一个输出型参数,用于获取次线程的退出结果,如果不关心,可以传递 nullptr

返回值:成功返回 0,失败返回 error number

函数原型很简单,使用也很简单,我们可以直接在主线程中调用并等待所有次线程运行结束

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

using namespace std;

#define NUM 5

void* threadRun(void *name)
{
    while(true)
    {
        cout << "我是次线程 " << (char*)name << endl;
        sleep(1);
    }
    delete[] (char*)name;

    return nullptr;
}

int main()
{
    pthread_t pt[NUM];

    for(int i = 0; i < NUM; i++)
    {
        // 注册新线程的信息
        char *name = new char[64];
        snprintf(name, 64, "thread-%d", i + 1);
        pthread_create(pt + i, nullptr, threadRun, name);
    }

    // 等待次线程运行结束
    for(int i = 0; i < NUM; i++)
    {
        int ret = pthread_join(pt[i], nullptr);
        if(ret != 0)
            cerr << "等待线程 " << pt[i] << " 失败!" << endl;
    }

    cout << "所有线程都退出了" << endl;

    return 0;
}

主线程需要等待次线程运行结束,整个程序也就正常运行了

图示

2.3、线程终止

线程可以被创建并运行,也可以被终止,线程终止方式有很多种

比如 等待线程回调函数执行结束,次线程运行五秒后就结束了,然后被主线程中的 pthread_join 等待成功,次线程使命完成

void* threadRun(void *name)
{
    // 只让次线程运行五秒
    int n = 5;
    while(n--)
    {
        cout << "我是次线程 " << (char*)name << endl;
        sleep(1);
    }
    delete[] (char*)name;

    return nullptr;
}

还有一种方法是 在次线程回调方法中调用 exit() 函数,但这会引发一个大问题:只要其中一个线程退出了,其他线程乃至整个进程都得跟着退出,显然这不是很合理,不推荐这样玩多线程

void* threadRun(void *name)
{
    while(true)
    {
        cout << "我是次线程 " << (char*)name << endl;
        sleep(1);

        // 直接终止进程,退出码设为 10
        exit(10);
    }
    delete[] (char*)name;

    return nullptr;
}

每个线程顶多存活一秒(存活在同一秒中)就被终止了,通过 echo $? 查询最近一次退出码,正是 10

图示

其实 原生线程库 中有专门终止线程运行的接口 pthread_exit,专门用来细粒度地终止线程,谁调用就终止谁,不会误伤其他线程

#include <pthread.h>

void pthread_exit(void *retval);

仅有一个参数 void*用于传递线程退出时的信息

这个参数名叫 retvalpthread_join 中的参数2也叫 retval,两者有什么不可告人的秘密吗?
答案是这俩其实本质上是同一个东西,pthread_join 中的 void **retval 是一个输出型参数,可以把一个 void * 指针的地址传递给 pthread_join 函数,当线程调用 pthread_exit 退出时,可以根据此地址对 retval 赋值,从而起到将退出信息返回给主线程的作用

图示

为什么 pthread_join 中的参数2类型为 void**

  • 因为主线程和次线程此时并不在同一个栈帧中,要想远程修改值就得传地址,类似于 int -> &int,不过这里的 retval 类型是 void*

注意: 直接在 回调方法 中 return 退出信息,主线程中的 retval 也是可以得到信息的,因为类型都是 void*,彼此相互呼应

所以比较完善的多线程操作应该是这样的:

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

using namespace std;

#define NUM 5

void* threadRun(void *name)
{
    cout << "我是次线程 " << (char*)name << endl;
    sleep(1);

    delete[] (char*)name;

    pthread_exit((void*)"EXIT");

    // 直接return "EXIT" 也是可以的
    // return (void*)"EXIT";
}

int main()
{
    pthread_t pt[NUM];

    for(int i = 0; i < NUM; i++)
    {
        // 注册新线程的信息
        char *name = new char[64];
        snprintf(name, 64, "thread-%d", i + 1);
        pthread_create(pt + i, nullptr, threadRun, name);
    }

    // 等待次线程运行结束
    void *retval = nullptr;
    for(int i = 0; i < NUM; i++)
    {
        int ret = pthread_join(pt[i], &retval);
        if(ret != 0)
            cerr << "等待线程 " << pt[i] << " 失败!" << endl;
        cout << "线程 " << pt[i] << " 等待成功,退出信息是 " << (const char*)retval << endl;
    }

    cout << "所有线程都退出了" << endl;

    return 0;
}

void* 非常之强大,可以指向任意类型的数据,甚至是一个对象

图示

既然线程复用进程的设计思想,为什么线程退出时不需要考虑是否正常退出、错误码是什么之类的?

  • 因为线程是进程的一部分,在进程中获取线程的错误信息等是无意义的,前面说过,如果一个线程因错误而被终止了,那么整个进程也就都活不了了,错误信息甄别交给父进程去完成,因此 pthread_join 就没必要关注线程退出时的具体状态了;如果次线程有信息要交给主线程,可以通过 retval 输出型参数获取

2.4、线程实战

无论是 pthread_create 还是 pthread_join,他们的参数都有一个共同点:包含了一个 void* 类型的参数,这就是意味着我们可以给线程传递对象,并借此进行某种任务处理

比如我们先创建一个包含一下信息的线程信息类,用于计算 [0, N] 的累加和

  • 线程名字(包含 ID
  • 线程编号
  • 线程创建时间
  • 待计算的值 N
  • 计算结果
  • 状态

为了方便访问成员,权限设为 public

// 线程信息类的状态
enum class Status
{
    OK = 0,
    ERROR
};

// 线程信息类
class ThreadData
{
public:
    ThreadData(const string &name, int id, int n)
        :_name(name)
        ,_id(id)
        ,_createTime(time(nullptr))
        ,_n(n)
        ,_result(0)
        ,_status(Status::OK)
    {}

public:
    string _name;
    int _id;
    time_t _createTime;
    int _n;
    int _result;
    Status _status;
};

此时就可以编写 回调方法 中的业务逻辑了

void* threadRun(void *arg)
{
    ThreadData *td = static_cast<ThreadData*>(arg);

    // 业务处理
    for(int i = 0; i <= td->_n; i++)
        td->_result += i;
    
    // 如果业务处理过程中发现异常行为,可以设置 _status 为 ERROR
    
    cout << "线程 " << td->_name << " ID " << td->_id << " CreateTime " << td->_createTime << " done..." << endl;

    pthread_exit((void*)td);

    // 也可以直接 return 
    // return td;
}

主线程在创建线程及等待线程时,就可以使用 ThreadData 对象了,后续涉及业务修改时,也只需要修改类及回调方法即可,无需再更改创建及等待逻辑,有效做到了 解耦

int main()
{
    pthread_t pt[NUM];

    for(int i = 0; i < NUM; i++)
    {
        // 注册新线程的信息
        char name[64];
        snprintf(name, sizeof(name), "thread-%d", i + 1);

        // 创建对象
        ThreadData *td = new ThreadData(name, i, 100 * (10 + i));
        pthread_create(pt + i, nullptr, threadRun, td);
        sleep(1); // 尽量拉开创建时间
    }

    // 等待次线程运行结束
    void *retval = nullptr;
    for(int i = 0; i < NUM; i++)
    {
        int ret = pthread_join(pt[i], &retval);
        if(ret != 0)
            cerr << "等待线程 " << pt[i] << " 失败!" << endl;

        ThreadData *td = static_cast<ThreadData*>(retval);

        if(td->_status == Status::OK)
            cout << "线程 " << pt[i] << " 计算 [0, " << td->_n << "] 的累加和结果为 " << td->_result << endl;
        delete td;
    }

    cout << "所有线程都退出了" << endl;

    return 0;
}

程序可以正常运行,各个线程也都能正常计算出结果;这里只是简单计算累加和,线程还可以用于其他场景:网络传输、密集型计算、多路 IO等,无非就是修改线程的业务逻辑

图示

结论:多线程可以传递对象指针,自由进行任务处理

2.5、其他接口

与多线程相关的还有一批其他接口,比较简单,就放在一起介绍了

2.5.1、关闭线程

线程可以被创建,自然也可以被关闭,可以使用 pthread_cancel 关闭已经创建并运行中的线程

#include <pthread.h>

int pthread_cancel(pthread_t thread);

参数1 pthread_t被关闭的线程 ID

返回值:成功返回 0,失败返回一个非零的 error number

这里可以直接模拟关闭线程的场景

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

using namespace std;

void *threadRun(void *arg)
{
    const char *ps = static_cast<const char*>(arg);

    while(true)
    {
        cout << "线程 " << ps << " 正在运行" << endl;
        sleep(1);
    }

    pthread_exit((void*)10);
}

int main()
{
    pthread_t t;
    pthread_create(&t, nullptr, threadRun, (void*)"Hello Thread");

    // 3秒后关闭线程
    sleep(3);

    pthread_cancel(t);

    void *retval = nullptr;
    pthread_join(t, &retval);

    // 细节:使用 int64_t 而非 uint64_t
    cout << "线程 " << t << " 已退出,退出信息为 " << (int64_t)retval << endl;
    return 0;
}

程序运行 3 秒后,可以看到退出信息为 -1,与我们预设的 10 不相符

图示

原因很简单:只要是被 pthread_cancel 关闭的线程,退出信息统一为 PTHREAD_CANCELED-1

这也就解释了为什么要强转为 ingt64_t,因为无符号的 -1 非常大,不太好看

图示

比较奇怪的实验

  • 次线程可以自己关闭自己吗?答案是可以的,但貌似关闭后,主线程没有正常等待,整个进程一四正常结束(退出码为 0
  • 次线程可以关闭主线程吗?答案是不可以,类似于 kill -9 无法终止 1 号进程

2.5.2、获取线程ID

线程 ID 是线程的唯一标识符,可以通过 pthread_self 获取当前线程的 ID

#include <pthread.h>

pthread_t pthread_self(void);

返回值:当前线程的 ID

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

using namespace std;

void *threadRun(void *arg)
{
    cout << "当前次线程的ID为 " << pthread_self() << endl;
    return nullptr;
}

int main()
{
    pthread_t t;
    pthread_create(&t, nullptr, threadRun, nullptr);

    pthread_join(t, nullptr);
    cout << "创建的次线程ID为 " << t << endl;

    return 0;
}

可以看到结果都是一样的

图示

2.5.3、线程分离

父进程需要阻塞式等待子进程退出,主线程等该次线程时也是阻塞式等待,父进程可以设置为 WNOHANG,变成轮询式等待,避免自己一直处于阻塞;次线程该如何做才能避免等待时阻塞呢?

答案是 分离 Detach

线程在被创建时,默认属性都是 joinable 的,即主线程需要使用 pthread_join 来等待次线程退出,并对其进行资源释放;实际上我们可以把这一操作留给系统自动处理,如此一来主线程就可以不必等待次线程,也就可以避免等待时阻塞了,这一操作叫做 线程分离

原生线程库 提供的线程分离接口是 pthread_detach

#include <pthread.h>

int pthread_detach(pthread_t thread);

参数1 pthread_t待分离的线程 ID

返回值:成功返回 0,失败返回 error number

线程分离的本质是将 joinable 属性修改为 detach,告诉系统线程退出后资源自动释放

注意: 如果线程失去了 joinable 属性,就无法被 join,如果 join 就会报错

接下来简单使用一下 线程分离

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

using namespace std;

void *threadRun(void *arg)
{
    int n = 3;
    while(n)
    {
        cout << "次线程 " << n-- << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t t;
    pthread_create(&t, nullptr, threadRun, nullptr);

    pthread_detach(t);

    int n = 5;
    while(n)
    {
        cout << "主线程 " << n-- << endl;
        sleep(1);
    }


    return 0;
}

主线程可以不用等待次线程,两个执行流并发运行,并且不必担心次线程出现僵尸问题

图示

建议将 pthread_detach 放在待分离线程的 线程创建 语句之后,如果放在线程执行函数中,可能会因为调度优先级问题引发错误(未知结果)

  • 线程被创建后,谁先执行不确定

总之,线程被分离后,主线程就可以不必关心了,即不需要 join 等待,是否分离线程取决于具体的应用场景


3、深入理解线程

3.1、理解线程库及线程 ID

在见识过 原生线程库 提供的一批便利接口后,不由得感叹库的强大,如此强大的库究竟是如何工作的呢?

图示

原生线程库本质上也是一个文件,是一个存储在 /lib64 目录下的动态库,要想使用这个库,就得在编译时带上 -lpthread 指明使用动态库

程序运行时,原生线程库 需要从 磁盘 加载至 内存 中,再通过 进程地址空间 映射至 共享区 中供线程使用

图示

由于用户并不会直接使用 轻量级进程 的接口,于是 需要借助第三方库进行封装,类似于用户可能不了解系统提供的 文件接口,从而使用 C语言 封装的 FILE 库一样

图示

对于 原生线程库 来说,线程不止一个,因此遵循 先描述,再组织 原则,在线程库中创建 TCB 结构(类似于 PCB),其中存储 线程 的各种信息,比如 线程独立栈 信息

图示
在内存中,整个 线程库 就像一个 “数组”,其中的一块块空间聚合排布 TCB 信息,而 每个 TCB 的起始地址就表示当前线程的 ID,地址是唯一的,因此线程 ID 也是唯一的

因此,我们之前打印 pthread_t 类型的 线程 ID 时,实际打印的是地址,不过是以 十进制 显示的,可以通过函数将地址转化为使用 十六进制 显示

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

using namespace std;

string toHex(pthread_t t)
{
    char id[64];
    snprintf(id, sizeof(id), "0x%x", t);
    return id;
}

void *threadRun(void *arg)
{
    cout << "我是[次线程],我的ID是 " << toHex(pthread_self()) << endl;

    return (void*)0;
}

int main()
{
    pthread_t t;
    pthread_create(&t, nullptr, threadRun, nullptr);

    pthread_join(t, nullptr);

    cout << "我是[主线程],我的ID是 " << toHex(pthread_self()) << endl;


    return 0;
}

线程 ID 确实能转化为地址(虚拟进程地址空间上的地址)

图示

注意: 即便是 C++11 提供的 thread 线程库,在 Linux 平台中运行时,也需要带上 -lpthread 选项,因为它本质上是对 原生线程库 的封装

3.2、理解线程独立栈

线程 之间存在 独立栈,可以保证彼此之前执行任务时不会相互干扰,可以通过代码证明

多个线程使用同一个入口函数,并打印其中临时变量的地址

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

using namespace std;

string toHex(pthread_t t)
{
    char id[64];
    snprintf(id, sizeof(id), "0x%x", t);
    return id;
}

void *threadRun(void *arg)
{
    int tmp = 0;
    cout << "thread " << toHex(pthread_self()) << " &tmp: " << &tmp << endl;

    return (void*)0;
}

int main()
{
    pthread_t t[5];
    for(int i = 0; i < 5; i++)
    {
        pthread_create(t + i, nullptr, threadRun, nullptr);
        sleep(1);
    }

    for(int i = 0; i < 5; i++)
        pthread_join(t[i], nullptr);
    return 0;
}

可以看到五个线程打印 “同一个” 临时变量的地址并不相同,足以证明 线程独立栈 的存在

图示

存在这么多 栈结构CPU 在运行时是如何区分的呢?

答案是 通过 栈顶指针 ebp 和 栈底指针 esp 进行切换ebpespCPU 中两个非常重要的 寄存器,即便是程序启动,也需要借助这两个 寄存器main 函数开辟对应的 栈区

图示

除了移动 esp 扩大栈区外,还可以同时移动 ebpesp 更改当前所处栈区

图示

所以,多线程中 独立栈 可以通过 ebpesp 轻松切换并使用

如果想要在栈区中开辟整型空间,可以使用 ebp - 4 定位对应的空间区域并使用,其他类型也是如此,原理都是 基地址 + 偏移量

注意:

  1. 所有线程都要有自己独立的栈结构(独立栈),主线程中用的是进程系统栈,次线程用的是库中提供的栈
  2. 多个线程调用同一个入口函数(回调方法),其中的局部变量地址一定不一样,因为存储在线程独立栈中

3.3、理解线程局部存储

线程 之间共享 全局变量,对 全局变量 进行操作时,会影响其他线程

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

using namespace std;

int g_val = 100;

string toHex(pthread_t t)
{
    char id[64];
    snprintf(id, sizeof(id), "0x%x", t);
    return id;
}

void *threadRun(void *arg)
{
    cout << "thread: " << toHex(pthread_self()) << " g_val: " << ++g_val << " &g_val: " << &g_val << endl;
    return (void*)0;
}

int main()
{
    pthread_t t[3];
    for(int i = 0; i < 3; i++)
    {
        pthread_create(t + i, nullptr, threadRun, nullptr);
        sleep(1);
    }

    for(int i = 0; i < 3; i++)
        pthread_join(t[i], nullptr);
    return 0;
}

在三个线程的影响下,g_val 最终变成了 103

图示

如何让全局变量私有化呢?即每个线程看到的全局变量不同

可以给全局变量加 __thread 修饰,修饰之后,全局变量不再存储至全局数据区,而且存储至线程的 局部存储区中

__thread int g_val = 100;

结果:修饰之后,每个线程确实看到了不同的 “全局变量”

特点:此时的 “全局变量” 的地址变大了

图示

“全局变量” 地址变大是因为此时它不再存储在 全局数据区 中,而且存储在线程的 局部存储区 中,线程的局部存储区位于 共享区,并且 共享区 的地址天然大于 全局数据区

图示

注意: 局部存储区位于共享区中,可以通过 __thread 修饰来改变变量的存储位置


🌆总结

以上就是本次关于 Linux多线程【线程控制】的全部内容了,在本文中我们首先补充了线程理解的相关知识,明白线程的私有与共享资源;然后学习了一批原生线程库中的接口,包括创建、等待、终止等;最后深入学习了线程库及线程资源的知识。有了线程控制的相关知识后,就可以开始着手编写多线程代码了,在写代码的过程中,必然会遇到 [并发访问] 问题,解决方法在于 [线程互斥与同步]


星辰大海

相关文章推荐

Linux多线程 =====:>
【初始多线程】

Linux进程信号 ===== :>
【信号产生】、【信号保存】、【信号处理】

Linux进程间通信 ===== :>

【消息队列、信号量】、【共享内存】、【命名管道】、【匿名管道】

Linux基础IO ===== :>

【软硬链接与动静态库】、【深入理解文件系统】、【模拟实现C语言文件流】、【重定向及缓冲区理解】、【文件理解与操作】

Linux进程控制 ===== :>

【简易版bash】、【进程程序替换】、【创建、终止、等待】

Linux进程学习 ===== :>

【进程地址】、【环境变量】、【进程状态】、【基本认知】

Linux基础 ===== :>

【gdb】、【git】、【gcc/g++】、【vim】、Linux 权限理解和学习、听说Linux基础指令很多?这里都帮你总结好了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1018101.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FreeSWITCH 1.10.10 简单图形化界面8 - 讯时FXO网关SIP注册公网IPPBX落地

FreeSWITCH 1.10.10 简单图形化界面8 - 讯时FXO网关SIP注册公网IPPBX落地 0、 界面预览1、创建一个话务台2、创建PBX 分机中继并设置呼入权限3、设置呼出规则4、设置分机呼出权限5、设置FXO 网关相关信息6、设置FXO网关中继线路呼入号码7、设置FXO网关呼叫路由&#xff08;呼入…

为啥我的第二个for循环不加框红的代码就运行失效呢?(文末赠书)

点击上方“Python爬虫与数据挖掘”&#xff0c;进行关注 回复“书籍”即可获赠Python从入门到进阶共10本电子书 今 日 鸡 汤 苟全性命于乱世&#xff0c;不求闻达于诸侯。 大家好&#xff0c;我是皮皮。 一、前言 前几天在Python最强王者群【哎呦喂 是豆子&#xff5e;】问了一…

【机组】计算机系统组成课程笔记 第二章 计算机中的信息表示

2.1 无符号数和有符号数 2.1.1 无符号数 没有符号的数&#xff0c;其实就是非负数。在计算机中用字节码表示&#xff0c;目前最常用的是八位和十六位的。 2.1.2 有符号数 将正负符号数字化&#xff0c;0代表 &#xff0c;1代表 - &#xff0c;并把代表符号的数字放在有效数…

【Linux升级之路】6_进程间通信

&#x1f31f;hello&#xff0c;各位读者大大们你们好呀&#x1f31f; &#x1f36d;&#x1f36d;系列专栏&#xff1a;【Linux升级之路】 ✒️✒️本篇内容&#xff1a;进程间通信介绍&#xff0c;管道&#xff0c;共享内存&#xff0c;消息队列&#xff0c;信号量 &#x1f…

八股文学习一(存储)

一. 存储 行式存储的原理与特点 对于 OLAP 场景&#xff0c;大多都是对一整行记录进行增删改查操作的&#xff0c;那么行式存储采用以行的行式在磁盘上存储数据就是一个不错的选择。 当查询基于需求字段查询和返回结果时&#xff0c;由于这些字段都埋藏在各行数据中&#xf…

直播设备之ENC1高级篇拆机刷uboot教程

直播设备之ENC1高级篇拆机刷uboot教程 第一步&#xff1a; 准备材料第二步&#xff1a;拆外壳取出裸板第三步&#xff1a;链接串口线第四步&#xff1a;进入电脑&#xff0c;开始刷uboot第五步&#xff1a;开始刷设备固件 老铁们好&#xff0c;好久没出文章了&#xff0c;这两天…

【面试题】智力题

文章目录 腾讯1000瓶毒药里面只有1瓶是有毒的&#xff0c;问需要多少只老鼠才能在24小时后试出那瓶有毒。有两根不规则的绳子&#xff0c;两根绳子从头烧到尾均需要一个小时&#xff0c;现在有一个45分钟的比赛&#xff0c;裁判员忘记带计时器&#xff0c;你能否通过烧绳子的方…

leetcode21合并两个有序链表

题目&#xff1a; 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例 1&#xff1a; 输入&#xff1a;l1 [1,2,4], l2 [1,3,4] 输出&#xff1a;[1,1,2,3,4,4]示例 2&#xff1a; 输入&#xff1a;l1 [], l2 [] 输…

Android之MediaMetricsService实现本质(四十二)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 人生格言: 人生从来没有捷径,只有行动才是治疗恐惧和懒惰的唯一良药. 更多原创,欢迎关注:Android…

AE-如何让一副静止的画变成动态图

如何让一副静止的画变成动态图,如图所示,如何让图中静态的芦苇随风摆动? 制作过程如下: 1.安装AutoSway AutoSway 可以从lookae 下载。 AutoSway 安装方法: 1)复制 AutoSway.jsxbin和AutoSway_ffx文件夹到AE脚本目录: Win:...Adobe After Effects CC\Support Fil…

【SDXL_LORA模型训练详细教程(含云端教程)】

个人网站&#xff1a;https://tianfeng.space 一、前言 之前写过一篇SD1.5 LORA模型的炼制方法&#xff0c;有的人想要我详细点说说秋叶启动器的lora训练器&#xff0c;SDXL建议使用秋叶的训练器&#xff0c;SD1.5赛博丹炉&#xff0c;个人习惯仅供参考&#xff01;这次基于s…

TCP详解之滑动窗口

TCP详解之滑动窗口 引入窗口概念的原因 我们都知道 TCP 是每发送一个数据&#xff0c;都要进行一次确认应答。当上一个数据包收到了应答了&#xff0c; 再发送下一个。 这个模式就有点像我和你面对面聊天&#xff0c;你一句我一句。但这种方式的缺点是效率比较低的。 如果你…

红黑树的原理

文章目录 红黑树的原理红黑树的定义为什么会有红黑树红黑树构建 红黑树的原理 红黑树&#xff08;Red-Black Tree&#xff09;是一种自平衡的二叉搜索树&#xff0c;它在计算机科学中被广泛应用于实现有序集合和映射等数据结构。它通过引入颜色标记和一些特定的操作规则&#…

springboot+vue“智慧食堂”设计与实现springboot002

大家好✌&#xff01;我是CZ淡陌。一名专注以理论为基础实战为主的技术博主&#xff0c;将再这里为大家分享优质的实战项目&#xff0c;本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路…

JAVA医药进销存管理系统(附源码+调试)

JAVA医药进销存管理系统 功能描述 &#xff08;1&#xff09;登录模块&#xff1a;登录信息等存储在数据库中 &#xff08;2&#xff09;基本信息模块&#xff1a;分为药品信息模块、客户情况模块、供应商情况模块&#xff1b; &#xff08;3&#xff09;业务管理模块&#x…

QStandardItem通过setCheckable添加复选框后无法再次通过setCheckable取消复选框的问题

前言 如题所示&#xff0c;通过setCheckable添加复选框后&#xff0c;想要通过setCheckable(false)取消复选框&#xff0c;你会发现根本没有作用的。 问题复现 #include "widget.h" #include "ui_widget.h" #include <QList>Widget::Widget(QWidg…

QT基础教学(QMainWindow)

文章目录 前言一、QMainWindow介绍二、代码示例三、QMainWindow高级用法总结 前言 之前我们都是在QWidget中来进行学习的&#xff0c;那么今天我将为大家讲解一下QMainWindow。 一、QMainWindow介绍 QMainWindow是Qt框架提供的一个重要的窗口类&#xff0c;用于创建具有标准…

结构体的简单介绍(4)——位段

目录 位段的概念&#xff1a; 位段的内存分配&#xff1a; 问题1&#xff1a;当开辟了内存后&#xff0c;内存中每个比特位从右向左使用?还是从左向右使用? 这个不确定。 问题2&#xff1a;当前面时候&#xff0c;剩余的空间不足下一个成员使用的时候&#xff0c;剩余的空…

1.使用turtle换一个五环2.设计这样一个程序:输入一个数字 判断它是不是一个质数

1.使用turtle换一个五环 import turtle turtle.pensize(15) turtle.penup() turtle.color(blue) turtle.goto(-150,-35) turtle.pendown() turtle.circle(60) turtle.penup() turtle.color(black) turtle.goto(0,-35) turtle.pendown() turtle.circle(60) turtle.penup() turtl…

ffplay源码解析-PacketQueue队列

包队列架构位置 对应结构体源码 MyAVPacketList typedef struct MyAVPacketList {AVPacket pkt; //解封装后的数据struct MyAVPacketList *next; //下一个节点int serial; //播放序列 } MyAVPacketList;PacketQueue typedef struct PacketQueue {MyAVPacketList …