Linux——多线程(一)

一、线程的概念

1.1线程概念

教材中的概念:                                                                   (有问题?)        

线程是进程内部的一个执行分支,线程是CPU调度的基本单位

之前我们讲的进程:

加载到内存中的程序,叫做进程---(修正)---->进程=内核数据结构+进程代码和数据

 1.2线程的理解(以Linux系统为例)

正文:代码段(区),我们的代码在进程中,全部都是串行(单进程)调用的 

进程新建,(时空)成本较高(一堆数据结构,映射...)

地址空间和地址空间上的虚拟地址,本质其实是一种"资源"(进程的大部分资源需要通过地址空间来访问)

多执行流(并行)  :正文代码分好,已、未初始化区公开,创建执行流就多加一个task_struct,指向同一块地址空间

我们把在进程地址空间中这样创建的"进程",称为线程

 

理解概念:线程是进程内部(进程地址空间)的一个执行分支,线程是CPU调度的基本单位,CPU调度只关注进程PCB,也就是task_struct

Linux为什么要这么设计"线程"?

如果我们要设计线程,OS也要对线程做管理。

怎么管理呢?依旧是那六个字,先描述,再组织

如果像上面这样设计的话,OS会变得非常的复杂 

Linux的设计者认为:进程和线程都是执行流,具有极度的相似性,没必要单独设计数据结构和代码,直接复用就行 

1.2.1线程与进程  

进程 vs 线程

什么是进程?

中间那个圈里面的叫进程 

task_struct 是进程中的执行流,这里我们暂时叫它线程

以前我们说的进程:一个内部只有一个线程的进程;

今天我们讲的进程:一个内部至少有一个线程的进程

CPU不用区分task_struct(进、线程都是执行流)

在内核的角度: 进程是承担分配系统资源的基本实体

关于调度的问题:线程<=执行流(轻量级进程)<=进程

Linux所有的调度执行流都叫做:轻量级进程

Linux内核中并没有线程的概念,线程是用进程PCB来模拟的

那么多个执行流如何进行代码划分?如何理解?

OS要不要管理内存呢?自然是要的

大部分OS的内存都是以4KB为大小的内存块组成

 在OS的术语里:把4KB的空间or内容叫 页框/页帧

在物理内存中,每一块被分割成4KB大小的数据块,被叫做页框

以上还不是管理,OS要先描述,再组织----------->定义一个结构体

先描述:

#define Kerucl 0x1
#define User 0x2
#define USR 0x4
#define NOUSR 0x8

struct Page
{
    int flag;
    //其他属性...
}

再组织

struct Page mem[1048576];

对内存的管理,变成对此数组的增删查改(操作系统对内存管理的基本单位是4KB)

可执行文件:写好的代码会被编译器处理形成二进制可执行文件放在磁盘中,在运行的时候加载到内存中

编译器在处理源文件生成二进制可执行文件,同样是以4KB为单位的,这4KB的数据块被叫做页帧

1.2.2页表(页目录+页表项)

再来讲讲页表

每个进程的虚拟地址空间都是4GB,也就是2^32个地址,如果每个虚拟地址都在页表中存一个物理地址,那样占的空间也太大了,这还没算映射在物理内存的地址,再加上其它属性和另外一些杂七杂八的,假设每一行就占10B的空间,那么光是页表就占10*2^32 = 40G 的空间,怎么可能一个进程的页表比物理内存都大。

真正的页表是什么呢?

在32位机器上,地址的大小是4G字节,也就是有32个比特位

将32个比特位分为10个比特位,10个比特位,12个比特位,共3组:

32个比特位的最高10位,代表的是页目录的下标;[0,1023]的下标代表了页目录总共有2^10 = 1024个

页目录中存放的是页表项的地址,可以通过下标找到对应的页表项。

32个比特位的中间10位代表的是页表项的下标,最多能够有1024个页表项

页表项里面存的是物理内存中每个4KB内存块(页框)的起始地址

 这最后的12位是什么呢?

0x1234+虚拟地址的后12位为对应的数据(页内偏移刚好4KB)

2^12 = 4KB = 1024 * 2 * 2  

1.3Linux线程的系统调用

在上面中我们讲过,在Linux内核中是不存在线程这一个概念的,没有TCB数据结构以及管理算法,而我们所说的线程,在Linux系统都是轻量级进程

Linux操作系统中也没有提供创建线程的系统调用。

只提供了轻量级进程的系统调用 

无论是宏观操作系统,还是用户(程序员)都只认线程的概念,但Linux内核中没有线程的概念。

那么Linux是怎么做到当用户在创建线程时,就在内核中创建轻量级进程的呢?

