37 线程控制

内核中没有明确的线程的概念,线程作为轻量级进程。所以不会提供线程的系统调用,只提供了轻量级进程的系统调用,但这个接口比较复杂,使用很不方便,我们用户,需要一个线程的接口。应用层对轻量级进程的接口进行封装,为用户提供了应用层线程的接口,封装在pthread线程库。几乎所有的linux平台都默认自带这个库,编写多线程代码,需要使用第三方pthread库

目录

1.POSIX线程库
2.创建线程
3.进程ID和线程ID
4.线程id及进程地址空间布局
5.线程终止
6.线程等待
7.测试
8.c++11线程
9.多线程私有变量
10.线程分离

1. POSIX线程库

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

2. 创建线程

功能:创建一个新的线程
原型
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函数会这样做),而是将错误码通过返回值返回,可以调用sterror函数解析
pthreads同样也提供了线程内的errno变量,以支持其他使用errno的代码,对于pthreads函数的 错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小

测试

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

using namespace std;
void *run(void *args)
{
    while (true)
    {
        printf("new thread: %d\n", getpid());
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, run, nullptr);

    while (true)
    {
        printf("main thread: %d\n", getpid());
        sleep(1);
    }
}

因为是第三方库,所以编译时需要-l带上pthread库名

g++ -o $@ $^ -std=c++11 -lpthread

在这里插入图片描述

pid是一样的,说明是一个进程

3. 进程ID和线程ID

在linux中,目前的线程实现是Native POSIX Thread Library,简称NPTL,在这种实现下,线程又被称为轻量级进程(Light Weighted Process),每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)
没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。但是引入线程后,情况发生了变化,一个用户机场南管辖N个用户态线程,每个线程作为一个独立的调度实体在内核态都有自己的进程描述符,进程和内核的描述符一下子就变成了1:N关系,POSIX标准又要求 进程内的所有线程调用getpid函数时返回相同的进程ID,如何解决上述问题?
linux引入了进程组的概念

struct task_struct {
...
pid_t pid;
pid_t tgid;
...
struct task_struct *group_leader;
...
struct list_head thread_group;
...
};

多线程的进程,又称为线程组,线程组内的每一个线程在内核中都存在一个进程描述符与之对应。进程描述符结构体中的pid,表面上是对应进程id,起始对应线程ID,进程描述符的tgid,含义是Thread Group ID,对应用户层面的进程id

用户态系统调用内核进程描述符中对应的结构
线程idpid_t gettid (void)pid_t pid
进程idpid_t getpid (void)pid_t tgid

现在介绍的线程ID,不同于pthread_t类型的线程id,和进程id一样,线程ID是pid_t类型,而且是用来唯一标识线程的一个整型变量

查看轻量级进程

ps -aL

在这里插入图片描述在这里插入图片描述
ps命令的 -L选项,会显示如下信息:
LWP:线程id,即gettid()系统调用的返回值
NLWP: 线程组内线程的个数

进程id和线程id一样是主线程,内核称为(group leader),也就是进程,内核在创建第一个线程时,会将线程组的id的值设置为第一个线程线程id,group_leader指针指向自身,即主进程的进程描述符。不一样的是子线程

linux提供了gettid调用来返回线程id,可是glibc并没有将该系统调用封装起来,在开放接口来供使用。如果确实需要获得线程id,可以使用如下方法:

#include <sys/syscall.h>
pid_t tid;
tid = syscall(SYS_gettid);

/* 线程组ID等于线程ID,group_leader指向自身 */
p->tgid = p->pid;
p->group_leader = p;
INIT_LIST_HEAD(&p->thread_group);

至于线程组其他线程的id则由内核负责分配,其线程组id总是和主线程的线程组id一样,无论是主线程直接创建线程,还是创建出来的线程再次创建线程,都是这样

if ( clone_flags & CLONE_THREAD )
p->tgid = current->tgid;
if ( clone_flags & CLONE_THREAD ) {
P->group_lead = current->group_leader;
list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
}

强调一点,线程和进程不一样,进程有父进程的概念,在线程组里面,所有的线程都是对等关系

同一个线程组的线程,没有层次关系

4. 线程id及进程地址空间布局

创建轻量级进程的函数
在这里插入图片描述

上面的clone函数需要传入调用的函数,自定义栈空间,flag是要不要地址空间共享等待。一个是参数比较负责,一个是有的内容os不让调用。所以线程库对其封装,产生了上面的一些函数。
在这里插入图片描述
线程的概念是库维护的,线程库要加载到内存中。要维护多个线程属性合集,也要进行管理。线程要执行的函数是什么,栈空间在哪,线程id是什么,状态是什么,时间片还有多少等待,库需要维护。线程结构有一个线程控制块,存储用户关心的信息,包含线程的栈,id是什么,os关心的信息,LWP底层指向哪一个执行流,os才能运行。所以是用户级线程。对外返回的线程id实际上可以认为是这个线程结构的地址,如下图

