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

 

4.4pthread_detach(线程分离)

上面线程等待的时候,主线程就需要阻塞式的等待线程的释放,主线程就什么都干不了。能不能像以前的进程那样不需要阻塞等待(将信号设为忽略),等新线程结束后自动释放呢?尤其是不关心线程的返回值的时候,线程等待是一种负担

自然是可以的,只需要将自动释放的线程设置成分离状态(主线程不需要关心该线程状态,它就会自动释放)

man pthread_detach

 参数:pthread_t thread: 要分离线程的tid

返回值:分离成功返回0,失败返回错误码

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

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

相关文章

数据库与缓存⼀致性⽅案

数据库与缓存⼀致性⽅案 1、背景2、数据⼀致性⽅案设计3、数据⼀致性⽅案流程图4、关键代码4.1、 处理数据⼀致性的消息队列⼊⼝4.2、数据⼀致性配置的常量信息 1、背景 现有的业务场景下&#xff0c;都会涉及到数据库以及缓存双写的问题&#xff0c;⽆论是先删除缓存&#xf…

claude3国内API接口对接

众所周知&#xff0c;由于地理位置原因&#xff0c;Claude3不对国内开放&#xff0c;而国内的镜像网站使用又贵的离谱&#xff01; 因此&#xff0c;团队萌生了一个想法&#xff1a;为什么不创建一个一站式的平台&#xff0c;让用户能够通过单一的接口与多个模型交流呢&#x…

壁纸动态-Mac电脑-4K超高清[po破]动态壁纸[解]Dynamic WallPaper 安装使用教程

Mac分享吧 文章目录 效果一、准备工作二、开始安装1、双击运行软件&#xff0c;将其从左侧拖入右侧文件夹中&#xff0c;等待安装完毕2、应用程序显示软件图标&#xff0c;表示安装成功 三、运行调试1、打开软件&#xff0c;选择自己喜欢的壁纸2、调整设置&#xff0c;使多个壁…

OpenAI 再次刷新认知边界:GPT-4 颠覆语音助手市场,流畅度直逼真人互动?

前言 近日&#xff0c;美国人工智能研究公司 OpenAI 发布了其最新旗舰模型 GPT-4o&#xff0c;这一革命性的进展不仅标志着人工智能领域的新突破&#xff0c;更预示着即将步入一个全新的交互时代&#xff1f;GPT-4o 的发布&#xff0c;对于我们来说&#xff0c;意味着人工智能…

23.Labview中的数值类型讨论 ---- 位(bit)、字节(byte)、I8、U8、单双精度、复数

hello&#xff0c;大家好&#xff0c;本篇向大家介绍一个最常用但最容易让人忽略和最容易犯错的知识&#xff1a;数值。 “数值” 这个概念在Labview中被涉及的还是很多的&#xff0c;几乎任何一个程序都无可避免的会用到&#xff0c;但我相信大家绝大多数人对数值这个概念应用…

低代码开发平台(Low-code Development Platform)的模块组成部分

低代码开发平台&#xff08;Low-code Development Platform&#xff09;的模块组成部分主要包括以下几个方面&#xff1a; 低代码开发平台的模块组成部分可以按照包含系统、模块、菜单组织操作行为等维度进行详细阐述。以下是从这些方面对平台模块组成部分的说明&#xff1a; …

python编程:SQLite 管理图片数据库

在本博客中&#xff0c;我们将介绍如何使用 wxPython 和 sqlite3 模块构建一个 GUI 应用程序&#xff0c;该程序可以遍历指定文件夹中的所有图片&#xff0c;并将其信息存储到 SQLite 数据库中。 C:\pythoncode\new\InputImageOFFolderTOSqlite.py 项目简介 我们的目标是创建…

【Modelground】个人AI产品MVP迭代平台(2)——网站从0-1部署教程

文章目录 1.选购一台云服务器2. 购买域名3. 通过nginx部署静态网站4. 通过gitee在云服务器拉取代码5. ICP备案总结 1.选购一台云服务器 目前阿里云在促销&#xff0c;一台2核2GB内存3Mbps宽带的云服务器&#xff0c;一年只需要99元&#xff0c;学生更便宜&#xff0c;我认为这…

LitCTF部分wp

litctf wp 第一次ak了web和misc&#xff0c;非常激动&#xff0c;感谢lictf给我这个机会 最终成果 全靠队里的密码逆向✌带飞。一个人就砍了近一半的分数 这里是我们队的wp web exx 题目名反过来就是xxe&#xff0c;考察xxe&#xff0c;查看登录的数据包 发现传的就是xml…