用户创建线程时会调用一个线程库,这个线程库里封装了Linux的轻量级进程的系统调用接口,这样一来就能在Linux中创建轻量级进程了。 而且这个线程库时所有Linux操作系统都一定自带的一个库,所以也叫原生线程库

1.4pthread_create(线程创建)

给不同的线程分配不同的区域,本质就是让不同的线程,各自看到全部页表的子集

man pthread_create

参数:

pthread_t *thread:                                线程标识符线程tid

const pthread_attr_t *attr:                    线程属性,这个阶段我们一般设为nullptr

void *(*start_routine) (void *):              函数指针,创建出的新线程执行的就是此函数的代码

void *arg :                                            传给线程函数的参数,是上个参数的指向函数的形参

返回值:创建成功返回0,创建失败返回错误码

用pthread_create创建一个新线程 

#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;

void* handler(void *args)
{
    while(true)
    {
        cout<<"i am newthread"<<endl;
        sleep(1);
    }
    
} 

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,handler,nullptr);
    while(true)
    {
        cout<<"i am main thread,running..."<<endl;
        sleep(1);
    }

    return 0;
}

 makefile

 

mytest:thread.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f mytest

此时编译报错:链接错误,编译器不认识pthread_create函数。我们创建新线程需要通过原生线程库去创建,此时编译器找不到原生线程库。我们需要使用-l去选项指定链接原生线程库pthread

这时候就编译成功了

 

  用命令查了一下只查到了一个进程

用另外的命令查到两个执行流(-L查看轻量级进程)

PID: 这个我们之前讲过进程pid(进程标识符)

LWP:light weight process 轻量级进程

通过上图我们发现,两个执行流他们的PID是相同的,而LWP是不同的

这也证明线程是进程的一部分(其中的一个执行流)

可以看到第一个线程的LWP和PID是一样的,这个线程被叫做主线程,其它不一样的叫做新线程

 

新创建出来的线程称为新线程

main所对应的执行流会自动向下运行,称为主线程 

那么OS在进行调度时,用哪个id进行调度呢?LWP

那以前我们讲的单进程、多进程的调度呢?

其实就是每个进程内部只有一个执行流,LWP==PID

二、线程的共有资源、私有资源

再了解一个函数

man pthread_self

无参数,获取线程id(tid) 

#include<iostream>
#include<unistd.h>
#include<string>
#include<pthread.h>
using namespace std;
std::string ToHex(pthread_t tid)
{
    char id[64];
    snprintf(id,sizeof(id),"0x%lx",tid);
    return id;
}
int g_val = 2;
void* threadrun(void *args)
{
    int cnt = 5;
    std::string threadname = (char*)args;
    while(cnt)
    {
        cout<<threadname<<"is running:"<<cnt<<",pid:"<<getpid()<<",mythread id:"<<ToHex(pthread_self())<<",g_val:"<<g_val<<",&g_val"<<&g_val<<endl;
        g_val++;
        sleep(1);
        cnt--;
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,threadrun,(void*)"thread -1");
    int cnt = 10;
    while(cnt)
    {
        cout<<"main thread is running...,cnt:"<<cnt<<",pid:"<<getpid()<<",newthread tid"<<ToHex(tid)<<",main thread id:"<<ToHex(pthread_self())
        <<",g_val:"<<g_val<<",&g_val"<<&g_val<<endl;
        sleep(1);
        cnt--;
    }
    return 0;
}

 注意不同线程中g_val的值以及地址:新线程、主线程看到的全局变量是同一个;当任何一个线程修改这个全局变量的值时,都会影响另外一个线程使用这个值

在新线程、主线程中,这个全局变量的地址相同,说明他们使用的是同一个全局变量

上面代码说明进程的数据段资源也被线程共享

进程中的绝大部分资源都是被所有线程共享的(代码和全局数据、进程文件描述符表)

那么线程自己有没有私有的资源呢?

PCB属性私有:所有线程都有各自的PCB,PCB中的属性肯定是私有的,属于各自线程

线程的硬件上下文数据(CPU寄存器的值)私有:CPU在调度PCB的时候,采用轮转时间片的方式,当一个线程被换下时,该线程的上下文一定是私有的,防止被其他线程修改而导致恢复上下文的时候出现错误。

线程的独立栈结构

不同线程各自的临时变量一定是私有的,而临时变量存放在栈结构中,所有栈也是私有的

共享的都是同一块虚拟地址空间,为什么非是栈结构私有呢?还有一个原因是原生线程库的实现。

man clone 

