前言:大家早上中午晚上好!!今天来学习一下linux系统下所谓的线程吧!!!
一、重新理解进程,什么是进程?
1.1 图解
其中黑色虚线部分一整块就是进程,注意:一整块指的是:①:从一个程序运行起来后成为进程,OS堆这个进程进行描述PCB结构、②这个PCB中的进程地址空间所映射的物理内存资源、③页表,等等。。。总而言之:所有保证这个进程正常运行的所有资源!!
也就是说:进程是系统资源分配的基本单位,每一个进程的资源由系统分配,分配出来后的整一块资源都属于进程,进程相当于大家庭;
1.2、Linux系统下的线程
图解:
图中通过第一个创建的PCB描述出来的PCB2、PCB3、PCB4、PCB5暂时可以把它理解为线程!!(注意:只是暂时把它认为是线程);
这些通过主线程描述出来的PCB2、3、4、5.....等他们共同瓜分进程地址空间,而这个地址空间就是资源的窗口(通过页表跟物理内存建立一一映射关系),所以相当于同一进程下所有线程在同一块资源下进行瓜分,然后这些线程就可以分别执行自己的任务!!!(所以线程相当于家庭中的每一个成员:爸爸、妈妈、爷爷、奶奶、哥哥、姐姐、等!!)
1.3线程的调度
创建并完成资源分配后,cpu就可以对一个个PCB进行调度了,让他们分别执行代码中的某一部分内容,各自完成自己的任务!!所以:线程是cpu调度的基本单位
二、pthread库
2.1、pthread库是什么先不管,先用,先写一段代码
写一段代码实现创建一个线程,并让它执行自己的任务:
写之前先介绍一个创建线程的接口:
首先第一个参数为:输出型参数,需要自己定义pthread_t 类型变量,把这个变量地址传入,创建完成后获取到pthread_t;
第二个参数:线程属性,不需要修改设置为nullptr即可;
第三个参数:线程的入口函数,这个线程一被调用就会从入口函数开始执行,这个函数自己定义,这个函数的返回值是void*类型,参数为void*类型;
第四个参数:给入口函数传送的形参,如果不许要传设置为nullptr;
创建完线程,线程跑完后要对线程进程回收;
介绍一个等待线程的接口:
第一个参数:为线程的tid,传入哪个tid就等待哪个线程;
第二个参数为:获取这个线程的退出信息,如果不需要设置为nullptr;
返回值int:如果等待成功返回0,如果失败返回错误码;
有了这两个接口我们就来写一段代码吧:
#include <iostream>
using namespace std;
#include <pthread.h>
#include <unistd.h>
void* print(void* args)
{
int cnt=10;
while(cnt--)
{
cout<<"i am a thread !!!!"<<endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,print,nullptr);//线程创建
int cnt2=10;
while(cnt2--)
{
cout<<"i am a main thread!!!!"<<endl;
sleep(1);
}
int n=pthread_join(tid,nullptr);//线程等待
if(n==0)cout<<"wait success!!!"<<endl;
return 0;
}
当我们编译的时候发现报错:
原因是因为pthread.h是动态库,我们编译的时候需要加上 -lpthread 才能链接成功:
makefile:
mythread:mythread.cc
g++ -o $@ $^ -std=c++11 -lpthread //编译时加上-lpthread
.PHONY:clean
clean:
rm -f mythread
编译成功并生成mythread可执行程序:
运行:
我们看到主线程和分线程分别打印了10次,并成功等待了线程;
2.2 批量创建线程
上面我们创建的是一个线程,如果我们想批量创建线程呢?
那很简单,用一个for循环创建线程同时把tid保存起来,再用一个for循环等待tid:
同时我们每个线程获取一下pid_t(进程pid),同时每个线程获取一下pthread_t(tid),同时用轻量级命令 :ps- aL 查看所有轻量级进程:
#include <iostream>
using namespace std;
#include <pthread.h>
#include <unistd.h>
#include <vector>
#define THREAD_NUM 5
void* print(void* args)
{
cout<<"i am a thread !!!! tid is :"<<pthread_self()<<" pid is:"<<getpid()<<endl;
sleep(1);
return nullptr;
}
int main()
{
vector<pthread_t> tids;
//批量创建线程并保存tid
for(int i=0;i<THREAD_NUM;i++)
{
pthread_t tid;
pthread_create(&tid,nullptr,print,nullptr);
tids.push_back(tid);
}
for(int i=0;i<THREAD_NUM;i++)
{
int n=pthread_join(tids[i],nullptr);//线程批量等待
if(n==0)cout<<"wait success!!! tid is : "<<tids[i]<<endl;
}
return 0;
}
编译后运行:
同时命令行输入ps - aL:
我们发现所有线程它的pid 都是相同的,tid是一串很长的数字且都是不相同的,用ps -aL命令查看到LWP中只一个数字15304是跟pid一样其他都不相同!!
pthread_t tid 究竟是什么?LWP是什么?他们跟PID之间的关系是什么?还有pthread库又是什么?
2.3重新理解线程
①:在linux系统下一般把线程称为轻量级进程,因为在Linux内核中没有用新的数据结构来描述线程,而是通过原有的进程PCB来描述线程的!!
②: 因此linux系统是通过LWP来对这些所谓的 “线程” 进行调度的,LWP就是:light weight process (轻量级进程的意思),所以在linux下所谓的线程:就是一个个通过(第一个创建出来的进程的PCB)描述出来的轻量级进程!
③: 那么linux系统不创建新的数据结构来描述线程,等于linux不想管理这些线程,那么谁来管理?pthread库!!来了!pthread库是第三方库,当我们安装linux操作系统的时候必须把pthread库安装好,这个pthread库里有别人编写好的一系列对线程操作的接口,且这个库需要管理每一个用户创建出来的线程!!!怎么管理?->先描述再组织!->pthread库必须有一个描述线程的数据结构(TCB)->pthread库通过对一个个TCB的管理从而管理一个个线程!!
④线程由pthread库管理,pthread库不属于OS内核,所以线程不属于OS内核!!在linux下所谓的线程是用户级线程!!
当pthread库调用一系列的pthread_create、pthread_join、等接口时其底层调用的其实是clone这个系统调用接口:
图解:
所以我们可以回答上面的第一个问题了:pthread_t tid 究竟是什么:
pthread_t tid 是用户级线程的tid!!所谓用户级就是管理线程的pthread库属于用户级!!每一个TCB有其对应的tid,用户通过tid便可以指定对某个线程进行操作!!
然后我们还能回答第二个问题了:LWP是什么:
图解:
我们知道每个PCB都有它的PID,linux下的“线程”是通过进程的PCB描述出来的;
当它被描述出来之后就是一个所谓的“线程”了,也就是称为了cup调度的基本单位!
而PID代表的是进程,所谓进程代表的是被分配到的整块资源,所有“线程”共享的这一个块资源;
因此所有的“线程”的PID只有一个!!这些新创建出来的“线程”所以用LWP来标志!!
如果LWP跟PID一样说明这是第一个被创建出来的“线程”,即主线程!!!
最后cup就能通过LWP调度到每个它想调度的“线程”!!
同时我们可以回答第三个问题:pthread库是什么:
因为linux内核不管理线程,所以linux中把线程称之为:轻量级进程(它管理的是进程);
因为线程需要被管理,所以linux必须安装有pthread库;
pthread库就是用来管理用户创建除了的一切线程;
既然要管理所以pthread库必须对线程进行描述,必须要有TCB结构,通过一个个TCB结构的管理实现对所有线程的管理;
因此Linux下的所谓线程其实是用户级线程,那么我们可以这么认为:Linux下的线程=LWP(linux内核中的PCB轻量级进程标志)+pthread_t TID(用户中的TCB线程的标志);
三、线程的控制
3.1 线程的分离
如果主线程不想等待分支线程,我们可以让线程自己分离,分离后由OS进行;
线程分离接口:
#include<pthread.h>
int pthread_detach(pthread_t thread);
//1. 这是用来分离线程的pthread库接口
//2. thread:你想分离的线程的tid
//3. 返回值int,如果分离成功返回0,如果分离错误返回错误码
获取当前线程tid接口:
#include <pthread.h>
pthread_t pthread_self(void);
//1. pthread_t 返回值如果获取成功返回当前线程的tid
代码:
我们分离的同时等待一下线程看看能否等待成功:
线程分离的两种方法:线程自己分离:
#include <iostream>
using namespace std;
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <cstring>
#define THREAD_NUM 5
void* print(void*args)
{
pthread_detach(pthread_self());
cout<<"i am a thread!!"<<endl;
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,print,nullptr);
sleep(1);//等待1s让线程分离完成
int code=pthread_join(tid,nullptr);
if(code==0)
{
cout<<"wait success !!"<<endl;
}
else
cout<<"wait false!! error num is:" <<code<<"error message :"<<strerror(code)<<endl;
return 0;
}
编译运行:
线程分离成功,主线程等待失败,返回22错误码,意思是无效的参数,也就是没有等待到对应的tid;
线程分离方法二:主线程让他分离:
编译运行:
结果一样,说明成功分离分支线程;
3.2 线程的退出
线程退出的几种方式:
①线程运行完走到return 正常退出;
②终止线程的接口:
#include<pthread.h>
void pthread_exit(void* retval);
//1. retval 可以传入退出信息,由外部线程等待函数获取
我们退出的同时可以传入参数retval让主线程获取退出信息:
主线程等待获取线程退出信息需要传入一一个二级参数,因为join的第二个参数是一个输出型参数:
void* print(void*args)
{
cout<<"i am a thread!!"<<endl;
pthread_exit((void*)100);
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,print,nullptr);
sleep(1);
void*retval;
pthread_join(tid,&retval);
(long long int)retval;
printf("%d\n",retval);
return 0;
}
编译运行:
100退出码被带了出来;
③还有一个线程会终止的场景:当同一线程组内的任一线程出现异常,接收到终止信号,那么整个进程会崩溃,所有线程都会退出;
3.3 线程的取消
主线程可以取消一个线程,前提是这个线程已经被启动:
取消线程的接口:
#include<pthread.h>
int pthread_cancel( pthread_t thread);
//1. 取消一个线程,前提这个线程被启动
//2. thread : 需要取消的线程的tid
//3. 如果成功返回0,失败返回一个错误码
void* print(void*args)
{
while(true)
{
cout<<"i am a thread!!"<<endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,print,nullptr);
int code= pthread_cancel(tid);
if(code==0)
{
cout<<"取消成功"<<endl;
}
else
cout<<"取消失败"<<endl;
//sleep(1);//线程已被启动
pthread_join(tid,nullptr);
return 0;
}
运行:
四、总结线程的特点
4.1 、线程是系统调度的基本单位,进程是资源分配的基本单位;
4.2、线程的粒度小于进程,占用资源更少,因此通常多线程比多进程更高效;
4.3、线程没有独立的地址空间,线程是划分进程的地址空间,本质上使用的是同一地址空间!线程不拥有系统资源,线程共享进程的资源!
4.4、大量的计算使用多线程和多进程都可以实现并行/并发处理,区别在于线程的资源消耗小于进程,线程的稳定性不如进程,因此需要具体更细致的需求场景;
4.5、线程除了独立的栈空间和一组寄存器(保存线程的上下文)其他线程看不到之外其他所有资源都是所有线程共享的;
4.6、线程的通信速度更快,切换更快,因为他们在同地址空间内,且还共享了很多其他进程资源,比如页表指针,这些是不需要切换的!
今天的分享就到这里!如果对你有所帮助记得点赞收藏+关注哦!!谢谢!!!
咱下期见!!!