pthread_creat会产生一个线程id,存放在第一个参数指向的地址中。该线程id和前面说的线程id不是一回事
前面的线程id属于进程调度的范畴,因为线程是轻量级进程,是os调度的最小单位,所以需要一个数值来唯一标识线程
pthread_creat函数第一个参数指向一个虚拟内存单元,该内存单元的地址即位新创建线程的现场id,属于NPTL线程库的范畴。现场库的后续操作,就是根据该线程id操作线程的

pthread_t pthread_self (void)

prhread_t是什么类型,取决于实现,对于目前实现的NPTL而言,pthread_t类型的线程id,本质是一个进程地址空间上的一个地址
在这里插入图片描述

5. 线程终止

如果需要终止某个线程有三种方法:
1.线程函数内调用return,这种方法对主线程不适用,从main函数return相当于调用exit
2.线程调用pthread_exit终止自己
3.一个线程可以调用pthread_cancel终止同一个进程中的另一个线程

pthread_exit

功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

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

pthread_cancel

功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码

6. 线程等待

为什么

已经退出的线程,空间没有被释放,仍然在地址空间中
创建新的现场不会复用刚才退出线程的地址空间

方法

功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码

调用该函数的线程终将被挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的 ,总结如下:
1.如果thread线程通过return返回,value_ptr指向的单元里存放的是thread的返回值
2.如果thread线程被别的线程调用thread_cancle异常终止,value_ptr所指向的单元里存放的是PTHREAD_CANCELED,值为-1
3.如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的是传给pthread_exit的参数
4.如果对thread线程的终止状态不感兴趣,可以传NULL做参数

7. 测试

线程的参数是void*,所以还可以返回结构体之类的结果。
用线程计算两个数之前所有数的求和,通过主线程返回取到结果,并判断结果的可靠性

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <cstring>

using namespace std;

struct Request
{
    Request(int st, int en)
    :start(st), end(en)
    {}
    int start;
    int end;
};

struct Response
{
    int result;
    int exitcode;  //结果是否可靠
};

void *sum(void *num)
{
    Request* eq = static_cast<Request*>(num);
    Response* ret = new Response;
    int i = eq->start;
    while (i <= eq->end)
    {
        ret->result += i;
        i++;
    }
    delete eq;
    return ret;
}

int main()
{
    pthread_t tid;
    Request* req = new Request(1, 100);
    pthread_create(&tid, nullptr, sum, req);
    void *ret;
    pthread_join(tid, &ret);

    printf("结果:%d\n", ((Response *)ret)->result);
}

用一个类接收两数区间,申请一个类,传入转换为对应类型计算,申请结构类放入返回,最后转换类型输出
在这里插入图片描述
如果计算量比较大,可以将一个计算分成几组交给每个线程,父线程将所有结果汇总

8. c++11线程

pthread是原生线程库,c++11对这个库进行了封装,也支持多线程了
编译的时候仍然需要带上库连接

void* run()
{
    while (true)
    {
        printf("子线程\n");
        sleep(1);
    }

}

int main()
{
    thread th(run);
    th.join();
    return 0;
}

原生线程只能用于linux平台,库的封装基于不同平台会有不同的实现,想跨平台的话得用库函数

线程的完整结构是用户级线程+内核级LWP,所谓用户级还是内核级的区分是线程在哪里实现。linux是用户级线程,用户执行流和内核lwp是1:1的

9. 多线程私有变量

生成多条线程,打印地址

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <cstring>
#include <thread>
#include <vector>

using namespace std;

void* run(void* num)
{
    long n = (long)num;
    int cnt = 5;
    while (cnt--)
    {
        printf("第%d条线程,地址:%p\n", n, pthread_self());
        sleep(1);
    }
       
}

int main()
{
    vector<pthread_t> v;
    for (long i = 0; i < 3; i++)
    {
        pthread_t tid;
        pthread_create(&tid, nullptr, run, (void*)i);
        v.push_back(tid);
    }

    sleep(1);
    for (size_t i = 0; i < 3; i++)
    {
        pthread_join(v[i], nullptr);
    }
}

在这里插入图片描述

每条线程的栈空间都是不一样的。对于全局变量,每个线程都可以访问,这种叫共享资源。如果主线程想要得到某个线程的值,也可以取到,通过全局变量判断是哪条线程赋值,主线程访问全局变量。这里取第1条线程:
在这里插入图片描述
线程和线程之间,几乎没有秘密,栈的数据,也可以被其他线程看到并访问
尽量让每个线程的变量独立,不要互相影响

