[Linux]:线程(一)

img

✨✨ 欢迎大家来到贝蒂大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:Linux学习
贝蒂的主页:Betty’s blog

1. 初识线程

1.1 线程的概念

在操作系统中,进程与线程一直是我们非常关注的话题,它们共同构建了程序的执行环境,前面我们已经介绍了进程,今天我们要了解的就是线程,在此之前,我们就得先谈谈进程与线程的区别:

  • 进程:进程是程序的一次执行实例。它是操作系统资源分配的基本单位。每个进程都有自己的内存空间、进程地址空间,文件描述符表、全局变量等系统资源。
  • 线程:线程是进程中的一个执行单元。一个进程可以包含多个线程,它们共享进程的资源(如内存、进程地址空间,文件描述符表等),但每个线程有自己独立的寄存器(存储上下文)、栈(保存临时数据)和程序计数器(记录指令执行位置),errno(错误码) 等。

操作系统为了方便多个进程,有了进程控制块 PCB,同样为了管理我们的线程也应该创建我们的TCB 结构(thread ctrl block),但是值得一提的是:在我们 Linux操作系统中,为了提高代码的可复用性,降低我们的维护成本,采用进程的内核数据结构也就是 task_struct来模拟的线程,所以我们常说 Linux中没有真正意义上的线程。

并且 CPU 中只有执行流的概念,所以原则上来说 CPU并不会区分进程与线程,但是 Linux操作系统需要区分线程与进程,所以我们可以称线程为轻量化进程。

画板

其中上图我们用虚线框住的就是我们的进程,而一个 task_struct代表的就是一个线程。并且进程与线程的关系如下图:

画板

1.2 线程的优缺点

1.2.1 线程的优点
  1. 创建一个新线程的代价要比创建一个新进程小得多。
  2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
  3. 线程占用的资源要比进程少很多。
  4. 能充分利用多处理器的可并行数量。
  5. 在等待慢速 IO 操作结束的同时,程序可执行其他的计算任务。
  6. 计算密集型应用,为了能在多处理器系统上运行。将计算分解到多个线程中实现。
  7. IO 密集型应用,为了提高性能,将 IO 操作重叠,线程可以同时等待不同的 IO 操作。

其中计算密集型指:执行流的大部分任务,主要以计算为主。比如加密解密、大数据查找。IO 密集型指:执行流的大部分任务,主要以 IO 为主。比如刷磁盘、访问数据库、访问网络等。

1.2.2 线程的缺点
  1. 性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器,如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的新能损失(切换浪费时间),这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  2. 健壮性降低:编写多线程需要更全面深入的考虑,在一个线程程序里,因时间分配上的细微偏差或因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说就是线程之间缺乏安全保护。
  3. 缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS 函数会对整个进程造成影响。
  4. 编程难度提高:编写与调试一个多线程程序比单线程程序困难的多。

1.3 线程异常

  1. 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
  2. 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。

1.4 线程用途

  1. 合理的使用多线程,能提高 CPU 密集型程序的执行效率。
  2. 合理的使用多线程,能提高 I/O 密集型程序的用户体验(如一边写代码一边下载开发工具,就是多线程运行的一种表现)

2. 线程控制

Linux 的内核中只有轻量级进程的概念,并无明确的线程概念,因此 Linux 操作系统不会直接为用户提供线程的系统调用,仅会提供轻量级进程的系统调用。然而这些系统调用使用成本较高,所以在应用层又为用户开发出了 pthread 线程库。几乎所有的 Linux 平台都默认自带这个库。在 Linux 中编写多线程代码需要使用第三方的 pthread 库。

因为 pthread线程库是第三方为用户提供的动态库,也叫原生线程库,所以编译时需要加上 -lpthread选项。

2.1. 线程创建

  1. 函数原型:int pthread_create(pthread_t thread,const pthread_attr_t attr,void(start_routine)(void),voidarg);
  2. 参数:
  • thread:输出型参数,返回用户层线程 ID
  • attr:设置线程的属性,为 NULL 表示使用默认属性。
  • start_routine:是个函数地址,线程启动后要执行的函数。
  • arg:传给启动线程的参数。
  1. 返回值:创建成功返回0,创建失败返回对应的错误码。

比如下面这段代码,我们让其创建出一个线程,并观察其 PID