系统调用clone是用来创建子进程(轻量级线程)的,也就是没有独立的虚拟地址空间,clone中有一个参数:void *stack,这个参数就是用来开这个子进程的栈空间的

所以 我们在调用pthread_create创建新线程的时候底层会调用clone去指定该线程的私有栈结构

三、线程特点

3.1线程优点

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

线程切换需要保存的上下文数据,只比进程少了一点,为什么说线程之间的切换需要操作系统做的工作要少很多? 

cache:集成在CPU内的离CPU寄存器远而容量比寄存器大得多的缓存

 一行代码执行完,大概率会执行下一行,可能将相关代码搬到cache中缓存起来,后面读代码直接从cache中读取;如果是两个进程之间的切换的话,那么cache缓存的内容会失效,切换B进程之后要清空cache,然后把B进程的代码和数据放里面,线程的切换中cache不用清空

3.2线程缺点 

线程的健壮性较差:

编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的

缺乏访问控制:
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高:
编写与调试一个多线程程序比单线程程序困难得多

性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

 主线程退出 == 进程退出 == 所有线程都要退出

所以

  1. 往往我们需要main thread最后结束
  2. 线程也要被"wait",要不然会产生类似进程那里的内存泄露的问题

四、线程控制

4.1pthread_create(线程创建)

在上面的1.4中我们已经提到了pthread_create的使用方法,这里做几点小补充

线程退出无非3中结果:

  1. 代码跑完,结果对
  2. 代码跑完,结果不对
  3. 出异常——重点

 

#include<iostream>
#include<unistd.h>
#include<string>
#include<pthread.h>
using namespace std;
int g_val = 2;
void *threadrun(void *args)
{
    int cnt = 5;
    while(cnt)
    {
        printf("new thread,g_val:%d,&g_val:%p\n",g_val,&g_val);
        g_val++;
        sleep(1);
        int *p = nullptr;
        *p = 100;//故意野指针
        cnt--;
    }
    return (void*)123;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,threadrun,(void*)"thread -1");
    void *ret = nullptr;
    while(true)
    {
        printf("main thread is running,g_val:%d,&g_val:%p\n",g_val,&g_val);
        sleep(1);
    }
    int n = pthread_join(tid,&ret);
    cout << "main thread quit, n=" << n << " main thread get a ret: " << (long long)ret << std::endl;
}

 

我们怎么没有像进程一样获取线程的退出信号呢?只有自己手动写的退出码。

因为一旦有一个线程出异常,整个进程就都寄了,不考虑线程异常的情况 

4.2pthread_join(线程等待)

如何进行线程等待呢?man pthread_join

函数参数:

pthread_t thread : 线程标识符(tid)

void **retval:输出型参数,主线程等待新线程的返回值,&(void)------>void**

 返回值:等待成功返回0,等待失败返回错误码

#include<iostream>
#include<unistd.h>
#include<string>
#include<pthread.h>
using namespace std;

void* threadrun(void *args)
{
    int cnt = 5;
    std::string threadname = (char*)args;
    while(cnt)
    {
        cout<<threadname<<" is running..."<<endl;
        sleep(1);
        cnt--;
    }
    return (void*)123;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,threadrun,(void*)"thread -1");
    void *ret = nullptr;
    int cnt = 10;
    while(cnt)
    {
        cout<<"main thread is running..."<<endl;
        sleep(1);
        cnt--;
    }
    int n = pthread_join(tid,&ret);
    cout << "main thread quit, n=" << n << std::endl;
    return 0;
}

 

 

 4.3pthread_exit(线程结束)

线程结束

1.线程函数结束

2.pthread_exit()

不能直接exit线程,因为它是终止进程的 

 

 参数:void* retval:返回线程结束信息,当前阶段设置成nullptr即可。

void* threadrun(void *args)
{
    int cnt = 5;
    std::string threadname = (char*)args;
    while(cnt)
    {
        cout<<threadname<<" is running..."<<endl;
        sleep(1);
        cnt--;
        if(cnt == 2)
        {
            pthread_exit(nullptr);
        }
    }
    return (void*)123;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,threadrun,(void*)"thread -1");
    void *ret = nullptr;
    int cnt = 10;
    while(cnt)
    {
        cout<<"main thread is running..."<<endl;
        sleep(1);
        cnt--;
    }
    int n = pthread_join(tid,&ret);
    cout << "main thread quit, n=" << n << std::endl;
    return 0;
}

新线程退出,只剩下主线程

3.int pthread_cancel(tid);//线程取消

 

参数:要取消线程的tid

返回值:取消成功返回0,取消失败返回错误码

 

