Linux——线程控制

目录

前言

一、线程创建

1.创建线程

2.线程传递结构体 

3.创建多线程 

4.收到信号的线程

二、线程终止

三、线程等待

四、线程分离

五、取消线程

六、线程库管理的原理

七、站在语言角度理解pthread库

八、线程的局部存储


前言

前面我们学习了线程概念和线程创建,今天我们学习线程控制,如何操控一个线程完成任务,同时能取消线程、等待线程,分离线程。

一、线程创建

1.创建线程

功能:创建一个新的线程

  • 参数 thread:返回线程ID
  • attr:设置线程的属性,attr为NULL表示使用默认属性
  • start_routine:是个函数地址,线程启动后要执行的函数
  • arg:传给线程启动函数的参数
  • 返回值:成功返回0;失败返回错误码
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;void* TreadToutine(void *arg)
{const char* threadname = (const char*) arg;while(1){cout<<"我是一个新线程"<<threadname<<endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid,NULL,TreadToutine, (void*)"thread 1");//主线程while(1){cout<<"我是主线程"<<endl;sleep(1);}return 0;
}

linux没有真正的线程概念,他的线程是复用的进程代码,只是做了一些区分。线程客观的可以叫做轻量级进程。因此Linux只会提供轻量级进程创建的函数调用,不会直接提供线程创建的接口。因此我们使用pthread原生线程库,编译时需要手动链接库文件(-lpthread)。

这样编译后就可以运行了。

从上面代码可以看出,给线程传递的信息可以是char*,由于pthread_create函数的最后一个参数为void*,同时线程去运行的函数参数也是void*,因此我们任意类型都可以传递过去,进行一下强转即可。

2.线程传递结构体 

比如现在我想传递很多内容过去,叫线程帮我们处理 

如下,我们传递了结构体 

线程成功收到结构体,并做出了处理。

3.创建多线程 

 创建多线程也很简单,只需要循环创建即可。

4.收到信号的线程

如果进程创建的线程有一个发生了异常,收到了信号,会导致整个进程都被终止,因为线程是进程创建出来的,发送信号是发给了进程,进程如果退出,那么该进程所有的资源也都得被回收。而线程本身就是进程资源的一部分。

二、线程终止

我们知道线程去执行的函数返回类型为void*,当线程执行结束,return时,线程就自动终止了

如果我们返回时调用exit()函数 ,那么整个进程都会被终止

同时,pthread.h库还给我们提供了 pthread_exit() 接口,我们使用该接口也可以终止线程。

pthread_exit()

作用:终止一个运行的线程

参数retval:返回void*的全局变量

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

代码如下,两种方法都可以退出 

 运行结果如下,线程被退出,不再打印消息。

三、线程等待

线程退出默认要被等待,如果不等待,就会发生类似于僵尸进程的问题。因此我们需要用pthread_join()函数进行等待

pthread_join()

功能:等待线程结束

  • 参数1::thread:线程ID
  • 参数2:value_ptr:它指向一个指针,后者指向线程的返回值(void**指向的线程返回值void*)
  • 返回值:成功返回0;失败返回错误码

代码如下,让子线程程循环5次后退出并传参常量字符串,主线程去join等待,并将等待的结果输出。

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
using namespace std;class Add
{
public:Add(string name, int a, int b): _name(name), _a(a), _b(b){}public:string _name;int _a;int _b;
};void *TreadToutine(void *arg)
{Add *a1 = (Add *)arg;int cnt = 5;while (cnt--){cout << "我是一个新线程: " << a1->_name << ",计算结果为" << a1->_a + a1->_b << endl;sleep(1);}//return nullptr;pthread_exit((void*)"pthread-1 退出"); //常量区
}int main()
{pthread_t tid;Add *td = new Add("thread-1", 10, 20);pthread_create(&tid, NULL, TreadToutine, td);// 主线程cout << "我是主线程,子线程的tid: "<< tid << endl;void* msg = nullptr;pthread_join(tid,&msg);cout<<"等待成功,子线程退出信息: "<<(char*)msg <<endl;sleep(1);return 0;
}

等待成功,同时输出了消息。注意等待是阻塞式等待,子线程退出后才会执行后续代码。

四、线程分离

我们知道,线程是需要被等待的,不然会发生类似于僵尸进程的现象,那么如果我想让线程一直去运行,比如说一直帮我播放音乐,那么主线程就会一直等待,不可能执行后面的代码。

在这种情况下,我们可以让线程分离,也就是主线程不再关心创建的子线程的死活,他要运行就运行,不运行了操作系统会回收。不过一般建议主线程最后再退出

