【Linux】详解线程控制 -- 线程用法 | 线程等待 | 线程ID及地址空间布局

news2024/10/7 6:45:59

  • OS提供的轻量级进程接口
    • POSIX线程库
  • 线程使用
    • 1.如何创建一堆线程
    • 2.线程如何终止
    • 3.线程如何取消
  • 线程等待
  • 线程退出返回值
  • C++11的多线程
  • 线程ID及地址空间布局
    • 线程地址空间布局
    • 线程局部存储
  • 分离线程

OS提供的轻量级进程接口

在这里插入图片描述
(关于 用户 → 库 → OS :具体可看下面线程地址空间布局)

在这里插入图片描述

这个clone我们不用,这是OS提供给第三方库所用的接口

POSIX线程库

与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”开头的,要使用这些函数库,要通过引入头文<pthread.h>,链接这些线程函数库时要使用编译器命令的“-lpthread”选项

创建线程:
在这里插入图片描述
函数原型:

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:传给线程启动函数的参数

返回值:成功返回0;失败返回错误码

错误检查:

  • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
  • pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
  • pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,
    建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小

线程使用

1.如何创建一堆线程

试验:创建一批线程:

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

using namespace std;
void *start_routine(void *args)
{
    string name = static_cast<const char *>(args);
    while (true)
    {
        cout << "new thread create success , name: " << name << endl;
        sleep(1);
    }
}
int main()
{
    vector<pthread_t> tids;
#define NUM 10
    for (int i = 0; i < NUM; i++)
    {
        pthread_t tid;
        char namebuffer[64];
        snprintf(namebuffer, sizeof(namebuffer), "%s:%d", "thread", i);
        pthread_create(&tid, nullptr, start_routine, namebuffer);
        // sleep(1);
    }
    while (true)
    {
        cout << "new thread create success , name: main thread" << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
在这里插入图片描述

为什么这里连续输出9呢?
在这里插入图片描述

我们这样的写法是有问题的,我们可以对其进行更改:

class ThreadData
{
public:
    int number;
    pthread_t tid;
    char namebuffer[64];
};
void *start_routine(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args); // 安全的进行强制类型转化
    int cnt = 10;
    while (cnt)
    {
        cout << "new thread create success, name: " << td->namebuffer << " cnt: " << cnt-- << endl;
    }
    delete td;
    return nullptr;
}
int main()
{
    vector<ThreadData *> threads;
#define NUM 10
    for (int i = 0; i < NUM; i++)
    {
        ThreadData *td = new ThreadData();
        td->number = i + 1;
        snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i + 1);
        pthread_create(&td->tid, nullptr, start_routine, td);
        threads.push_back(td);
    }
    for (auto &iter : threads)
    {
        cout << "create thread: " << iter->namebuffer << " : " << iter->tid << " suceesss" << endl;
    }
    while (true)
    {
        cout << "new thread create success , name: main thread" << endl;
        sleep(1);
    }
    return 0;
}

将其创建成结构体,每次创建都new一个ThreadData对象,然后将结构体对象的地址传递给start_routine,每一个结构体对象都有自己独立的缓冲区,这样就能避免缓冲区被刷新了的问题。
在这里插入图片描述

思考:start_routine这个函数被10个线程同时运行,它是什么状态?函数里面定义的tdcnt在变化会影响别的线程吗?
start_routine这个函数现在是重入状态,该函数是可重入函数,虽然cnt在变化,但是在函数内定义的变量,都叫做局部变量,具有临时性,现在依旧适用在多线程情况下,也没有问题。其实每一个线程都有自己独立的栈结构!(打印输出现在不解释,这个输出是往文件输出,当然只能执行一个)

在这里插入图片描述

2.线程如何终止

  1. return nullptr;线程函数结束,return的时候,线程就算终止了
  2. pthread_exit(nullptr);我们在start_routine函数while循环中终止:
void *start_routine(void *args)
{
    sleep(1);
    ThreadData *td = static_cast<ThreadData *>(args); // 安全的进行强制类型转化
    int cnt = 10;
    while (cnt)
    {
        cout << "cnt : " << cnt << " &cnt: " << &cnt << endl;
        cnt--;
        pthread_exit(nullptr);//终止线程
        sleep(1);
    }
    delete td;
    return nullptr;
}

在这里插入图片描述
循环打印线程脚本:

while :; do ps -aL | head -1 && ps -aL | grep mythread;sleep 1;done