【Java数据结构】详解LinkedList与链表(四)

&#x1f512;文章目录&#xff1a; 1.❤️❤️前言~&#x1f973;&#x1f389;&#x1f389;&#x1f389; 2.什么是LinkedList 3.LinkedList的使用 3.1LinkedList的构造方法 3.2LinkedList的其他常用方法介绍 addAll方法 subList方法 LinkedList的常用方法总使…

携程梁建章:持续投资创新与AI,开启旅游行业未来增长

5月30至31日&#xff0c;携程集团在上海和张家界举办Envision 2024全球合作伙伴大会&#xff0c;邀请超50个国家和地区的1600余名外籍旅游业嘉宾与会&#xff0c;共同探讨中国跨境旅游市场发展机遇&#xff0c;讲好中国故事。 携程国际业务增速迅猛&#xff0c;创新与AI解锁未…

【设计模式深度剖析】【4】【结构型】【组合模式】| 以文件系统为例加深理解

&#x1f448;️上一篇:适配器模式 | 下一篇:桥接模式&#x1f449;️ 设计模式-专栏&#x1f448;️ 目 录 组合模式定义英文原话直译如何理解&#xff1f; 3个角色UML类图代码示例 组合模式的优点组合模式的使用场景示例解析&#xff1a;文件系统UML类图代码示例 组合模…

sqoop操作

介绍 sqoop是隶属于Apache旗下的, 最早是属于cloudera公司的,是一个用户进行数据的导入导出的工具, 主要是将关系型的数据库(MySQL, oracle...)导入到hadoop生态圈(HDFS,HIVE,Hbase...) , 以及将hadoop生态圈数据导出到关系型数据库中 操作 将数据从mysql中导入到HDFS中 1.全量…

【用Python画画】六一儿童节画爱心

本文收录于 《Python编程入门》专栏&#xff0c;从零基础开始&#xff0c;分享一些Python编程基础知识&#xff0c;欢迎关注&#xff0c;谢谢&#xff01; 文章目录 一、前言二、代码示例三、知识点梳理四、总结 一、前言 本文介绍如何使用Python的海龟画图工具turtle&#xf…

echarts绘制三维柱状图

echarts ECharts 是一个使用 JavaScript 实现的开源可视化库&#xff0c;主要用于数据的可视化展示。ECharts 支持丰富的图表类型&#xff0c;如折线图、柱状图、饼图、地图、K线图等&#xff0c;可以满足不同类型数据的展示需求。 文档地址&#xff1a;echarts 本次所绘制三…

React-表单受控绑定

概念&#xff1a;使用React组件的状态&#xff08;useState&#xff09;控制表单的状态 1.准备一个React状态值 2.通过value属性绑定状态&#xff0c;通过onChange属性绑定状态同步的函数

【Linux】写一个日志类

文章目录 1. 源代码2. 函数功能概览3. 代码详细解释3.1 头文件和宏定义3.2 Log类定义3.3 打印日志的方法3.4 操作符重载和析构函数3.5 可变参数函数的原理 4. 测试用例 1. 源代码 下面代码定义了一个 Log 类&#xff0c;用于记录日志信息。这个类支持将日志信息输出到屏幕、单…

T检验——单样本t检验/两独立样本t检验/配对样本t检验

T检验——单样本t检验/两独立样本t检验/配对样本t检验 1.单样本t检验1.1 适用范围 2. &#xff08; 独立样本t检验&#xff09;两独立样本t检验3.ANOVA多组样本显著性检验&#xff08;2组以上&#xff09;4. 配对样本T检验 1.单样本t检验 1.1 适用范围 单样本t检验:即已知样本…

virtualbox虚拟机、centos7安装增强工具

文章目录 1. virtualBox语言设置2. 设置终端启动快捷键3. 添加virtualbox 增强工具4. 设置共享文件夹 1. virtualBox语言设置 virtualbox -> file -> perferences -> language ->选择对应的语言 -> OK virtualbox -> 管理 -> 全局设定 -> 语言 -> …

Redis数据类型(下篇)

5.Redis有序集合zset(sorted set) 本质就是在set的基础上&#xff0c;每个val值前面加了一个score分数值。 &#xff08;1&#xff09;向有序集合中添加多个&#xff08;或者一个&#xff09;元素和其对应的分数 127.0.0.1:6379> zadd zset1 100 a 90 b 80 c 70 d 60 e (…