#include<iostream>
using namespace std;
#include<unistd.h>
#include<pthread.h>
void* threadRoutine(void*args)
{while(true){cout<<"new thread,pid: "<<getpid()<<endl;sleep(2);}return nullptr;
}
int main()
{pthread_t tid;pthread_create(&tid,nullptr,threadRoutine,nullptr);while(true){cout<<"main thread,pid: "<<getpid()<<endl;sleep(1);}return 0;
}

我们观察到主线程与新创建的线程的 pid相同,这样证明它们是同属于一个进程的。

并且我们也可以通过指令ps -aL查看当前操作系统中的所有轻量级线程。

其中LWP就是指一个轻量级进程的 ID,如果一个线程的PID == LWP ,我们就称该线程为主线程。

然后我们可以在了解一个接口 pthread_self获取当前用户层线程的 ID(即 pthread_create 第一个参数),其原型如下:

pthread_t pthread_self(void);//其中 pthread_t一般是一个无符号长整型,具体取决于实现

比如我们可以创建五个线程,分别打印其进程与线程 ID观察。

#include <iostream>
using namespace std;
#include <unistd.h>
#include <cstdio>
#include <pthread.h>
void *Routine(void *args)
{char *buffer = (char *)args;cout << "I am " << buffer << ",pid:" << getpid() << ", tid:" << pthread_self() << endl;sleep(1);return nullptr;
}
int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char buffer[64] = {'\0'};snprintf(buffer, sizeof(buffer), "thread %d", i);pthread_create(&tid[i], nullptr, Routine, buffer);sleep(1);}while (true){cout << "I am main thread,pid:" << getpid() << ", tid:" << pthread_self() << endl;sleep(1);}return 0;
}

值得注意的是: pthread_self 函数获得的线程 ID 与内核的 LWP 值是不相等的,pthread_self 函数获得的是用户级原生线程库的线程 ID,而 LWP 是内核的轻量级进程 ID,它们之间是一对一的关系。

要想搞清楚用户级原生线程库的线程 ID与内核 LWP的区别,我们首先得明白所使用的原生线程库本质其实就是一个动态库,在程序运行时,其会被加载到内存共享区中。

画板

上面我们就提到每一个线程都有自己独立的栈结构,其中主线程采用的栈是进程地址空间中的原生栈,而其余线程采用的栈就是在共享区中开辟的。除此之外,每个线程都有自己的 struct pthread结构,当中包含了对应线程的各种属性;每个线程还有自己的线程局部存储,当中包含了对应线程被切换时的上下文数据。

每一个新线程在共享区都存在一块对其进行描述的区域,所以要找到一个用户级线程,只需找到该线程内存块的起始地址,这样就能获取到该线程的各种信息。所以用户层线程 <font style="color:rgb(28, 31, 35);">ID</font>本质就是一个指向线程起始位置的虚拟地址。

画板

每个线程在创建后都需拥有独立的栈结构。原因在于每个线程都具备自身的调用链,而执行流的本质正是调用链。此栈空间能够保存一个执行流在运行期间所产生的临时变量,并且在函数调用时进行入栈操作。而 LWP则只是操作系统在内核唯一标识轻量级进程的编号。

2.2 线程等待

其实一个线程被创建出来也是需要被等待的,如果不等待,也会发生类似进程的"僵尸"问题,即内存泄漏。而线程等待我们需要使用的接口是 pthread_join

  1. 函数原型:int pthread_join(pthread_t thread,void**retval);
  2. 参数:
  • thread:要等待的线程 ID。
  • retval:输出型参数,获取线程函数的返回值,如果不关心可传 nullptr
  1. 返回值:等待成功返回 0;等待失败,返回对应的错误码。

比如下面我们创建一个线程,让其退出后返回一个值,让线程等待获取这个值。

#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
int g_val = 100;
void *threadRoutine(void *args)
{const char *name = (const char *)args;int cnt = 5;while (true){printf("%s, pid: %d, g_val: %d, &g_val: %p\n", name, getpid(), g_val, &g_val);sleep(1);cnt--;if (cnt == 0)break;}return (void *)100;
}
int main()
{pthread_t pid;pthread_create(&pid, nullptr, threadRoutine, (void *)"Thread 1");void *ret;pthread_join(pid, &ret);cout << "main thread quit..., Thread 1 return val: " << (long long int)ret << endl;return 0;
}

其中主线程等待时,默认是阻塞等待。当然在线程运行时也可能发生异常退出,比如除零错误,这时整个进程都会异常退出,此时我们就可以接受 pthread_join的返回值,判断具体是什么异常。

