[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;
}

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

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

相关文章

简单了解Redis(初识阶段)

1.认识Redis 对于Redis有一个很重要的点就是&#xff0c;它存储数据是在内存中存储的。 但是对于单机程序&#xff0c;直接通过变量存储数据的方式是更优的&#xff0c;在分布式系统下 Redis才能发挥威力 因为进程是有隔离性的&#xff0c;Redis可以基于网络&#xff0c;把进…

CentOS 7 YUM源不可用

CentOS 7 操作系统在2024年6月30日后将停止官方维护&#xff0c;并且官方提供的YUM源将不再可用。 修改&#xff1a;nano /etc/yum.repos.d/CentOS-Base.repo # CentOS-Base.repo [base] nameCentOS-$releasever - Base baseurlhttp://mirrors.aliyun.com/centos/$rel…

前端——flex布局

flex布局——弹性布局 传统布局: 浮动 定位 行内块等 1. flex布局 方法简单 不需要计算 能自动分配父级里面的子元素排版 对齐方式等等 >flex布局 可以适应不同屏幕布局 2. flex布局使用 - 给父级盒子 display: flex 开启弹性盒模型 - 子元素就会默…

html中为div添加展开与收起功能2(div隐藏与显示)

效果图&#xff1a; 1、单个隐藏div项 html布局 <div class"question-detail active"><div class"item-handle"><span class"btn-detail">作答详情 <i class"layui-icon layui-icon-down layui-font-12"><…

数据分析师之Excel学习

前言 excel作为职场人来说&#xff0c;已经是人人必备的技能了&#xff0c;所以还不知道这个的小伙伴&#xff0c;一定要抓紧时间学习&#xff0c;紧跟时代的步伐。 Excel 几个重要的版本 97-2003版本是国内最早流行的版本 .xlsx后缀的表格文件&#xff0c;基本是07版本及…

【数据结构】Java的HashMap 和 HashSet 大全笔记,写算法用到的时候翻一下,百度都省了!(实践篇)

本篇会加入个人的所谓鱼式疯言 ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人…

1.随机事件与概率

第一章 随机时间与概率 1. 随机事件及其运算 1.1 随机现象 ​ 确定性现象&#xff1a;只有一个结果的现象 ​ 确定性现象&#xff1a;结果不止一个&#xff0c;且哪一个结果出现&#xff0c;人们事先并不知道 1.2 样本空间 ​ 样本空间&#xff1a;随机现象的一切可能基本…

什么是智慧党建?可视化大屏如何推动高质量党建?

在数字化时代&#xff0c;党建工作迎来了新的发展机遇。智慧党建&#xff0c;作为新时代党建工作的创新模式&#xff0c;正逐渐成为推动党的建设向高质量发展的重要力量。它不仅改变了传统的党建工作方式&#xff0c;还通过现代信息技术的应用&#xff0c;提升了党建工作的效率…

【CSS】鼠标 、轮廓线 、 滤镜 、 堆叠层级

cursor 鼠标outline 轮廓线filter 滤镜z-index 堆叠层级 cursor 鼠标 值说明值说明crosshair十字准线s-resize向下改变大小pointer \ hand手形e-resize向右改变大小wait表或沙漏w-resize向左改变大小help问号或气球ne-resize向上右改变大小no-drop无法释放nw-resize向上左改变…

AI绘画Stable Diffusion 自制素材工具: layerdiffusion插件—你的透明背景图片生成工具

大家好&#xff0c;我是灵魂画师向阳 今天给大家分享一款AI绘画的神级插件—LayerDiffusion。 Layerdiffusion是一个用于stable-diffusion-webui 的透明背景生成&#xff08;不是生成图再工具扣图&#xff0c;是直接生成透明背景透明图像&#xff09;插件扩展&#xff0c;它可…

Java笔试面试题AI答之设计模式(2)