线程的局部存储

怎么让每个线程拥有独立的全局变量,可以前面加__pthread。前面线程的机构里说线程的tcb结构里有线程的局部存储,这部分存储的就是动态库中的这些变量,是编译器提供的编译选项,会给每个线程提供一份

__thread int g_val;

打印一下这个地址观察
在这里插入图片描述

每条线程对于这个变量都会有独立的地址,也就是每个线程都拥有私有的变量
局部存储只能定义内置类型,不能修饰自定义类型
局部存储可以保存线程里调用流需要多次读取的值,不需要传入参数或调用函数就可以访问

10. 线程分离

线程的等待是阻塞式的,但如果想让主线程做其他事情,同时不关心次线程的执行结果,可以将线程分离
默认情况下,新创建的线程是joinable的,线程退出后,需要join操作,否则无法释放资源造成内存泄露
分离后线程退出会自动释放资源

int pthread_detach(pthread_t thread);

可以是线程组内其他线程对目标线程分离,也可以线程自己分离自己。joinable和分离是冲突的,不能既joinable又分离。分离后继续join会失败

在这里插入图片描述

detch的分离实际上是修改tcb的属性,记录有没有被分离,如果分离了就不能等待

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

企业如何保证内部传输文件使用的工具是安全的?

企业内部文件的频繁交换成为了日常运营不可或缺的一环。然而&#xff0c;随着数据量的爆炸式增长和网络攻击手段的日益复杂&#xff0c;内网文件传输的安全隐患也日益凸显&#xff0c;成为企业信息安全的薄弱环节。本文将探讨内网文件传输的安全风险、企业常用的防护措施。 内网…

Python轻量级Web框架Flask(12)—— Flask类视图实现前后端分离

0、前言&#xff1a; 在学习类视图之前要了解前后端分离的概念&#xff0c;相对于之前的模板&#xff0c;前后端分离的模板会去除views文件&#xff0c;添加两个新python文件apis和urls&#xff0c;其中apis是用于传输数据和解析数据 的&#xff0c;urls是用于写模板路径的。 …

终于有人把无人机5G通信原理讲清楚了

在现代科技快速发展的背景下&#xff0c;无人机技术在各个领域都有了广泛应用&#xff0c;从送外卖到农业监控&#xff0c;无人机正变得越来越普遍。然而&#xff0c;无人机的效能很大程度上受到其通信系统的限制&#xff0c;尤其是在城市这种高楼林立、障碍物众多的环境中。为…

五一旅游必备物品清单 建议把这份清单记在备忘录

五一小长假就要来临&#xff0c;相信很多人已经跃跃欲试&#xff0c;准备带着家人或朋友外出旅游&#xff0c;享受这难得的休闲时光。出游总是让人兴奋不已&#xff0c;但带小孩出游&#xff0c;行李准备可是一项大工程。为了让旅程更加顺利&#xff0c;提前列一份必备物品清单…

Python绘制3D曲面图

&#x1f47d;发现宝藏 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 探索Python中绘制3D曲面图的艺术 在数据可视化的世界中&#xff0c;3D曲面图是一种强大的工…

压铸机PQ控制阀比例放大器

压铸机PQ控制阀比例放大器是确保压铸机正常工作的重要组成部分&#xff0c;它通常由多种液压元件组成&#xff0c;负责提供动力和控制系统中各个部件的运动。液压系统通过液体&#xff08;通常是油&#xff09;传递压力能&#xff0c;以驱动机械装置工作。在压铸机中&#xff0…

ElasticSearch教程入门到精通——第五部分(基于ELK技术栈elasticsearch 7.x+8.x新特性)

ElasticSearch教程入门到精通——第五部分&#xff08;基于ELK技术栈elasticsearch 7.x8.x新特性&#xff09; 1. Elasticsearch集成1.1 框架集成-SpringData-整体介绍1.2 Spring Data Elasticsearch 介绍1.3 框架集成-SpringData-代码功能集成1.3.1 创建Maven项目1.3.2 修改po…

[C++] 类和对象 _ 剖析构造、析构与拷贝

一、构造函数 构造函数是特殊的成员函数&#xff0c;它在创建对象时自动调用。其主要作用是初始化对象的成员变量&#xff08;不是开辟空间&#xff09;。构造函数的名字必须与类名相同&#xff0c;且没有返回类型&#xff08;即使是void也不行&#xff09;。 在C中&#xff0…

Yolov5简单部署(使用自己的数据集)

