网上购物网站开发的背景,网站开发与应用 论文,k歌里的相片是通过网站做的吗,html 网站源码 卖手机文章目录 线程概念线程的优点线程的缺点线程异常线程用途理解虚拟地址 线程控制线程的创建线程终止线程等待线程分离封装线程库 线程概念
什么是线程#xff1f;
在一个程序里的一个执行路线就叫做线程#xff08;thread#xff09;。更准确的定义是#xff1a;线程是“一… 文章目录 线程概念线程的优点线程的缺点线程异常线程用途理解虚拟地址 线程控制线程的创建线程终止线程等待线程分离封装线程库 线程概念
什么是线程
在一个程序里的一个执行路线就叫做线程thread。更准确的定义是线程是“一个进程内部的控制序 列一切进程至少都有一个执行线程线程在进程内部运行本质是在进程地址空间内运行在Linux系统中在CPU眼中看到的PCB都要比传统的进程更加轻量化线程是比进程更加轻量化的一种执行流。透过进程虚拟地址空间可以看到进程的大部分资源将进程资源合理分配给每个执行流就形成了线程 执行流
如何看待之前的进程 之前的进程是内部只有一个执行流。
如何看待现在的进程 现在的进程内部有多个执行流。并且多个执行流共享大部分资源。
线程更像是一种标准各个平台的实现方式可能不同但是作用都是一样的。在Linux中因为线程也是执行流进程也是并且一个进程内的所有线程共享大部分资源。所以Linux中线程的实现就直接复用了进程的代码这样在OS的调度算法就只有一个进程调度就可以了一个进程中的的线程是共享大部分数据所以创建线程可以直接复制PCB就可以了一个进程中是可以存在多个线程的所以OS也一定会对线程进行管理所以OS也一定要有对线程描述的结构体(TCB)但是线程是直接复制进程的所以Linux中描述线程的结构体也是PCB。所以Linux下线程也称为轻量级进程。 因此现在看来线程是CPU调度的基本单位进程就是承担系统资源的基本实体。
线程的优点
创建一个新线程的代价要比创建一个新进程小得多与进程之间的切换相比线程之间的切换需要操作系统做的工作要少很多线程占用的资源要比进程少很多能充分利用多处理器的可并行数量在等待慢速I/O操作结束的同时程序可执行其他的计算任务计算密集型应用为了能在多处理器系统上运行将计算分解到多个线程中实现I/O密集型应用为了提高性能将I/O操作重叠。线程可以同时等待不同的I/O操作。
为什么说创建线程比进程的代价小呢呢 因为线程是在进程的地址空间中运行的并且线程创建更简单只需要复制进程的PCB只有一小部分的数据是私有的大部分数据都和进程是一样的。
线程切换的效率为什么高 如果是一个进程中的两个线程进程切换的话CPU中的有一部分寄存器中的内容是不需要被切换的并且因为局部性原理CPU中是存在Cache缓存的如果是一个进程中的两个线程进程切换根据局部性原理Cache缓存也大部分不会被替换但是如果是进程切换所有的寄存器和Cache都是要被切换的。
线程的缺点
性能损失 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多那么可能会有较大的性能损失这里的性能损失指的是增加了额外的同步和调度开销而可用的资源不变。健壮性降低 编写多线程需要更全面更深入的考虑在一个多线程程序里因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的换句话说线程之间是缺乏保护的。缺乏访问控制 进程是访问控制的基本粒度在一个线程中调用某些OS函数会对整个进程造成影响。编程难度提高 编写与调试一个多线程程序比单线程程序困难得多
线程异常
单个线程如果出现除零野指针问题导致线程崩溃进程也会随着崩溃线程是进程的执行分支线程出异常就类似进程出异常进而触发信号机制终止进程进程终止该 进程内的所有线程也就随即退出
线程用途
合理的使用多线程能提高CPU密集型程序的执行效率合理的使用多线程能提高IO密集型程序的用户体验如生活中我们一边写代码一边下载开发工具就是多线程运行的一种表现
我们说线程和线程之间大部分数据是共享的但是有一部分数据是私有的那么什么共享什么私有 共享 文件描述符表 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数) 当前工作目录 用户id和组id 地址空间 私有 线程ID(lwp) 一组寄存器 栈 errno 信号屏蔽字 调度优先级 理解虚拟地址
我们现在直到磁盘中文件是以4KB为单位存储的称之为页帧。并且我们编译好的可执行程序仍然遵守这样的规则所以我们的内存空间也是被划分为4KB大小为单位的空间称之页框所以在访问一块内存时只需要知道页框的首地址页内偏移就可以访问内存中的任意一个地址空间。因为内存会被划分成很多的页框所以OS要对内存管理就需要先描述在组织可以理解为所有的页框都被放在一个数组中然后OS对内存的管理就变成了对数组的增删查改。
虚拟地址到物理地址的转换是需要页表的页表的每一行存在很多的字段假设现在是10个字节要是每个物理地址都存在一个虚拟地址跟他直接映射的话假设是2^32的内存就需要40G来存放页表显然是不可能的所以虚拟地址和物理地址并不是直接进行映射的。 以32为的地址为例假设先现在有一个地址 11110011 10111011 00101001 10100101 一个32个比特位把前10 为1111001110作为一个整体一共10个比特位可以表示的范围就是0~1023所以假设有一个1024大小的数组就可以通过前十位的数据找到一个数组的下标数组的内容还是一个大小为1024的数组这个数组为页目录然后11 ~ 20为比特位1110110010作为数组指向的那个数组的下标数组的内容就是页框的起始地址然后最后12个比特位就是页内的偏移地址。所以通过这样的方式找到物理地址并且大大的减少了直接映射的使用空间因此在页表中是没有物理地址的在CPU中有一个MMU寄存器我们只需要把一个虚拟地址放进去就可以值就拿到物理地址然后进行访问。当然CPU中也有一个寄存器专门保存的就是当前页目录的起始地址。 每个线程要执行自己的代码根据我们传递的函数本质就是划分页表划分页表的本质就是划分地址空间。所以在进程的视角虚拟地址空间本身就是资源。
进程和线程关系如下
线程控制
Linux中是没有真正的线程的只有轻量级进程的概念所以OS只会提供轻量级进程的系统调用不会直接提供线程调用的接口。所以为了便于人们对线程的控制写Linux的程序员就把对线程的控制封装成了pthread原生线程库。对上提供线程控制的接口。
与线程有关的函数构成了一个完整的系列绝大多数函数的名字都是以“pthread_”打头的要使用这些函数库要通过引入头文pthread.h链接这些线程函数库时要使用编译器命令的“-lpthread”选项
线程的创建 传统的一些函数是成功返回0失败返回-1并且对全局变量errno赋值以指示错误。pthreads函数出错时不会设置全局变量errno而大部分其他POSIX函数会这样做。而是将错误代码通过返回值返回pthreads同样也提供了线程内的errno变量以支持其它使用errno的代码。对于pthreads函数的错误建议通过返回值判定因为读取返回值要比读取线程内的errno变量的开销更小
在Linux中可以通过ps -aL 查看创建的线程 我们可以看到同个进程内的线程的pid是相同的但是LWP是不同的因为LWP是线程的idLWP在内核中使用和我们用pthread_create获取出来的线程id是不一样的。内核中用LWP来表示线程的唯一。
pthread_create获取出来的线程id是我们用户自己使用的可以通过pthread_ self()来获取。 那么这个线程id到底是什么呢 我们使用的所有的线程的函数都不是系统直接提供的是原生线程库提供的而原生线程库一定不只会有我们一个进程用所以原生线程库中一定会存在多个进程创建的多个线程所以线程库一定要把我们多个进程创建的线程给管理好所以线程库中会存在描述线程的结构体结构体中有很多线程的数据(属于哪个进程线程id等)然后再用数据结构把各个描述线程的结构体管理起来。我们来认识一个系统调用 它可以通过flags的标识符来表示创建一个进程或者是创建一个轻量级进程(线程)我们看到参数中有一个child_stack的参数表示我们是可以传一段空间是作为线程的栈空间的所以我们前面说每个线程有自已的独立栈空间pthread_create的底层就是封装了这个函数。因此我们每个新线程都会有自己的栈空间而默认地址空间中的栈由主线程使用。在原生线程库中每个线程和每个线程的数据结构和栈空间还有一些相关的独立的数据放在一起而我们用户用的线程id就是线程属性在线程库中的地址。
现在理解了线程id后我们迷惑的应该是线程的局部存储是什么我们知道对于全局变量来说是被所有线程共享的但是加了一个__thread修饰一个变量程序在编译的时候就会为每个线程开辟一段空间专门存储这个变量也就是说这个变量每个线程都存在一份互不干扰。
线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。线程可以调用pthread_ exit终止自己。一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
pthread_exit pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
pthread_cancel
线程等待
为什么要进程线程等待
已经退出的线程其空间没有被释放仍然在进程的地址空间内。创建新的线程不会复用刚才退出线程的地址空间。
pthread_join 调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的总结如下:
如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED(-1)。如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参 数。如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
线程分离
一般情况下对于创建的线程我们是需要join的但是如果我们不关系线程的返回值那么join就会成为一中负担这时我们就可以对线程进程分离。即当线程退出时自动释放线程资源。
pthread_detach 可以是线程组内其他线程对目标线程进行分离也可以是线程自己分离可以通过pthread_self()来获取自己的线程id。
join和分离是冲突的一个线程不能既是join又是分离的。
如何理解语言中的线程库 本质就是对原生线程库的封装。
线程中可以进程fork吗可以进程execl程序替换吗 线程中是可以fork的也是可以进程execl程序替换的但是进行程序替换整个进程的代码都会被替换可能会影响其他线程的正常运行比较推荐先fork然后在进程程序替换。
封装线程库
基于上面的接口我们来模拟实现一下简单版的线程库。
#pragma once
#include iostream
#include string
#include functional
#include pthread.h
#include unistd.htemplateclass T
using func_t std::functionvoid(T);templateclass T
class Thread
{
public:Thread(const std::string name, func_tT func, T data) : _name(name), _func(func), _tid(0), _isruning(false), _data(data){}static void* threadRountine(void* attr){Thread* t static_castThread*(attr);t-_func(t-_data);}void Start(){int n pthread_create(_tid,nullptr,threadRountine,this);if(n 0) {_isruning true;}else {std::cerr pthread error std::endl;}}void Join(){if(!_isruning) return;int n pthread_join(_tid,nullptr);if(n 0){_isruning false;}else {std::cerr join error std::endl;}}std::string getname(){return _name;}bool isruning(){return _isruning;}
private:std::string _name;pthread_t _tid;bool _isruning;func_tT _func;T _data;
};如果需要返回值可以在成员变量可以加个模板参数在成员变量中定义一个返回值通过join得到就可以如果调用的函数参数有多个也可以通过类似的方法实现。