2.3 线程终止

我们除了在一个线程函数中使用 return终止线程外,还可以通过接口 pthread_exit终止线程,其具体用法如下:

  1. 函数原型:void pthread_exit(void*retval);
  2. 参数:retval :线程函数的返回值。

比如下面我们创建一个线程,让其通过 pthread_exit退出后返回一个值,再让线程等待获取这个值。

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;int g_val = 100;void *threadRoutine(void *args)
{const char *name = (const char*)args;int cnt = 5;while (true){printf("%s, pid: %d, g_val: %d, &g_val: %p\n", name, getpid(), g_val, &g_val);sleep(1);cnt--;if(cnt == 0) break;}pthread_exit((void *)200);
}int main()
{pthread_t pid;pthread_create(&pid, nullptr, threadRoutine, (void *)"Thread 1");void *ret;pthread_join(pid, &ret);cout << "main thread quit..., Thread 1 return val: " << (long long int)ret << endl;return 0;
}

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

2.4 线程取消

我们也可以通过 pthread_cancel接口取消一个已经存在的线程,其用法如下:

  1. 函数原型:int pthread_cancel(pthread_t thread);
  2. 参数:thread:要取消的线程 ID。
  3. 返回值:取消成功返回 0;取消失败,返回对应的错误码。

比如下面我们创建一个线程,让其通过 pthread_cancel取消,再让线程等待获取其返回值。

#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
int g_val = 100;
void *threadRoutine(void *args)
{const char *name = (const char*)args;int cnt = 5;while (true){printf("%s, pid: %d, g_val: %d, &g_val: %p\n", name, getpid(), g_val, &g_val);sleep(1);cnt--;if(cnt == 0) break;}pthread_exit((void *)200);
}
int main()
{pthread_t pid;pthread_create(&pid, nullptr, threadRoutine, (void *)"Thread 1");sleep(1);pthread_cancel(pid);void *ret;pthread_join(pid, &ret);cout << "main thread quit..., Thread 1 return val: " << (long long int)ret << endl;return 0;
}

如果一个线程被取消,它会返回一个名为 PTHREAD_CANCELED 的宏,其值为-1。

其实我们也能够通过新线程来取消我们的主线程,主线程会停止运行,但其他线程并不会收到任何影响。但这种做法并不符合我们的一般逻辑,所以并不推荐。

2.5 线程分离

在默认情况下,新创建的线程是 joinable 的,线程退出后,需要我们对其进行 pthread_join 操作,否则无法释放资源,从而造成资源泄露。但是如果主线程不关心子线程的返回值,join 其实也成是一种负担,这个时候,我们可以使用 pthread_detach接口,让当线程退出时,自动释放线程资源。其具体用法如下:

  1. 函数原型:int pthread_detach(pthread_t thread);
  2. 参数:thread:要分离的线程 ID。
  3. 返回值:分离成功返回 0;分离失败,返回对应的错误码。

比如下面我们创建五个新线程后让这五个新线程分离,此后主线程就不需要在对这五个新线程进行回收了。同时因为主线程并不需要等待其他线程,也能继续执行后续代码。

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
using namespace std;
void *Routine(void *arg)
{pthread_detach(pthread_self());char *msg = (char *)arg;int count = 0;while (count < 5){printf("I am %s...pid: %d,tid: %lu\n", msg, getpid(),pthread_self());sleep(1);count++;}pthread_exit((void *)10);
}
int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char buffer[64] = {'\0'};snprintf(buffer, sizeof(buffer),"thread %d", i);pthread_create(&tid[i], nullptr, Routine, buffer);sleep(1);}while (true){printf("I am main thread...pid: %d,tid: %lu\n", getpid(),pthread_self());sleep(1);}return 0;
}

2.6 线程的局部存储

我们知道普通的全局变量是被所有线程所共享的,如果想让该全局变量被每个线程各自私有一份,可以在定义全局变量的前面加上 __thread ,这并不是语言给我们提供的,而是编译器给我们提供。并且 __thread 只能用来修饰内置类型,不能用来修饰自定义类型。

