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 0x8struct 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.ccg++ -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;
}

 

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

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

相关文章

栈的特性及代码实现(C语言)

目录 栈的定义 栈的结构选取 链式储存结构和顺序栈储存结构的差异 栈的代码实现 "stack.h" "stack.c" 总结 栈的定义 栈&#xff1a;栈是限定仅在表尾进行插入和删除操作的线性表。 我们把运行插入的和删除的一段叫做栈顶&#xff08;TOP&#xff…

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

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

adb获取包名和界面名

adb获取包名和界面名 mac adb shell dumpsys window windows | grep mFocusedApp windows adb shell dumpsys window windows | findstr mFocusedApp 这个是在当前手机打开哪个界面获取的就是哪个界面的包名与界面 注意第一次连接时会有提示&#xff0c;需要连接两次才可以 …

【AI算法岗面试八股面经【超全整理】——机器学习】

AI算法岗面试八股面经【超全整理】 概率论信息论机器学习深度学习CVNLP 目录 1、回归损失函数2、分类损失函数3、误差&#xff08;Error&#xff09;、偏差&#xff08;Bias&#xff09;、方差&#xff08;Variance&#xff09;4、PCA&#xff08;Principle Component Analysi…

四川汇聚荣聚荣科技有限公司好不好?

在当今科技飞速发展的时代&#xff0c;企业要想在激烈的市场竞争中脱颖而出&#xff0c;必须具备强大的技术实力和良好的市场口碑。那么&#xff0c;作为一家专注于科技创新的公司&#xff0c;四川汇聚荣聚荣科技有限公司究竟如何呢?接下来&#xff0c;我们将从四个方面进行详…

扔掉 MacBook,挑战带OrangePi出差!

背景 由于工作需要&#xff0c;博主经常会到各大企业的自建机房中私有化部署公司的软件产品。 在某些企业自建机房中&#xff0c;有时给到全新的机器&#xff0c;没有基础环境&#xff0c;甚至有的还无法互联网&#xff0c;而且因为近几年CentOS的停止更新&#xff0c;服务器…

【Linux】Linux的权限_2 + Linux环境基础开发工具_1

文章目录 三、权限3. Linux权限管理修改文件的拥有者和所属组 4. 文件的类型5. 权限掩码 四、Linux环境基础开发工具1. yumyum 工具的使用 未完待续 三、权限 3. Linux权限管理 修改文件的拥有者和所属组 在上一节我们讲到如何更改文件的访问权限&#xff0c;那我们需要更改…

光伏智慧化运营解决方案的应用和价值

在社会对新能源需求的不断扩大&#xff0c;光伏已经成为了可再生能源的重要组成部分&#xff0c;随着光伏电站数量和规模的不断扩大&#xff0c;相关企业和用户都就开始关注如何能够高效精准的进行电站管理&#xff0c;对此&#xff0c;鹧鸪云提出了光伏智慧化运营解决方案&…

【官方指南】3ds Max中纹理贴图问题及正确解决方案

在使用3ds Max进行设计和制作时&#xff0c;纹理贴图是一个非常重要的环节。然而&#xff0c;许多用户在使用过程中常会遇到各种纹理贴图问题。为此&#xff0c;Autodesk官方提供了一些有效的解决方案&#xff0c;可以解决90%的纹理贴图难题。这里小编都帮大家整理好了&#xf…

简化跨网文件传输摆渡过程,降低IT人员工作量

在当今数字化时代&#xff0c;IT企业面临着日益增长的数据交换需求。随着网络安全威胁的不断演变&#xff0c;网关隔离成为了保护企业内部网络不受外部威胁的重要手段。然而&#xff0c;隔离的同时&#xff0c;企业也需要在不同网络间安全、高效地传输文件&#xff0c;这就催生…

线性回归计算举例

使用正规方程计算&#xff08;一元线性回归&#xff09; import numpy as np import matplotlib.pyplot as plt # 转化成矩阵 X np.linspace(0, 10, num 30).reshape(-1, 1) # 斜率和截距&#xff0c;随机生成 w np.random.randint(1, 5, size 1) b np.random.randint(1,…

Qt项目使用pato mqtt C

一,下载pato mqtt C 源码 git 地址:https://github.com/eclipse/paho.mqtt.c.git git 地址可能下载不下来,提供我的gitee地址 gitee地址:https://gitee.com/chaojidahuaidan2021/paho.mqtt.c.git 二,编译共享库 clone下来后,将项目导入到Qt工程中,此时这是一个cmke工程…

三十一、openlayers官网示例Draw Features解析——在地图上自定义绘制点、线、多边形、圆形并获取图形数据

官网demo地址&#xff1a; Draw Features 先初始化地图&#xff0c;准备一个空的矢量图层&#xff0c;用于显示绘制的图形。 initLayers() {const raster new TileLayer({source: new XYZ({url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/…

Kubernetes和Docker对不同OS和CPU架构的适配关系

Docker Docker官网对操作系统和CPU架构的适配关系图 对于其他发行版本&#xff0c;Docker官方表示没有测试或验证在相应衍生发行版本上的安装&#xff0c;并建议针对例如Debian、Ubuntu等衍生发行版本上使用官方的对应版本。 Kubernetes X86-64 ARM64 Debian系 √ √ Re…

贪心算法[1]

首先用最最最经典的部分背包问题来引入贪心的思想。 由题意可知我们需要挑选出价值最大的物品放入背包&#xff0c;价值即单位价值。 我们需要计算出每一堆金币中单位价值。金币的属性涉及两个特征&#xff0c;重量和价值。 所以我们使用结构体。 上代码。 #include <i…

【debug】windows11安装WSL+Docker+本地部署cvcat

windows系统安装wsl虚拟机 首先观察是否已启用虚拟化&#xff1a; 在windows应用商店下载wsl 下载好后打开&#xff0c;创建用户名和密码&#xff0c;即可使用&#xff1a; 换源&#xff1a;ubuntu | 镜像站使用帮助 | 清华大学开源软件镜像站 | Tsinghua Open Source Mirr…

LeetCode215数组中第K个最大元素

题目描述 给定整数数组 nums 和整数 k&#xff0c;请返回数组中第 k 个最大的元素。请注意&#xff0c;你需要找的是数组排序后的第 k 个最大的元素&#xff0c;而不是第 k 个不同的元素。你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。 解析 快速排序的思想&#xff…

C++:vector的介绍及使用

✨✨✨学习的道路很枯燥&#xff0c;希望我们能并肩走下来! 文章目录 文章目录 前言 一、vector的介绍 二、vector的使用 2.1.构造和赋值重载&#xff08;Member functions&#xff09; 2.2 vector iterator 的使用 2.3 vector 空间增长问题 2.4 vector 增删查改 三 sort 四 v…

所以研究生有不变胖的吗?

天天吃 记得和骏骏一样减肥 分享昨天无人机拍的照片