可以使用pthread_detach()函数进行线程的分离。

pthread_detach()

作用:分离线程

分离线程很简单,直接调用pthread_detach()就可以,我们不过多展示,下面代码是先分离线程,再等待线程看看会发生什么。 

发现等待线程的返回值为22,不是0证明等待失败,22的意思是该线程不需要等待。

这是我们是在主线程进行分离的,子线程也可以被分离, 由于子线程默认看不到自己的tid,因此可以调用pthread_self()函数获取自己的tid。

pthread_self

作用:让线程获取自己的tid

如下是子线程选择分离。 

小总结:

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

五、取消线程

主线程可以取消线程,也就是让子线程退出,可以调用pthread_cancel()函数进行终止线程。

pthread_cancel()

功能:向线程发送取消请求

  • 参数1:thread,线程ID
  • 返回值:成功返回0;失败返回错误码

代码如下,先取消进程,再等待线程,同时查看线程退出码

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
using namespace std;void *TreadToutine(void *arg)
{while(1){cout << "我是一个新线程" << endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, NULL, TreadToutine, (void *)"pthread-1");sleep(3);// 取消线程int n = pthread_cancel(tid);cout << "线程取消成功,n: " << n << endl;// 等待线程void *ret = nullptr;n = pthread_join(tid, &ret);cout << "等待线程返回值n: " << n << ",线程返回值: " << (int64_t)ret << endl;return 0;
}

运行看到,线程返回值为0,取消成功,等待返回值为0,等待成功。我们看到线程没有阻塞在等待函数这里,而是直接往后运行,同时进程返回为-1。

这是因为如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数 PTHREAD_ CANCELED

而如果线程先被脱离,再取消,结果怎么样呢?

发现也是能被取消的,但是线程等待是22(等待失败)。因为系统直接回收了。 

小总结:

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

2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_CANCELED。

3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。

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

六、线程库管理的原理

我们对线程的操作一直要使用tid,那么tid里面的内容到底是什么呢?

其实他是一个地址,我们转成16进程来看一下。

确实是很像是地址,但这跟LWP(Light Weight Process)也不一样啊。该如何理解呢? 

  • 首先,我们知道pthread. h不是操作系统的接口,而是原生线程库。那么用户创建的线程,操作系统无法管理,则需要线程库来进行管理。他从系统中获取轻量级进程相关属性,从用户中也获取一些属性,这样就先描述起来了,再通过数据结构将线程组织起来,就将线程管理好了。
  • 我们也知道,线程要有独立属性,独立的主要有硬件上下文和栈空间,其中硬件上下文跟操作系统有关,而栈空间则是要从用户中来。栈不是只有一个吗?为什么每一个线程都有自己的栈空间呢?这其实是操作系统帮我们处理了的,操作系统会在堆区创建空间,来充当线程独立的栈。pthread库会获取到栈空间,并将他管理维护好,而默认地址空间中的栈,由主线程使用。

那么线程库如何管理呢,在哪管理呢? 

  • 在进程地址空间中,mmap(共享区)加载了动态库,其中我们使用的pthread库就在该区域,他会管理好每一份线程,每一份线程都在其中有自己的属性集。
  • struct pthread里存在很多线程属性,线程局部存储,还有线程栈,这个栈指向的是堆空间的区域,每当有新线程被创建,都会在后面继续创建这种数据结构。就这样将多个线程统一的描述组织起来了,可以进行管理了。因此我们调用pthread相关函数,相当于对该空间进行访问、处理。

那么现在,我们也可以理解 pthread_t tid 是什么了,他不就是每一个线程在进程地址空间的起始地址嘛,我们pthread_create 对tid进行写入,因为需要创建对应的数据结构,找到起始地址,然后返回,后续用户要继续对线程进行控制,等待啊,终止啊,分离啊,取消啊。都需要传入tid,也就是能找到在进程地址空间的位置后,才可以处理。

七、站在语言角度理解pthread库

我们之前学的pthread库,是Linux提供的原生线程库,在语言层面,比如C++/JAVA\PYTHON,他们也会提供给我们线程库。

我们写了一份代码,使用的是C++提供的线程库 thread

 #include<iostream>#include<unistd.h>#include<thread>using namespace std;void myrun()
{while(1){cout<<"我是一个新线程"<<endl;sleep(1);}
}int main(){thread t(myrun);t.join();}

编译后运行,发现说多线程操作被禁止了,这是因为我们没有链接pthread库。

c++提供的线程库封装了pthread.h。因此我们编译时仍然需要链接 pthread库。

