绪论
每日激励:“不设限和自我肯定的心态:I can do all things。 — Stephen Curry”
绪论:
本章是继承上一章线程基础,本章将结合代码和逻辑图的方式带你去认识和了解控制线程中常用的函数这些函数对后面的开发以及对线程底层的了解都非常的重要,后续将继续更新Linux线程的更多知识,敬请期待吧~
————————
早关注不迷路,话不多说安全带系好,发车啦(建议电脑观看)。
1. 线程控制的函数:
1.1创建线程:pthread_create:
头文件:
#include <pthread.h>
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数:
- thread:线程id(类似于进程的pid,输出型)
- attr:线程的属性(暂时不管统一写成nullptr)
- start_routine:函数指针,他的类型是void*( * )(void*),代表多线程执行的函数(自己自定义他的任务)
- arg:是用来给第三个函数指针传参数的 (因为其类型是void*所以我们可以传递任意类型进去!(包括传递对象))
Linux中本质是没有线程的,只有轻量级进程的概念,所以Linux OS只会提供轻量级进程创建的系统调用,不会直接提供线程的创建接口。
在学习操作系统时只学过操作系统中的线程,没有学过Linux中轻量级进程(LWP)概念的人也能正常的使用,就必须得在操作系统和用户之间重新在写一个pthread原生线程库让所有人都能正常使用通过线程,而底层其实是轻量级进程。
既然他是一个第三方库,那在编译时就需要接入外部库,通过-l附加指令(-l库名字)
所以通过编译器g++,需要附加:-lpthread引入线程库
注:其中如何传参给第三个参数的函数:
这个函数的参数类型是void*,也就表示能接受任何类型,在内部使用时进行强转即可。
练习证明线程的健壮性低
使用上面函数 以及 证明一个线程崩溃会影响所有线程崩溃:
代码:
#include<iostream>
#include<string>
#include<pthread.h>
#include<unistd.h>
#include<vector>
#include<functional>
using namespace std;
using func_t = function<void()>;//function创建了一个接收函数签名void()的包装器类型,他能接收该类型的函数(void:返回值,():函数没有参数)
//创建一个对象,其中包含了线程名,线程创建的时间,该线程所要执行的函数
class ThreadData
{
public:
ThreadData(const string& name,const uint64_t & ctime,func_t f)
:threadname(name),createtime(ctime),func(f)
{}
public:
string threadname;//线程名
uint64_t createtime;//创建时间
func_t func;//接收函数(void())的包装器
};
void* ThreadRountine(void*arg)//创建一个线程后调用的函数,其中arg是参数,通过第四个参数传递进来
{
ThreadData* td = static_cast<ThreadData*>(arg);//进行类型的强转成,对象
while(true)
{
td->func();//对象中存着,真的调用的函数!
cout << "threadname: "<< td->threadname << " create time: "<< td->createtime <<endl;//通过对象访问成员变量
sleep(1);
if(td->threadname == "thread-4")//在4号进处出产生信号,观察情况
{
cout << td->threadname << " create exeption" << endl;
int i = 1;
i /= 0;//让4号线程进来时发生除零异常导致崩溃看看是否会影响
}
}
}
void Print()
{
cout << "I am Thread of part: ";
}
const int cnt = 5;
int main()
{
cout << "main thread" << endl;
for(int i = 0; i < cnt ;i++)//主线程循环多次,创建多个线程
{
sleep(1);
char tname[64];
snprintf(tname,sizeof(tname),"%s-%d","thread",i);//让每个下标对应一个进程名tname
ThreadData* td = new ThreadData(tname,(uint64_t)time(nullptr),Print);//创建一个对象,并初始化
//time函数获取当前时间(传递空)
pthread_t tid;
pthread_create(&tid,nullptr,ThreadRountine,td);//创建进程
}
while(true)
{
sleep(10);
}
return 0;
}
最终结果如下图(4号线程崩溃导致所有线程崩溃):
1.2 获取线程id:pthread_self
头文件:
#include <pthread.h>
pthread_t pthread_self(void);
实操代码:
using func_t = function<void()>;
string ToHex(pthread_t tid)
{
char id[64];
snprintf(id,sizeof(id),"0x%lx",tid);
return id;
}
void* ThreadRountine(void*arg)
{
string threadname = static_cast<const char*>(arg);
while(true)
{
cout << "new thread name: "<< threadname << " thread id: "<< ToHex(pthread_self()) <<endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,ThreadRountine,(void*)"thread-1");
while(true)
{
cout << "main thread , sub thread " << tid << " main thread id: "<< ToHex(pthread_self())<<endl;
sleep(1);
}
return 0;
}
最终结果如下图:
其中不难发现线程id值非常大,把它通过自定义函数ToHex转成十六进制来看,发现它很像一个地址,那他是地址吗?
再通过ps -aL查看线程的LWP发现和线程id并不一样?
上述两问题,我们继续往下看,后面通过底层分析解释(因为内容较多,见2.线程id和LWP到底是什么)。
1.3 线程的终止
1. 默认情况下,线程跑完后就会终止
string ToHex(pthread_t tid)
{
char id[64];
snprintf(id,sizeof(id),"0x%x",tid);
return id;
}
void* ThreadRountine(void*arg)
{
string threadname = static_cast<const char*>(arg);
int cnt = 5;
while(cnt--)
{
cout << "new thread name: "<< threadname << " thread id: "<< ToHex(pthread_self()) <<endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,ThreadRountine,(void*)"thread-1");
while(true)
{
cout << "main thread , sub thread " << ToHex(tid) << " main thread id: "<< ToHex(pthread_self())<<endl;
sleep(1);
}
return 0;
}
其中注意的是:exit(int)是用来终止的进程(不能作为线程的一种),否则整个进程都会被终止!
2. 用pthread_exit 终止线程
头文件:#include<pthread.h>
void* ThreadRountine(void*arg)
{
string threadname = static_cast<const char*>(arg);
int cnt = 5;
while(cnt--)
{
cout << "new thread name: "<< threadname << " thread id: "<< ToHex(pthread_self()) <<endl;
sleep(1);
}
// return nullptr;
pthread_exit(nullptr);
}
这里替代上面代码中的同位置函数即源码
1.4 线程的返回
- 线程返回的时候默认是要被等待的
- 进程直接退出,没有等待线程的话,会导致类似进程的僵尸问题
那么就继续引出下面等待函数
1.4.1 线程等待的函数:
头文件:#include <pthread.h>
int pthread_join(pthread_t thread,
void **retval);
- thread:线程的id
- retval:一个输出型参数,用于获取线程的返回值(该参数得到线程返回值 void*,外部要得到就得使用void**),也就是等待后其参数二会接收来自对应线程的返回值。
返回值:如果等待成功返回0,失败则返回错误码
using func_t = function<void()>;//包装器
string ToHex(pthread_t tid)
{
char id[64];
snprintf(id,sizeof(id),"0x%x",tid);
return id;
}
void* ThreadRountine(void*arg)
{
string threadname = static_cast<const char*>(arg);
int cnt = 5;
while(cnt--)
{
cout << "new thread name: "<< threadname << " thread id: "<< ToHex(pthread_self()) <<endl;
sleep(1);
}
return (void*)"thread-done";//返回的是这个字符串常量的起始地址
// pthread_exit(nullptr);
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,ThreadRountine,(void*)"thread-1");
cout << " main thread id: "<< ToHex(pthread_self())<<endl;
sleep(8);
void* ret = nullptr;
int n = pthread_join(tid,&ret);
cout << "main thread get new thread return: " << (const char*)ret <<endl;//获取线程的返回值,并打印
return 0;
}
返回用ret接收,因为其类型是void*,进行强转为const char打印出来。
同理因为返回的是void所以可以返回的任意类型的(包括对象),外部使用强转回来即可使用。
1.5 线程的状态
- joinable状态:线程默认为joinable的,主线程会在pthread_join处阻塞式的等待线程。
- 分离状态:就不用等待了主线程就不用管线程,线程运行结束就自动退出了。(不等待)
状态可以理解成:一个家庭,家庭成员他们相互有关也就是joinable的(你的事情父母会管),而分离状态也就也就相当于儿子和父母的关系不好分家了,他们就互不关心了(资源隔离)。
不过线程再怎么分离,资源还是多线程共用的,一个线程出问题别的线程也会出错。
那么状态的不同就引出了不同的情况,也就引出了不同状态时使用的不同函数
1.5.1 线程分离的函数:
头文件:#include <pthread.h>
int pthread_detach(pthread_t thread);
该函数可以在线程内使用,也可以直接在主线程内使用(参数指定要分离的线程id)。
实操查看线程的两种状态(分离和等待)
void* ThreadRountine(void*arg)
{
pthread_detach(pthread_self());
int cnt = 5;
while(cnt--)
{
cout << "new thread runing... "<<endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,ThreadRountine,(void*)"thread-1");
sleep(1);
int n = pthread_join(tid,nullptr);
cout << "main thread get new thread return: " << n <<endl;
return 0;
}
其中返回错误码 22 是通用的错误代码,在不同的编程语言、系统或环境中可能有不同的含义。通常,它对应的描述是 Invalid argument(无效参数)。
通过上图,可以发现打印好像出了点问题,但其实本质就体现了线程的分离状态,线程和主进程分离了,也就形成了异步执行打印的操作,这样才出来点问题。
让我们继续看不分离的状态,也就是joinable状态(默认就是,所以注释分离函数即可)
void* ThreadRountine(void*arg)
{
// pthread_detach(pthread_self());
int cnt = 5;
while(cnt--)
{
cout << "new thread runing... "<<endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,ThreadRountine,(void*)"thread-1");
sleep(1);
pthread_detach(tid);
int n = pthread_join(tid,nullptr);
cout << "main thread get new thread return: " << n <<endl;
return 0;
}
1.6 取消(终止)线程函数pthread_cancel
头文件:#include <pthread.h>
int pthread_cancel(pthread_t thread);
成功返回0,反之返回错误码
-
取消后等待的退出码结果是-1(-1就表明线程是被取消掉的)
线程通过调用pthread_cancel异常终止,retval(pthread_join的第二个参数)所指向的单元里存放的是常量PTHREAD_CANCELED -
线程如果是被分离的,该线程仍可以被pthread_cancel取消,但不能被pthread_join等待。
并且主线程也是能取消的,取消后主线程内的代码将不会被执行。
2. 线程的id和LWP到底是什么?
首先了解线程中的概念:
- Linux中所有的线程接口都不是系统直接提供的接口,而是原生线程库pthread提供的接口
- 每个操作系统的任意版本都必须默认配备一个该库,否则多线程在LInux下跑步起来。
线程和系统的关系
-
用户成若有5个线程,那么本质上就是在内核系统中就会有5个LWP
-
Linux的线程一般称为用户级线程(因为本质线程只是在用户层实现,底层是轻量级进程)
-
对此所有线程(轻量级进程)都需要被管理,所以在中间的pthread库也需要能管理系统中的LWP(上图)
这样我们就能知道:
其实线程的id本质是原生库中的概念,而LWP是系统底层的概念,在Linux下在内核态和用户态见有一个线程库(他本质是为了给没学过LWP的人也能方便的使用的),其中线程库中有他自己的id(并且线程库是能找到对应线程tcb的),而底层使用的就是LWP,也就是通过id找到对应的tcb(id只是用于找到对应的tcb的他其实用的就是LWP),详细见3.2。
3.底层系统调用细节
线程的独立属性:
- 上下文(寄存器中存储)
- 栈结构,只有一个栈(只有一套寄存器),但有多个堆
3.1 创建轻量级进程
头文件:
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...);
- fn:是所要执行的方法(回调函数)
- child_stack:是申请所要使用的栈的空间地址(允许用户传入栈空间)
- flag:用来区分创建的是进程还是轻量级进程
- arg:是用来给第一个函数传参的
pthread创建线程的本质就是通过函数clone创建的:
也就表示
- pthread_create()的底层是封装了clone的
- clone它同样也是fork的底层。
child_stack可以通过malloc来申请空间,所以每个新线程的栈都是在库中维护,其中线程库在内存共享区有指向自己的栈空间的地址变量。而默认地址空间的栈,由主线程使用。
3.2 如何理解pthread库来管理线程
一个操作系统可能有多个进程,而这些进程他们所使用的都是同一个相同的库,所以称他为共享的!
并且线程是在用户地址空间也就是共享区(内存映射段,不了解的可以搜一下内存模型,如下图)
其中mmap区就是在内存映射段的(存储着线程库)
mmap区域的动态库中就包含有线程库,其中当一个线程的创建就会再动态库中创建一个线程的属性集:
struct pthread会存着线程的属性:
- 线程的内部存储
- 线程栈会指向一块自己的空间
线程的创建和管理通常由线程库(如 pthread)抽象化实现,底层依赖于 clone 系统调用。使用 clone 创建线程时,需要提供用户函数(线程执行的函数)、栈地址和一组标志位,这些标志位决定了线程之间共享的资源(如地址空间、文件描述符等)。线程的属性集由线程库维护,底层 clone 系统调用完成轻量级进程的创建。
当线程完成执行时,可以通过返回一个 void* 值向调用方传递结果。通过调用 pthread_join,可以等待线程完成,并获取其返回值。线程库维护一个类似数组的结构,用于存储每个线程的属性集(如线程 ID、栈地址、返回值等)。线程 ID(tid)可作为索引,用于高效管理和调度线程。
每个线程的属性集可以看作数组中的一个元素。线程库在底层以动态数组的形式维护这些属性集,随着线程的创建,不断扩展数组结构,以便支持更多的线程。
具体如下图(类似于数组的底层):
所以我们所用的线程id,它就是线程属性集合在库中的地址(每个属性集的起始地址)!!!所以pthread_join通过线程id就能很好的拿到想要拿到的线程的数据。
LWP和线程id不一样是因为:LWP是内核的概念,而tid是线程库里的概念(人们不用理解LWP)。
本章完。预知后事如何,暂听下回分解。
如果有任何问题欢迎讨论哈!
如果觉得这篇文章对你有所帮助的话点点赞吧!
持续更新大量Linux细致内容,早关注不迷路。
标签:函数,内附,void,Linux,id,线程,pthread,tid,逻辑图 From: https://blog.csdn.net/ZYK069/article/details/144943955