我们不能之间在线程中使用exit(),因为这是直接终止进程的,如果有线程调用这个函数,那么整个线程都会终止,pthread_exit(nullptr);可以终止新线程而不影响主线程。

3.线程如何取消

线程是可以被取消的但是注意:线程要被取消,前提是这个线程已经跑起来了
在这里插入图片描述
函数原型:

int pthread_cancel(pthread_t thread);

实操代码:

//新线程部分代码
void* start_routine(void *args)
{
	//....
    return (void*)123;
}
//主线程部分代码
for(int i = 0; i < threads.size()/2; i++)
{
    pthread_cancel(threads[i]->tid);
    cout << "pthread_cancel : " << threads[i]->namebuffer << " success" << endl;
}
for (auto &iter : threads)
{
    void* tmp=nullptr;
    int n = pthread_join(iter->tid, &tmp);
    assert(n == 0);
    cout << "join thread : " << iter->namebuffer <<"exit : "<< (long long)tmp << endl;
    delete iter;
}

在这里插入图片描述
线程如果是被取消的,退出码:-1(PTHREAD_CANCELED)


线程等待

线程也是要被等待的,如果不等待,会造成类似僵尸进程的问题–内存泄漏

线程必须也要被等待:

  1. 获取新线程的退出信息→可以不关心退出信息吗?可以
  2. 回收新线程对应的PCB等内核资源,防止内存泄漏 – 暂时无法查看!

线程等待函数pthread_join:
在这里插入图片描述
函数原型:

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

在主线程中等待并释放:

for (auto &iter : threads)
{
    int n = pthread_join(iter->tid,nullptr);
    assert(n ==0);
    cout << "join thread : " << iter->namebuffer << " success" << endl;
    delete iter;
}

在这里插入图片描述


线程退出返回值

我们的start_routine函数返回值是void*类型,这个类型有什么说法吗?
在这里插入图片描述
在这里插入图片描述

//新线程部分代码
void *start_routine(void *args)
{
    //....
    return (void*)123;
}
//主线程部分代码:
for (auto &iter : threads)
{
    void* tmp=nullptr;
    int n = pthread_join(iter->tid, &tmp);
    assert(n == 0);
    cout << "join thread : " << iter->namebuffer <<"exit : " << (long long)tmp<< " success" << endl;//(long long)类型是因为我使用的Linux版本是64位的
    delete iter;
}
//其余代码跟之前一样,这里只是增加的或更改了一点

在这里插入图片描述

这个的返回值也跟上述return是一样的

pthread_exit((void*)123);

既然假的地址,整数都能被外部拿到,那么如何返回的是,堆空间的地址呢?对象的地址呢?
返回一个对象指针:

class ThreadReturn
{
public:
    int exit_code;
    int exit_result;
};
//新线程部分代码
void* start_routine(void *args)
{
	//.....
    ThreadReturn * tr = new ThreadReturn();
    tr->exit_code = 1;
    tr->exit_result = 123;
    return (void*)tr;
}
//主线程部分代码
for (auto &iter : threads)
{
    ThreadReturn* tmp=nullptr;
    int n = pthread_join(iter->tid, (void**)&tmp);
    assert(n == 0);
    cout << "join thread : " << iter->namebuffer <<"exit_code : " << tmp->exit_code<< " exit_result : "<<tmp->exit_result << endl;
    delete iter;
}

在这里插入图片描述

为什么没有见到,线程退出的时候,对应的退出信号???
线程出异常,收到信号,整个进程都会退出!pthread_join:默认就认为函数会调用成功!不考虑异常问题,异常问题是进程该考虑的问题!


C++11的多线程

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

void thread_run()
{
    while (true)
    {
        std::cout << "我是新线程..." << std::endl;
        sleep(1);
    }
}

int main()
{
    std::thread t1(thread_run);

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

    t1.join();

    return 0;
}

在这里插入图片描述
任何语言,在linux中如果要实现多线程,必定要是用pthread库

如何看待C++11中的多线程呢?
C++11 的多线程,在Linux环境中,本质是对pthread库的封装!(使用原生线程库还是C++11的线程库都可以,只是C++11的线程库在windows下也能运行,Linux与windows底层的线程库的接口不一样)


线程ID及地址空间布局

  • pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
  • 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
  • pthread_create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
  • 线程库NPTL提供了pthread_self函数,可以获得线程自身的ID

在这里插入图片描述

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