到现在,我们可以知道,语言上也许线程库的使用不一定相同,但是他们底层都是用的linux原生线程库。

在Linux下做了封装,那么这段代码我们可以在Linux中运行。

如果thread头文件在Windows下,封装了Windows线程的操作,那么也可以在Windows下运行。这大大提高了文件的可移植性。

八、线程的局部存储

我们定义一个全局变量,创建线程,让新线程对全局变量做++,观察新线程和主线程全局变量是否发生变化。

#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;int g_val = 100;void *TreadToutine(void *arg)
{while (1){cout << "我是一个新线程,g_val: " << g_val << ",&g_val: " << &g_val << endl;g_val++;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, NULL, TreadToutine, (void *)"Thread 1");while (1){cout << "我是一个主线程,g_val: " << g_val << ",&g_val: " << &g_val << endl;sleep(1);}pthread_join(tid,nullptr);
}

 我们可以看到,全局变量值一样,地址也一样,我们现在知道全局变量是被所有进程共享的。

如果我们给全局变量前添加上__thread,GCC/G++编译器提供的一个扩展,用于声明线程局部存储变量。

现在运行,主线程和新线程g_val不一样,地址也不一样。

因为我们添加的__thread 会在G++编译时,给每个线程的局部存储空间里将变量拷贝进程,私有一份,于是每个线程自己管理自己的那一份资源。不再与外部共享。 

 只是__thread只能修饰内置类型,如string这种自定义类型无法处理。

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

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

相关文章

Topaz Video AI for Mac v5.0.0激活版 视频画质增强软件

Topaz Video AI for Mac是一款功能强大的视频处理软件&#xff0c;专为Mac用户设计&#xff0c;旨在通过人工智能技术为视频编辑和增强提供卓越的功能。这款软件利用先进的算法和深度学习技术&#xff0c;能够自动识别和分析视频中的各个元素&#xff0c;并进行智能修复和增强&…

k8s + springcloud 微服务开发调试工具kt Connect的使用

概览 KtConnect(全称Kubernetes Toolkit Connect)是一款基于Kubernetes环境用于提高本地测试联调效率的小工具。 通过这个工具,可以不在本地启动所有服务,只需启动当前开发的服务即可,其它服务使用的是部署在k8s集群的实例,如下图: Reference 官方文档:https://gith…

vCenter Server出现no healthy upstream的解决方法

https://blog.51cto.com/wangchunhai/4907250 访问vCenter 7.0 地址后&#xff0c;页面出现“no healthy upstream”,无法正常登录vCenter&#xff0c;重启后依旧如此&#xff0c;该故障的前提是没有对vCenter做过任何配置&#xff0c;如下图所示。 尝试登录"VMware vCen…

写时拷贝技术

不采用写时拷贝,如何fork? 第一:复制开销比较大; 第二:占用内存空间; 所以我们对fork复制进程的过程就做了一个优化-----写时拷贝技术; 综上,就是fork的时候,子进程直接把父进程的页表复制过来,子进程发生写入(修改)的时候才分配内存复制,然后进行相应的页表修改.写时拷贝是…

使用deepspeed小记

1. 减少显存占用的历程忠告 医学图像经常很大&#xff0c;所以训练模型有时候会有难度&#xff0c;但是现在找到了很多减少显存的方法。 不知道为什么&#xff0c;使用transformers的trainer库确确实实会减少显存的占用&#xff0c;即使没有使用deepspeed&#xff0c;占用的显…

【蓝桥杯嵌入式】11届程序题刷题记录及反思

一、题目介绍 按键输入&#xff1a;短按 模拟电压输出&#xff1a;ADC LCD显示 PWM输出&#xff1a;PA6,PA7 二、usr.c #include "usr.h" #include "lcd.h" #include "stdio.h" #include "tim.h" #include "adc.h" /*v…

Ps:颜色查找

颜色查找 Color Lookup命令通过应用预设的 LUT 来改变图像的色彩和调性&#xff0c;从而为摄影师和设计师提供了一种快速实现复杂色彩调整的方法&#xff0c;广泛应用于颜色分级、视觉风格的统一和创意色彩效果的制作。 Ps菜单&#xff1a;图像/调整/颜色查找 Adjustments/Colo…

C++11---右值引用(深度讲解)

简要介绍 右值引用是C11的新特性,无论左值引用还是右值引用&#xff0c;都是在给对象取别名 什么是左值 什么是右值 1.左值,左值引用 左值是一个数据的表达式(例如变量或者解引用后的指针),我们可以对其进行取地址和修改赋值,左值可以出现在赋值符号的左边,而右值不能出现在…