比如我们创建五个线程,并用 __thread定义一个全局变量 val,在各个新线程中打印其值域地址。

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
using namespace std;
__thread int val = 100;
void *Routine(void *arg)
{pthread_detach(pthread_self());char *msg = (char *)arg;printf("I am %s...val:%d,&val:%p\n", msg, val, &val);sleep(1);while(true);
}
int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char buffer[64] = {'\0'};snprintf(buffer, sizeof(buffer), "thread %d", i);pthread_create(&tid[i], nullptr, Routine, buffer);sleep(1);}sleep(3);return 0;
}

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

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

相关文章

Vivado - JTAG to AXI Master (GPIO、IIC、HLS_IP)

目录 1. 简介 2. JTAG to AXI Master 2.1 添加 IP Core 2.2 基本TCL命令 2.2.1 复位 JTAG-to-AXI Master 2.2.2 创建并运行写入传输事务 2.2.3 创建并运行读取传输事务 2.2.4 命令列表 2.3 帮助信息 2.4 创建TCL读写程序 2.4.1 Read proc 2.4.2 Write proc 2.4.3 …

cuda程序编译流程

cuda程序编译流程 本文以cuda example的matrixMul矩阵乘法为例说明cuda程序的编译流程。 1. 源代码 .cu 文件 在matrixMul示例中&#xff0c;源代码文件 matrixMul.cu 是典型的CUDA程序&#xff0c;包含以下部分&#xff1a; 流程图 主机代码&#xff08;Host Code&#xf…

GNSS定位中自适应调整电离层延迟参数过程噪声的方法

文章目录 前言一、非差非组合PPP模型二、电离层功率谱密度计算三、具体实现方法3.1 不平滑3.2 三阶多项式平滑 参考文献 前言 GNSS定位中不少技术手段如PPP和长基线RTK需要将电离层延迟作为参数估计&#xff0c;电离层延迟的变化通常被描述为随机游走过程&#xff0c;而功率谱密…

1.2.1 计算机网络分层结构(上)

体系结构可分层使得不同的层次承担不同的功能。 知识点&#xff1a; 1.不同类型的节点&#xff0c;实现的功能层次可能不一样。 2.分层结构的设计并不唯一&#xff0c;可以根据实际需求增加或减少层次。 3.一个功能可以放在不同的层次反复出现。 根据分层结构不同可以分为&…

CORE MVC 过滤器 (筛选器)《2》 TypeFilter、ServiceFilter

TypeFilter、ServiceFilter ServiceFilter vs TypeFilter ServiceFilter和TypeFilter都实现了IFilterFactory ServiceFilter需要对自定义的Filter进行注册&#xff0c;TypeFilter不需要 ServiceFilter的Filter生命周期源自于您如何注册&#xff08;全局、区域&#xff09;&…

推荐4款2024年热门的PDF转ppt工具

有时候&#xff0c;我们为了方便&#xff0c;需要将PDF里面的内容直接转换的PPT的格式&#xff0c;既方便自己演示和讲解&#xff0c;也让我们可以更加灵活的进行文件的编辑和修改。如果大家不知道要如何进行操作的话&#xff0c;我可以为大家推荐几个比窘方便实用的PDF转换工具…

STM32LL库之printf函数重定向