文章目录 6. 什么是单例模式&#xff0c;以及他解决的问题&#xff0c;应用的环境 &#xff1f;解决的问题应用的环境实现方式 7. 什么是工厂模式&#xff0c;以及他解决的问题&#xff0c;应用的环境 &#xff1f;工厂模式简述工厂模式解决的问题工厂模式的应用环境工厂模式的…

音乐服务器测试报告

项目背景 该音乐服务器系统使用的是前后端分离的方式来实现,将相关数据存储到数据库中, 且将其部署到云服务器上. 前端主要构成部分有: 登录页面, 列表页面, 喜欢页面, 添加歌曲4个页面组成. 通过结合后端实现了主要的功能: 登录, 播放音乐, 添加音乐, 收藏音乐, 删除音乐, 删…

vscode 配置django

创建运行环境 使用pip安装Django&#xff1a;pip install django。 创建一个新的Django项目&#xff1a;django-admin startproject myproject。 打开VSCode&#xff0c;并在项目文件夹中打开终端。 在VSCode中安装Python扩展&#xff08;如果尚未安装&#xff09;。 在项…

MySQL InnoDB MVCC读写逻辑分析与调测

目标 1、构建MVCC读写场景 2、gdb调试MVCC过程&#xff0c;输出流程图&#xff08;函数级别调用过程&#xff09; 前提 准备1 打开服务端 查询mysqld进程号 线程树 打开客户端&#xff0c;想创建几个事务号就打开几个客户端 准备2 数据库mvcc&#xff0c;两个表test和stu…

Spring Boot框架在甘肃非遗文化网站设计中的运用

3 系统分析 当用户确定开发一款程序时&#xff0c;是需要遵循下面的顺序进行工作&#xff0c;概括为&#xff1a;系统分析–>系统设计–>系统开发–>系统测试&#xff0c;无论这个过程是否有变更或者迭代&#xff0c;都是按照这样的顺序开展工作的。系统分析就是分析系…

数据库——sql语言学习 查找语句

一、什么是sql SQL是结构化查询语言&#xff08;Structured Query Language&#xff09;的缩写&#xff0c;它是一种专门为数据库设计的操作命令集&#xff0c;用于管理关系数据库管理系统&#xff08;RDBMS&#xff09;。 二、查找相关语句 ‌‌首先&#xff0c;我们已经设…

【洛谷】P10417 [蓝桥杯 2023 国 A] 第 K 小的和 的题解

【洛谷】P10417 [蓝桥杯 2023 国 A] 第 K 小的和 的题解 题目传送门 题解 CSP-S1 补全程序&#xff0c;致敬全 A 的答案&#xff0c;和神奇的预言家。 写一下这篇的题解说不定能加 CSP 2024 的 RP 首先看到 k k k 这么大的一个常数&#xff0c;就想到了二分。然后写一个判…

Unity 设计模式 之 创建型模式 -【单例模式】【原型模式】 【建造者模式】

Unity 设计模式 之 创建型模式 -【单例模式】【原型模式】 【建造者模式】 目录 Unity 设计模式 之 创建型模式 -【单例模式】【原型模式】 【建造者模式】 一、简单介绍 二、单例模式 (Singleton Pattern) 1、什么时候使用单例模式 2、单例模式的好处 3、使用单例模式的…

sheng的学习笔记-logback

基础知识 什么是logback Logback是一个用于Java应用程序的日志框架&#xff0c;提供了更好的性能、可扩展性和灵活性。 与Log4j相比&#xff0c;Logback提供了更快的速度和更低的内存占用&#xff0c;这使得它成为大型企业级应用程序的理想选择。 ‌logback和slf4j的关系是…

Hadoop安装与配置

一、Hadoop安装与配置 1、解压Hadoop安装包 找到hadoop-2.6.0.tar.gz,将其复到master0节点的”/home/csu”目录内&#xff0c;解压hadoop [csumaster0 ~]$ tar -zxvf ~/hadoop-2.6.0.tar.gz 解压成成功后自动在csu目录下创建hadoop-2.6.0子目录&#xff0c;可以用cd hadoo…