C语言 练习题

目录 1.统计二进制中1的个数 方法1 方法2 方法3 2.求两个数二进制中不同位的个数 方法1 方法2 3.打印整数二进制的奇数位和偶数位 4.用“ * ”组成的X形图案 5.根据年份和月份判断天数 6.结语 1.统计二进制中1的个数 【题目内容】 写一个函数返回参数二进制中 1 的个…

YARN集群 和 MapReduce 原理及应用

YARN集群模式 本文内容需要基于 Hadoop 集群搭建完成的基础上来实现 如果没有搭建&#xff0c;请先按上一篇: <Linux 系统 CentOS7 上搭建 Hadoop HDFS集群详细步骤> 搭建&#xff1a;https://mp.weixin.qq.com/s/zPYsUexHKsdFax2XeyRdnA 配置hadoop安装目录下的 etc…

web-AOP

AOP基础 AOP进阶 通知顺序和类型 切入点表达式 连接点

任意设定蜂鸣器响的次数

这次来分享一个比较有意思的知识点 控制蜂鸣器响的次数 首先&#xff0c;我自己画了一个蜂鸣器的小模块&#xff0c;用来测试的。如下图 实物和原理图如上图 下面是代码解释

回顾快速排序

快速排序 快速排序的核心&#xff1a; 找到一个key 通常左边的数比key小&#xff0c;右边的数比key大。 找key通常有三种方法&#xff1a; 1. 挖坑法&#xff1a; 代码实现&#xff1a; // int _pivot(int* a, int left, int right) {int begin left, end right;int in…

既然有 HTTP 协议,为什么还要有 WebSocket?(计算机网络)

看起来服务器主动发消息给客户端的场景&#xff0c;是怎么做到的&#xff1f; 使用 HTTP 不断轮询 怎么样才能在用户不做任何操作的情况下&#xff0c;网页能收到消息并发生变更。 最常见的解决方案是&#xff0c;网页的前端代码里不断定时发 HTTP 请求到服务器&#xff0c;服…

联系媒体要有方法莫让投稿发文章只剩一声长叹相见恨晚

曾有一位饱经世事的前辈以一句至理名言警醒世人:“人之所以领悟道理,往往不是源于抽象的道理本身,而是生活给予的实实在在的挫折教训,如同撞南墙一般的痛彻觉醒;同样,让人豁然开朗的,也不是空洞的说教,而是实实在在的人生磨砺。”这一哲理,放在我们日常工作中亦有深刻的启示作用…

DHCP原理重磅来袭——走过路过不要错过

目录 一.DHCP来源 &#xff08;1)手工分配缺点 (2)DHCP优点 二.DHCP设备调试 &#xff08;1&#xff09;.基本配置&#xff1a; &#xff08;2&#xff09;接口地址池 1.开启DHCP功能 2.开启DHCP接口地址池功能 3.查看IP地址分配结果 &#xff08;3&#xff09;全局地…

Grafana实时监控minio的极简方法

背景 想监控一下minio的部分信息. 使用过程中需要关注的内容挺多的. 只看简单的node感觉已经不够了. 所以想监控易一下. ERLANG 复制 全屏 方式和方法 minio其实集成了prometheus 支持的监控指标 只需要在配置文件中放开就可以了. 虽然可以使用mc 的命令 create beartoken 但…

每日一题:c语言实现n的阶乘

目录 一、要求 二、代码 三、结果 一、要求 实现n的阶乘&#xff0c;已知n&#xff01;1*2*3*…*n 二、代码 #define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h>int main() {//初始化变量n为要求的几阶&#xff0c;jiecheng存储结果的&#xff0c;初始化为1…

AWTK 开源串口屏开发(15) - 通过 MODBUS 访问远程设备数据

在 AWTK 串口屏中&#xff0c;内置了 MODBUS Client 的模型&#xff0c;支持用 MODBUS 协议从远程设备获取数据。不用编写一行代码即可实现对远程设备数据的显示和修改。 1. 功能 不用编写代码&#xff0c;实现对远程设备数据的显示和修改。 2. 创建项目 从模板创建项目&am…

UGUI 进阶

UI事件监听接口 目前所有的控件都只提供了常用的事件监听列表 如果想做一些类似长按&#xff0c;双击&#xff0c;拖拽等功能是无法制作的 或者想让Image和Text&#xff0c;RawImage三大基础控件能够响应玩家输入也是无法制作的 而事件接口就是用来处理类似问题 让所有控件都…