std::string changeId(const pthread_t &thread_id)
{
    char tid[128];
    snprintf(tid, sizeof(tid), "0x%x", thread_id);
    return tid;
}

void *start_routine(void *args)
{
    std::string threadname = static_cast<const char *>(args);
    while (true)
    {
        std::cout << threadname << " running ... : " << changeId(pthread_self()) << std::endl;
        sleep(1);
    }

    return nullptr;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, start_routine, (void *)"thread 1");
    std::string main_id = changeId(pthread_self());
    std::cout << "main thread running ... new thread id: " << changeId(tid) <<" main thread id: " << main_id << std::endl;
    pthread_join(tid,nullptr);
    return 0;
}

在这里插入图片描述

线程地址空间布局

pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。

在这里插入图片描述
原生线程库可能存在多个线程,我们同样也要对线程进行管理(先描述再组织),每一个轻量级进程对应原生库中的一个结构体对象(TCB),这是Linux的方案 – 用户级线程,用户关心的线程属性在库中,而内核里面提供执行流的调度。Linux用户级线程对比内核轻量级进程是1:1。

线程布局图:
在这里插入图片描述

用户级线程:线程ID值就是库中结构体(TCB)对象的地址

线程局部存储

我们在之前的“什么是线程?”一文已经了解到了:线程一旦被创建,几乎所有资源都是被线程所共享的

//定义一个全局变量
int g_val= 100;
//新线程:
std::cout << threadname << " running ... : " << changeId(pthread_self()) 
		  <<" g_val: "<< g_val++ << " &g_val: " << &g_val << std::endl;
//主线程:
std::cout << "main thread running ... new thread id: " <<changeId(tid) 
          <<" main thread id: " << main_id << " g_val: "<< g_val << " &g_val: " << &g_val << std::endl;

我们在主线程与新线程都打印输出g_val的值与地址:
在这里插入图片描述
我们可以发现,主线程与新线程都是输出同一个g_val值与地址,且这个地址非常小

添加__thread,可以将一个内置类型设置为线程局部存储

__thread int g_val = 100;

在这里插入图片描述
我们可以发现,给g_val添加__thread后,主线程与新线程打印输出的地址不一样了,新线程对g_val的值进行修改,主线程打印的值没有变化。

原因图:
在这里插入图片描述
一开始g_val已初始化数据段,添加__thread以后,使其设置为线程局部存储,每个线程都有一份且是在共享区已初始化数据段→共享区(地址由低到高),且它们访问不会互相影响。


分离线程

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
  • 如果线程设置了分离状态,那么就不能join等待了。

分离一个线程:
在这里插入图片描述
函数原型:

int pthread_detach(pthread_t thread);

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离

验证分离以后不能join(里面有小bug,请思考):

#include <iostream>
#include <string>
#include <pthread.h>
#include <cstring>
#include <unistd.h>
std::string changeId(const pthread_t &thread_id)
{
    char tid[128];
    snprintf(tid, sizeof(tid), "0x%x", thread_id);
    return tid;
}
void *start_routine(void *args)
{
    std::string threadname = static_cast<const char *>(args);
    // pthread_detach(pthread_self());
    int cnt = 5;
    while (cnt--)
    {
        std::cout << threadname << " running ... : " << changeId(pthread_self()) << std::endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, start_routine, (void *)"thread 1");
    std::string main_id = changeId(pthread_self());
    std::cout << "main thread running ... new thread id: " << changeId(tid) <<" main thread id: " << main_id << std::endl;
    int n = pthread_join(tid,nullptr);
    std::cout << "result : " << n << " : " << strerror(n) <<std::endl;
    return 0;
}

1.我们在start_routine里面没有使用线程分离:
在这里插入图片描述
2.我们在start_routine里面使用线程分离pthread_detach
在这里插入图片描述
思考:为什么我在start_routine里面使用线程分离pthread_detach然后pthread_join还是成功了?
在这里插入图片描述
原因:由于主线程创建新线程以后,到底是主线程先运行还是新线程先运行是随机的,如果主线程先运行了pthread_join使得其已经阻塞式等待了,然后新线程才pthread_detach分离,这个时候已经晚了。
在这里插入图片描述

当然这种分离方式我们不赞成,我们一般这样使用线程分离:

std::string changeId(const pthread_t &thread_id)
{
    char tid[128];
    snprintf(tid, sizeof(tid), "0x%x", thread_id);
    return tid;
}
void *start_routine(void *args)
{
    std::string threadname = static_cast<const char *>(args);
    int cnt = 5;
    while (cnt--)
    {
        std::cout << threadname << " running ... : " << changeId(pthread_self()) << std::endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, start_routine, (void *)"thread 1");
    std::string main_id = changeId(pthread_self());
    pthread_detach(tid);//线程分离

    std::cout << "main thread running ... new thread id: " << changeId(tid) << " main thread id: " << main_id << std::endl;

    // int n = pthread_join(tid,nullptr);
    // std::cout << "result : " << n << " : " << strerror(n) <<std::endl;
    while (true)
    {
        std::cout << "main thread running ... " << std::endl;
    }
    return 0;
}

在这里插入图片描述
在这里插入图片描述


如有错误或者不清楚的地方欢迎私信或者评论指出🚀🚀

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

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

相关文章

好用亦免费的系统镜像备份软件!

​Windows系统现状 Windows系统备份的重要性日益凸显。随着Windows 7退出历史舞台,Windows 11/10的使用率快速上升。新电脑不再支持Windows 7,许多用户只能转向更新系统。加之Windows 11功能趋于成熟稳定,越来越多人开始适应并接受它。 因此,选择一个适合Windo…

马上做还是等一下?

马上做还是等一下&#xff1f;这是一个产品人经常会遇到的问题。今天想跟大家分享的是面临这两个选择的两个场景和一些感悟。 图1 - 我们总会遇到各种各样的问题&#xff0c;难以抉择 场景一&#xff1a;当你接到一项工作时&#xff0c;会选择高效执行马上开始&#xff0c;还是…

前端图标解决方案

1. 前言 随着 Web 技术的发展与日益丰富的界面需求&#xff0c;图标逐渐成为前端开发中不可或缺的一部分&#xff0c;为此也诞生了各种各样的解决方案。文章总结及分析了目前常见的一些图标解决方案。 2. CSS 背景图片 2.1 background-image 图标本质上也是图片&#xff0c…

子网划分和计网解题方法

子网的基本概念 子网是计算机网络中的一个逻辑单元&#xff0c;是由多个IP地址组成的网络。在计算机网络中&#xff0c;IP地址是一个32位的二进制数&#xff0c;用于标识网络上的设备。子网划分是将一个大型的IP地址网络划分为多个小的IP地址网络&#xff0c;每个小的IP地址网…

软件外包开发的项目管理工具

在开发大型项目时涉及到多人管理&#xff0c;细节比较多&#xff0c;需要借助科学的项目管理方法和软件工具来提高软件项目效率。现在有比较多的项目管理方法和配套工具&#xff0c;每个项目和团队的情况不同&#xff0c;选择适合自己的是最重要的。今天和大家分享软件项目管理…

小程序新渲染引擎 Skyline 发布正式版

为了进一步提升小程序的渲染性能和体验&#xff0c;我们推出了一套新渲染引擎 Skyline&#xff0c;现在&#xff0c;跟随着基础库 3.0.0 发布 Skyline 正式版。 我们知道&#xff0c;小程序一直用 WebView 来渲染界面&#xff0c;因其有不错的兼容性和丰富的特性&#xff0c;且…

【通世智库】陈敏华:永存我心的爱——忆我的丈夫陶一凡

​ 2022年12月25日&#xff0c;我敬爱的丈夫&#xff0c;平静安然的告别了眷恋着他的亲友们&#xff0c;走了。 72年前&#xff0c;在上海致远中学上学不满16岁的陶一凡&#xff0c;毅然弃笔从戎&#xff0c;随志愿军跨过鸭绿江&#xff0c;奔赴朝鲜战场。他说过&#xff0c;12…

企业通过CRM分析销售数据有什么用处?

企业为什么要分析CRM销售数据&#xff1f;分析CRM销售数据的目的&#xff0c;是为企业提供对其销售业绩、客户行为和市场趋势的宝贵见解。通过分析这些数据&#xff0c;企业可以确定他们表现良好的领域和需要改进的领域。 1、销售业绩 通过分析CRM销售数据&#xff0c;企业可…

云计算——ACA学习 数据中心概述

作者简介&#xff1a;一名云计算网络运维人员、每天分享网络与运维的技术与干货。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​ 目录 写在前面 课程目标 学前了解 一.数据中心定义 二.数据中心涉及的主要标准与规范 …

adas知识