1. 加入以下代码 int fputc(int ch,FILE *f) {LL_USART_TransmitData8(USART1,ch);while(!LL_USART_IsActiveFlag_TXE(USART1));//需要等待发送完成return(ch); }记得添加 stdio.h 头文件 2. 在MDK中勾选&#xff1a;Use MicroLIB

swiper+fixed的错误,splice函数的使用,提取年月日substring

做项目时的一些问题 swiperfixedsplice函数的使用重点在 alldata.splice(0, alldata.length, ...response.data.data);splicealldata.splice(0, alldata.length, ...response.data.data) 这行代码的功能为什么不直接赋值 提取年月日 substring swiperfixed 项目中的一个错误&a…

【人人都是P8程序员】Cursor 使用的十大技巧

Cursor 使用的十大技巧 总是在一个空的文件夹中创建一个新的项目 表述需求时尽量明确但谨慎 让Cursor从项目一开始就写README文档&#xff0c;让其记录清楚产品功能、实现技术栈等等&#xff0c;并在完成关键步骤后对README文档做及时的更新&#xff0c;第二天继续完成项目时…

npj Climate and Atmospheric Science I 新疆生地所陈亚宁研究员团队孙帆博士后发表最新研究进展

题目&#xff1a;The dominant warming season shifted from winter to spring in the arid region of Northwest China 主导中国西北干旱区升温的季节已从冬季转变为春季 期刊&#xff1a;npj Climate and Atmospheric Science IF及分区&#xff1a;实时IF/JCR分区/中科院分…

【Linux】Docker下载与使用-nginx

目录 一、Docker介绍 二、Docker结构 三、下载Daocker 1. 在linux上下载docker&#xff0c;执行以下命令即可&#xff1a; 2. 开启docker 3. 执行以下操作并进行使用 四、在Docker上安装nginx 一、Docker介绍 Docker&#xff1a;是给予Go语言实现的开源项…

召回12 曝光过滤 Bloom Filter

在推荐系统中&#xff0c;如果用户看过某个物品&#xff0c;就不再把物品推荐给这个用户。小红书、抖音都这样做曝光过滤&#xff0c;原因是实验表明重复曝光同一个物品会损害用户体验。但也不是所有推荐系统都有曝光过滤&#xff0c;像 YouTube 这样的长视频就没有曝光过滤&am…

ASR-01语音模块+C8T6实现语音控制LED

不说废话&#xff0c;简单直接&#xff0c;上教程&#xff0c;包会的&#xff0c;看不会&#xff0c;后台私我 一、接线图 STM32F103C8T6 ASR-01OLED屏PA10(RX接收串口)TX&#xff08;发送串口&#xff09;PB8SCL PB9 SDAVCCVCCGNDGND 二、天问软件Block图形编程 大家不要问…

Grafana链接iframe嵌入Web前端一直跳登录页面的问题记录

概述 公司有个项目使用到Grafana作为监控界面,因为项目方的环境极其复杂,仅物理隔离的环境就有三四个,而且每个都得部署项目,今天在某个环境测试,查看界面遇到一个比较奇怪的Grafana问题,后面针对该问题进行跟踪分析并解决,故而博文记录,用于备忘。 问题 登录项目We…

fastadmin 搜索提交重置按钮文本修改

默认 修改require-backend.min.js文件 效果 当然最好还是去需修改lang文件 效果 如果修改没生效记得清楚一下缓存&#xff0c;再刷新 完结 赠人玫瑰&#xff0c;手有余香&#xff01;如果文章内容对你有所帮助&#xff0c;请不要吝啬你的点赞评论和关注&#xff0c;你…

Tair简介

概述 Tair是淘宝团队开源的高可用分布式KV存储引擎&#xff0c;采用服务端自动负载均衡方式&#xff0c;使客户端逻辑简单。Tair&#xff0c;即TaoBao Pair缩写&#xff0c;Pair表示一对、一双等意思&#xff0c;即Key-Value数据对。 Tair分为持久化和非持久化两种方式。非持…

【Linux学习】【Ubuntu入门】2-1 Linux系统下运行C语言输出hello word

1.双击打开VMware软件&#xff0c;点击开启此虚拟机后&#xff0c;等待点击头像输入密码进入 2.“CtrlAltt”调出命令行终端&#xff0c;输入命令sudo apt-get install vim安装vim&#xff0c;输入命令sudo apt-get install gcc安装gcc 3.输入命令vi hello.c进入C语言编写环境&…

【D3.js in Action 3 精译_025】3.4 让 D3 数据适应屏幕(中)—— 线性比例尺的用法

当前内容所在位置&#xff08;可进入专栏查看其他译好的章节内容&#xff09; 第一部分 D3.js 基础知识 第一章 D3.js 简介&#xff08;已完结&#xff09; 1.1 何为 D3.js&#xff1f;1.2 D3 生态系统——入门须知1.3 数据可视化最佳实践&#xff08;上&#xff09;1.3 数据可…

[Python数据分析]最通俗入门Kmeans聚类分析,可视化展示附代码。

什么是k-means分析?【头条@William数据分析,看原版】 想象一下,你有一堆五颜六色的糖果,你想把它们按照颜色分成几堆。k-means分析就是这么一个自动分类的过程。它会根据糖果的颜色特征,把它们分成若干个组,每个组里的糖果颜色都比较相似。 更专业一点说,k-means分析是…

【C++并发入门】摄像头帧率计算和多线程相机读取(上):并发基础概念和代码实现

前言 高帧率摄像头往往应用在很多opencv项目中&#xff0c;今天就来通过简单计算摄像头帧率&#xff0c;抛出一个单线程读取摄像头会遇到的问题&#xff0c;同时提出一种解决方案&#xff0c;使用多线程对摄像头进行读取。同时本文介绍了线程入门的基础知识&#xff0c;讲解了…