void* threadrun(void *args)
{
    int cnt = 5;
    std::string threadname = (char*)args;
    while(cnt)
    {
        cout<<threadname<<" is running..."<<endl;
        sleep(1);
        cnt--;
    }
    return (void*)123;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,threadrun,(void*)"thread -1");
    void *ret = nullptr;
    int cnt = 10;
    while(cnt)
    {
        cout<<"main thread is running..."<<endl;
        sleep(1);
        cnt--;
        if(cnt == 7)
        {
            break;
        }
    }
    pthread_cancel(tid);
    cout<<"pthread_cancel:"<<tid<<endl;
    int n = pthread_join(tid,&ret);
    cout << "main thread quit, n=" << n << " main thread get a ret: " << (long long)ret << std::endl;
    return 0;
}

 

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

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

相关文章

云易办springboot+vue后端

springbootvue云易办后端项目完成 一.创建项目 创建父项目&#xff1a;yeb&#xff0c; 使用spring Initializr&#xff0c;完成创建之后删除无用文件夹&#xff0c;作为父项目 添加packaging <packaging>pom</packaging>二.创建子模块&#xff1a;yeb-server …

PyCharm基本配置内容

如何更换 Python 解释器 输入一段代码点击运行后&#xff0c;画面下方有一个路径如图中框中所示&#xff1a; 上面的路径为虚拟路径&#xff0c;可以改为我们自己设置的路径 点击设置&#xff0c;选择settings 选择Project&#xff1a;y002———》Python Interpreter&#…

Clickhouse 嵌套数据类型总结—— Clickhouse 基础篇(三)

文章目录 创建嵌套类型的表插入读取数据在嵌套类型上使用数组函数 在 clickhouse 中存储嵌套类型的关键字是 Nested, 只支持一级嵌套。数据结构类似于在数据结构类似于在表的单元格里面嵌套“一张表格”&#xff0c;如下图所示&#xff1a; 嵌套类型是列存储&#xff0c;本质…

OWASP十大API漏洞解析:如何抵御Bot攻击?

新型数字经济中&#xff0c;API是物联网设备、Web和移动应用以及业务合作伙伴流程的入口点。然而&#xff0c;API也是犯罪分子的前门&#xff0c;许多人依靠Bot来发动攻击。对于安全团队来说&#xff0c;保护API并缓解Bot攻击至关重要。那么Bot在API攻击中处于怎样的地位&#…

【JVM】一次JVM内存泄露分析处理

一次内存泄露分析 背景情况 编写了一个大数据基础组件的可用性监控程序&#xff0c;采用Bootstrap监测端口的方式&#xff0c;使得方法常驻&#xff08;main线程常驻&#xff09;&#xff0c;通过一个调度线程ScheduledThreadPoolExecutor&#xff0c;定时的调动监测任务。 …

OTFS系统建模、通信性能分析、信道估计、模糊函数【附MATLAB代码】

文献来源&#xff1a;​微信公众号&#xff1a;EW Frontier OTFS简介 OTFS信道估计 % Clear command window, workspace variables, and close all figures clc; clear all; close all; ​ % Define Eb values in dB EbdB -10:2:10; ​ % Convert Eb values from dB to lin…

微软提出“Copilot+ PCs”构想,强调本地AI处理;OpenAI暂停ChatGPT语音功能因声音相似争议

&#x1f989; AI新闻 &#x1f680; 微软提出“Copilot PCs”构想&#xff0c;强调本地AI处理 摘要&#xff1a;在微软 Build 开发者前瞻大会上&#xff0c;CEO 萨蒂亚・纳德拉介绍了“Copilot PCs”&#xff0c;一种新类 Windows PC&#xff0c;需配备神经处理单元&#xf…

视频技术在智慧营业厅中的应用:AI识别与智能化转型

一、方案背景 随着信息技术的快速发展&#xff0c;图像和视频分析技术已广泛应用于各行各业&#xff0c;特别是在营业厅场景中&#xff0c;该技术能够有效提升服务质量、优化客户体验&#xff0c;并提高安全保障水平。TSINGSEE青犀智慧营业厅视频管理方案旨在探讨视频监控和视…

你真的懂firewalld吗?不妨看看我的这篇文章

一、firewalld简介 firewalld防火墙是Linux系统上的一种动态防火墙管理工具&#xff0c;它是Red Hat公司开发的&#xff0c;并在许多Linux发行版中被采用。相对于传统的静态防火墙规则&#xff0c;firewalld使用动态的方式来管理防火墙规则&#xff0c;可以更加灵活地适应不同…

ld链接文件