车辆坐标系 右手坐标系&#xff0c;红色箭头方向角度为正。 传感器坐标系&#xff08;如相机&#xff09;

关于项目,会问我什么?

作者&#xff1a;阿秀 校招八股文学习网站&#xff1a;https://interviewguide.cn 这是阿秀的第「288」篇原创 小伙伴们大家好&#xff0c;我是阿秀。 在校招求职这块&#xff0c;简历上比较重要的点就是教育背景、实习经历、项目经验三块&#xff0c;其中教育背景都到了秋招这…

TencentOS Server镜像操作系统介绍_常见问题解答FAQ

腾讯云TencentOS Server镜像是腾讯云推出的Linux操作系统&#xff0c;完全兼容CentOS生态和操作方式&#xff0c;TencentOS Server操作系统为云上运行的应用程序提供稳定、安全和高性能的执行环境&#xff0c;TencentOS可以运行在腾讯云CVM全规格实例上&#xff0c;包括黑石物理…

naive-ui的dialog.warning 关闭和阻止关闭

序&#xff1a; 1、如果你卡到 了&#xff0c;博主没写博客&#xff0c;可以在博主的公众号&#xff1a;“程序员野区” 留言。博主看到有时间再帮你去试 2、博主主要讲的怎么 主动关闭dialog和阻止dialog 自动关闭。 注意&#xff01;&#xff01;&#xff01;&#xff01;来&…

蛋白组学富集分析 uniport id蛋白ID如何进行KEGG和GO富集分析 代谢组学

使用蛋白ID如何进行KEGG和GO富集分析 - 知乎 (zhihu.com) 昨天&#xff0c;有个童鞋咨询如何使用蛋白ID进行功能富集分析&#xff0c;功能富集分析主要是KEGG和GO。 思路 蛋白ID转UniProt数据库IDUniProt数据库ID转KEGG和GO号使用KEGG和GO号进行富集分析 教程&#xff08;实操…

5.CSS(二)

目录 一、Emmet语法 &#xff08;一&#xff09;快速生成HTML结构语法 &#xff08;二&#xff09;快速生成CSS样式语法 二、CSS的复合选择器 &#xff08;一&#xff09;后代选择器&#xff08;重要&#xff09; &#xff08;二&#xff09;子选择器&#xff08;重要&…

这些文档翻译软件助力你成功翻译外语文档

明华&#xff1a;嘿&#xff0c;你知道吗&#xff1f;我刚刚发现了三款超级好用的文档翻译软件&#xff01;简直就是我的救星啊&#xff01; 彦琪&#xff1a;真的吗&#xff1f;我在翻译文档的问题一直觉得很头痛。我想找一款网站来翻译文档&#xff0c;又不知道文档翻译在线…

Nginx教程(相关概念)

Nginx 简介 1、什么是Nginx Nginx(engine x") 是一个高性能的HTTP和反向代理服务器,特点是占有内存少&#xff0c;并发能力强&#xff0c;事实上nginx的并发能力确实在同类型的网页服务器中表现较好Nginx专为性能优化而开发&#xff0c;性能是其最重要的考量…

vue中在使用keep-alive时,会出现在页面跳转后el-tooltip或el-dropdown不消失的问题以及解决方法

一、 问题复现 跳转前&#xff1a; 跳转后&#xff1a; 二、分析 由于在vue中使用了keep-alive&#xff0c;页面在切换时&#xff0c;上一个页面的实例被缓存了&#xff0c;跳转后并没有销毁&#xff0c;所以才会残留 tooltip或dropdown&#xff0c;所以有以下解决思路&am…

[C++] C++入门第一篇 -- 命名空间,输入输出,缺省函数,函数重载底层原理

目录 1、关键字 2、命名空间 2.1 命名空间的定义 2.2 命名空间的使用方式 2.2.1 加命名空间名称及作用域限定符 2.2.2 使用using将命名空间中某个成员引入 2.2.3 使用using namespace 命名空间名称引入 3、C输入与输出 4、缺省参数 4.1 缺省参数的概念 4.2 缺省参数…

Blazor前后端框架Known-V1.2.7

V1.2.7 Known是基于C#和Blazor开发的前后端分离快速开发框架&#xff0c;开箱即用&#xff0c;跨平台&#xff0c;一处代码&#xff0c;多处运行。 Gitee&#xff1a; https://gitee.com/known/KnownGithub&#xff1a;https://github.com/known/Known 概述 基于C#和Blazor…