一.注意事项 1.本文主要是引用大佬的文章&#xff08;侵权请联系&#xff0c;马上删除&#xff09;&#xff0c;做的工作为简单补充 二.正文 1.大体流程按照 准备&#xff1a;【简单易懂&#xff0c;一看就会】yolov5保姆级环境搭建_哔哩哔哩_bilibili 主要过程&#xff1…

Java | Leetcode Java题解之第55题跳跃游戏

题目&#xff1a; 题解&#xff1a; public class Solution {public boolean canJump(int[] nums) {int n nums.length;int rightmost 0;for (int i 0; i < n; i) {if (i < rightmost) {rightmost Math.max(rightmost, i nums[i]);if (rightmost > n - 1) {retu…

VitePress 构建的博客如何部署到 github 平台?

VitePress 构建的博客如何部署到 github 平台&#xff1f; 1. 新建 github 项目 2. 构建 VitePress 项目 2.1. 设置 config 中的 base 由于我们的项目名称为 vite-press-demo&#xff0c;所以我们把 base 设置为 /vite-press-demo/&#xff0c;需注意前后 / export default…

tidb离线本地安装及mysql迁移到tidb

一、背景&#xff08;tidb8.0社区版&#xff09; 信创背景下不多说好吧&#xff0c;从资料上查tidb和OceanBase“兼容”&#xff08;这个词有意思&#xff09;的比较好。 其实对比了很多数据库&#xff0c;有些是提供云服务的&#xff0c;有些“不像”mysql&#xff0c;综合考虑…

uniapp:K线图,支持H5,APP

使用KLineChart完成K线图制作,完成效果: 1、安装KLineChart npm install klinecharts2、页面中使用 <template><view class="index"><!-- 上方选项卡 --><view class="kline-tabs"><view :style="{color: current==ite…

Windows使用bat远程操作Linux并执行命令

背景&#xff1a;让客户可以简单在Windows中能自己执行 Linux中的脚本&#xff0c;傻瓜式操作&#xff01; 方法&#xff1a;做一个简单的bat脚本&#xff01;能远程连接到Linux&#xff0c;并执行Linux命令&#xff01;客户双击就能使用&#xff01; 1、原先上网查询到使用P…

深度学习:基于Keras框架,使用神经网络模型对葡萄酒类型进行预测分析

前言 系列专栏&#xff1a;机器学习&#xff1a;高级应用与实践【项目实战100】【2024】✨︎ 在本专栏中不仅包含一些适合初学者的最新机器学习项目&#xff0c;每个项目都处理一组不同的问题&#xff0c;包括监督和无监督学习、分类、回归和聚类&#xff0c;而且涉及创建深度学…

《Fundamentals of Power Electronics》——全桥型隔离降压转换器

以下是关于全桥型隔离降压转换器的相关知识点&#xff1a; 全桥变压器隔离型降压转换器如下图所示。 上图展示了一个具有二次侧绕组中心抽头的版本&#xff0c;该电路常用于产生低输出电压。二次侧绕组的上下两个绕组可以看作是两个单独的绕组&#xff0c;因此可以看成是具有变…

CSS-复合选择器

作用&#xff1a; 后代选择器&#xff1a; 子代选择器 并集选择器 用逗号隔开&#xff0c;在style里面写的时候&#xff0c;每一个标签空一行。 <title>Document</title><style>p,div,span{color: aqua;}</style> </head> <body><p>…

Java对象在堆和栈上的存储(对象布局,待完善)

0、前言 这里提到的 Java 对象不仅仅包含引用类型&#xff08;Object&#xff09;&#xff0c;还包含基本数据类型&#xff08;boolean、int、long、float、double&#xff09;。文中部分图片来源于 B站 黑马程序员。 1、在栈上的数据存储 1.1、局部变量 局部变量包含以下情…

C++:map和set的封装

关于红黑树的模拟实现&#xff0c;大家不清楚的先去看看博主的博客再来看这篇文章&#xff0c;因为set和map的封装底层都是利用用的红黑树。所以这里不会过多介绍红黑树的相关内容&#xff0c;而更多的是去为了契合STL中的红黑树去进行改造&#xff0c;让封装的set和map能够去复…

【Java】Java基础 使用集合实现斗地主分牌

&#x1f4dd;个人主页&#xff1a;哈__ 期待您的关注 今天使用集合TreeSet来实现一个斗地主的分牌流程。 TreeSet集合的一个特点就是 元素有序&#xff0c;这样就方便我们分的牌自动排序。 0.思路 1.创建玩家手牌集合 我们到时候分的牌都存储在这里&#xff0c;但你可能会…