文章目录 1. sections缩写2. 链接脚本2.1 MEMORY&#xff08;内存命令&#xff09;2.1.1 作用2.1.2 格式 2.2 SECTIONS&#xff08;段命令&#xff09;2.2.1 作用2.2.2 格式 2.3 特殊符号含义2.4 通配符2.5 Eg 1. sections缩写 2. 链接脚本 https://www.cnblogs.com/jianhua19…

mysql 01 linux 上安装mysql服务端

01.linux安装 MySQL的大部分安装包都包含了服务器程序和客户端程序&#xff0c;不过在Linux下使用RPM包时会有单独的服 务器RPM包和客户端RPM包&#xff0c;需要分别安装。 1.查看是否已经安装了MySQL rpm -qa | grep mysql如果什么都没有&#xff0c;就是还没有装过MySQL …

【设计模式】JAVA Design Patterns——Circuit Breaker(断路器模式)

&#x1f50d;目的 以这样一种方式处理昂贵的远程服务调用&#xff0c;即单个服务/组件的故障不会导致整个应用程序宕机&#xff0c;我们可以尽快重新连接到服务 &#x1f50d;解释 真实世界例子 想象一个 Web 应用程序&#xff0c;它同时具有用于获取数据的本地文件/图像和远程…

(1) 初识QT5

文章目录 Qt Quickdemo信号的命名方式 qml语言一个很重要的概念 qt 模块 Qt Quick Qt Quick是Qt5中⽤户界⾯技术的涵盖。Qt Quick⾃⾝包含了以下⼏种技术&#xff1a; QML-使⽤于⽤户界⾯的标识语⾔JavaScript-动态脚本语⾔Qt C具有⾼度可移植性的C库. 类似HTML语⾔&#xf…

生成式AI的GPU网络技术架构

生成式AI的GPU网络 引言&#xff1a;超大规模企业竞相部署拥有64K GPU的大型集群&#xff0c;以支撑各种生成式AI训练需求。尽管庞大Transformer模型与数据集需数千GPU&#xff0c;但实现GPU间任意非阻塞连接或显冗余。如何高效利用资源&#xff0c;成为业界关注焦点。 张量并…

泰达克TADHE uv胶水在粘接聚酰亚胺(Polyimide,PI)时具有一些优势,并在各行业中得到了广泛应用,尤其是在特定应用中

泰达克TADHE uv胶水在粘接聚酰亚胺&#xff08;Polyimide&#xff0c;PI&#xff09;时具有一些优势&#xff0c;并在各行业中得到了广泛应用&#xff0c;尤其是在特定应用中。以下是一些使用UV胶水粘接PI的优势&#xff1a; 1.快速固化&#xff1a; UV胶水通过紫外线照射进行固…

Java进阶学习笔记23——API概述

API&#xff1a; API&#xff08;Application Programming Interface&#xff09;应用程序编程接口 就是Java帮我们写好了一些程序&#xff1a;如类、方法等等&#xff0c;我们直接拿过来用就可以解决一些问题。 为什么要学别人写好的程序&#xff1f; 不要重复造轮子。开发…

回文链表(快慢指针解法之在推进过程中反转)

归纳编程学习的感悟&#xff0c; 记录奋斗路上的点滴&#xff0c; 希望能帮到一样刻苦的你&#xff01; 如有不足欢迎指正&#xff01; 共同学习交流&#xff01; &#x1f30e;欢迎各位→点赞 &#x1f44d; 收藏⭐ 留言​&#x1f4dd;抱怨深处黑暗&#xff0c;不如提灯前行…

系统开发与运行知识

系统开发与运行知识 导航 文章目录 系统开发与运行知识导航一、软件工程二、软件生命周期三、开发模型四、开发方法五、需求分析结构化分析 六、数据流图分层数据流图的画法设计注意事项 七、数据字典数据字典的内容 八、系统设计九、结构化设计常用工具十、面向对象十一、UML…

【Windows】本地磁盘挂载 Minio 桶

目录 1.软件安装安装winfsp支持安装rclone 2.新建rclone远程存储类型S3服务类型验证方式地区终端地址ACL服务端加密KMS 3.挂载存储盘 1.软件安装 安装winfsp支持 下载地址 或 下载地址2 文件为msi文件&#xff0c;下载后双击直接安装即可&#xff0c;可以选择安装路径 安装r…

接口响应断言-json

json认识JSONPath源码类学习/json串的解析拓展学习 目的&#xff1a;数据返回值校验测试 json认识 json是什么-是一种数据交换格式&#xff0c;举例平时看到的json图2&#xff0c;在使用中查看不方便&#xff0c;会有格式转化的平台&#xff0c;json格式的展示 JSON在线视图…