Linux——线程的慨念及控制
文章目录
目录
文章目录
前言
一、线程的概念
1、如何理解线程
2、重新定义线程及进程
3、重谈地址空间
4、线程的优缺点
1、优点:
2、缺点:
5、线程异常
6、线程的用途
二、线程的控制
1、线程创建
2、线程终止
3、线程等待
4、线程基本函数的使用代码:
5、主线程和次线程之间通信
总结
前言
- 线程是现代操作系统中实现并发执行的重要机制。在多处理器系统中,多个线程可以同时在不同的处理器核心上执行,充分利用硬件的并行性,提高系统的吞吐量。
- 对于单处理器系统,线程可以通过时分复用的方式,让用户感觉多个任务在同时进行。例如,在一个同时运行着文件下载、音乐播放和文本编辑的桌面操作系统中,这些任务可以分别由不同的线程来处理,操作系统通过合理地调度线程,使得每个任务都能得到适当的执行时间,从而提供了良好的用户体验。同时,线程也为软件开发者提供了一种更灵活的编程模型,方便他们实现复杂的并发算法和应用程序。
一、线程的概念
线程是进程内的执行分支。线程的执行分支是粒度,要比进程细。
1、如何理解线程
1、从执行角度理解线程
- 线程是进程内部的一条执行路径。可以把进程想象成一个工厂,而线程就是工厂里的工人。在这个工厂(进程)里有很多资源,如原材料(数据)、工具(文件描述符等资源),每个工人(线程)都可以使用这些资源来完成自己的工作任务。
2、从资源共享角度理解线程
- 线程共享进程的许多重要资源。这包括进程的地址空间,意味着所有线程可以访问进程中的全局变量和动态分配的内存。例如,在一个多线程的游戏程序中,多个线程可能共享游戏场景的数据结构,如地图信息、角色位置等。
3、从并发和并行角度理解线程
- 在单处理器系统中,线程通过时分复用的方式实现并发。就好像一个人(处理器)要同时做几件事情,他会快速地在不同任务(线程)之间切换。例如,在一个同时运行着文字处理软件和音乐播放软件的系统中,操作系统会快速地在这两个软件对应的线程之间切换,给用户造成一种两个软件同时运行的感觉。
4、从编程模型角度理解线程
- 对于程序员来说,线程提供了一种方便的方式来编写并发程序。通过创建多个线程,可以将一个复杂的任务分解成多个子任务,让这些子任务并发或并行地执行。
2、重新定义线程及进程
进程是资源分配的基本单位线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
线程ID一组寄存器栈
errno
信号屏蔽字调度优先级
进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
文件描述符表
每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)当前工作目录
用户id和组id
关系图
这是我们当初学进程的地址空间的时候的图片,它是我们进程的基本内核,进程是有自己的task_struct的,然后task_struct指向我们的地址空间,地址空间一共4gb,3gb用户级空间,1gb内核空间,然后进程还有自己的页表,它是用来映射物理空间和虚拟地址之间的关系表。
而我们的多线程通过概念我们知道多线程每一个进程是分支,那么说多线程的单一线程在进程内部,那么我们的图就变成下图
线程是进程的一个分支是最小的单位 ,我们学习了线程以后,我们就不在把进程和线程分开说了,我们把它称之为执行流,在linux中叫做轻量级进程。
在cup调度时,不会区分是进程还是线程,它只知道是执行流,只有操作系统才有进程和线程的区别。
那么线程是进程的分支,那么进程的地址空间就和分给线程,正文代码等空间都会给线程分配空间。
- 进程:进程是资源分配的基本单位,也是操作系统进行保护和调度的独立单位。它拥有自己独立的地址空间,这个地址空间包括代码段、数据段、堆和栈等部分。进程之间相互独立,一个进程的崩溃通常不会直接影响到其他进程。例如,在同时打开多个应用程序(如浏览器、文本编辑器和音乐播放器)时,每个应用程序都在操作系统中作为一个独立的进程运行。每个进程都有自己的一套资源,包括文件描述符、内存空间等。
- 线程:线程是进程内部的执行单元,是进程的一个实体,它是 CPU 调度和分派的基本单位。一个进程可以包含多个线程,这些线程共享进程的地址空间和其他资源,如打开的文件、全局变量等。比如,在一个多线程的浏览器进程中,一个线程可能负责页面的渲染,另一个线程负责处理用户的交互操作(如点击链接、滚动页面)。
因为线程是进程内部的,所以资源都在一起,cup在调度时就会很快找到数据,所以线程占用的资源要比进程少很多。
3、重谈地址空间
学习了线程以后我们的地址空间变成下图这样,但是我们的页表只有4kb大小,它是如何映射那么大的物理空间的呢?
在32位机器上,我们内核空间有1gb里面4kb是页表,然而我们把页表再细看,这是最基本的页表形式,是一个一维数组。数组的每个元素(页表项)对应虚拟地址空间中的一个页面。它是一个地址,32位机器的地址是32个比特位,前十个bit位是用来存储页目录项,中间 10 位用于索引页表,低 12 位为页内偏移量。
为什么后12位是偏移位呢?
因为我们的物理地址是以“页”,1页是4kb,在学习磁盘的时间我们知道操作系统的基本读取单位是4kb。而12个比特位就是4kb;
4、线程的优缺点
1、优点:
1、 创建一个新线程的代价要比创建一个新进程小得多
2、与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
3、线程占用的资源要比进程少很多
4、能充分利用多处理器的可并行数量
5、在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
6、计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
7、I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
2、缺点:
1、性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
2、健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
3、缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
4、编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
5、线程异常
1、单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
2、线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
6、线程的用途
合理的使用多线程,能提高CPU密集型程序的执行效率
合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现
二、线程的控制
1、线程创建
线程创建函数 - pthread_create ()
功能描述:用于创建一个新的线程。新线程会从指定的函数开始执行,并且可以传递一个参数给这个函数。
函数原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
参数解释:
thread
:是一个指向pthread_t
类型的指针,用于存储新线程的标识符。pthread_t
类型是一个用于唯一标识线程的类型,类似于进程 ID。attr
:是一个指向pthread_attr_t
类型的指针,用于设置线程的属性。例如,可以设置线程的栈大小、调度策略等属性。如果设置为NULL
,则表示使用默认属性。start_routine
:是一个函数指针,指向线程启动后要执行的函数。这个函数的参数类型是void*
,返回值类型也是void*
。arg
:是传递给start_routine
函数的参数,类型为void*
,可以通过强制类型转换在start_routine
函数中使用实际类型的参数。
返回值:成功时返回 0;失败时返回一个非零错误码,例如,如果没有足够的资源创建线程或者线程属性无效等情况会导致失败。可以使用perror
函数来打印错误信息。
2、线程终止
线程退出函数 - pthread_exit ()
- 功能描述:用于显式地终止当前线程。线程退出后,其占用的资源(如栈空间等)会被释放。
- 函数原型:
void pthread_exit(void *retval);
- 参数解释:
retval
是线程的返回值,类型为void*
。这个返回值可以被其他线程通过pthread_join
函数获取。
3、线程等待
线程等待函数 - pthread_join ()
- 功能描述:用于阻塞当前线程,直到指定的线程终止。它还可以获取被等待线程的返回值。
- 函数原型:
int pthread_join(pthread_t thread, void **retval);
- 参数解释:
thread
:是要等待的线程的标识符,即pthread_create
函数返回的线程 ID。retval
:是一个指向void*
类型的指针的指针,用于获取被等待线程的返回值。如果不需要获取返回值,可以将其设置为NULL
。
- 返回值:成功时返回 0;失败时返回一个非零错误码,例如,如果指定的线程已经被分离或者线程 ID 无效等情况会导致失败。
4、线程基本函数的使用代码:
#include<iostream>
#include<unistd.h>
using namespace std;
void* runtine(void* args)
{
cout<<"我是一个线程 "<<getpid()<<endl;
return (void*)10;
}
int main()
{
pthread_t pth;
pthread_create(&pth,nullptr,runtine,nullptr);
void* retun;
pthread_join(pth,&retun);
cout<<"我是一个主线程"<<getpid()<<"我等到了子线程的返回值;"<<(long long int)retun<<endl;
return 0;
}
因为返回的是一个将10强转为void*,是一个地址,所以是十六进制,也就是0xa(32位),我们直接将它强制long long 类型(int会出现段错误二进制位不够);
#include<iostream>
#include<unistd.h>
using namespace std;
void* runtine(void* args)
{
int cnt=10;
while (cnt--)
{
cout<<"我是一个线程 "<<getpid()<<endl;
if(cnt==5)
{
cout<<"线程exit退出"<<endl;
sleep(1);
pthread_exit((void*)5);
}
}
cout<<"线程return 退出"<<endl;
cout<<"我是一个线程 "<<getpid()<<endl;
return (void*)10;
}
int main()
{
pthread_t pth;
pthread_create(&pth,nullptr,runtine,nullptr);
void* retun;
pthread_join(pth,&retun);
cout<<"我是一个主线程"<<getpid()<<"我等到了子线程的返回值;"<<(long long int)retun<<endl;
return 0;
}
我们发现线程有两种退出方式,一种是return(代码跑完),一种是exit退出
还发现,它们的pid相等,那么就证明线程在内,那么如何区分线程和进程呢?
命令ps -aL
这里的LWP就是它们的不同 ;
5、主线程和次线程之间通信
#include<iostream>
#include<unistd.h>
#include<string.h>
#include <pthread.h>
#include <sys/syscall.h>
using namespace std;
void* runtine(void* args)
{
cout<<"我是子线程我的任务是打印"<<syscall(SYS_gettid)<<endl;
char* buff=(char*)args;
cout<<buff<<endl;
return (void*)"打印完成";
}
int main()
{
pthread_t pth;
void* buff=(void*)"我是要让次线程打印的内容";
pthread_create(&pth,nullptr,runtine,buff);
void* retun;
pthread_join(pth,&retun);
cout<<"我是一个主线程"<<syscall(SYS_gettid)<<"我等到了子线程的返回值;";
for(int i=0;i<20;i++)
{
cout<<((char*)retun)[i];
}
return 0;
}
总结
在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”一切进程至少都有一个执行线程
线程在进程内部运行,本质是在进程地址空间内运行
在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流