目录
前言:
上一篇文章我们着重对线程他的共享代码这个特点进行了论述,讲解了部分性质与容易出现的问题。
那么现在我们本篇文章就更加深层次的来学习一下线程吧!
一、上文补充
我们说线程的绝大部分资源都是共享的,这句话其实不是很完善。
最准确的说法,线程之间一切都是共享的。
有人说:不是还有独立的栈结构吗?
其实,栈结构的独立性是编程语言或操作系统提供的逻辑保护,而非物理隔离。他只是不想让你看见,所以说如果你非要看见,是有办法的:
#include<iostream>
#include<pthread.h>
#include<unistd.h>
int *ptr=nullptr;
void* func1(void*_name)
{
std::string name=static_cast<const char*>(_name);
int a=100;//临时变量,在栈上面
ptr=&a;
while(true)
{
std::cout<<name<<"线程运行: "<<a<<std::endl;
sleep(1);
}
}
void* func2(void *_name)
{
std::string name=static_cast<const char*>(_name);
while(true)
{
if(ptr!=nullptr)//防止func2先运行此时ptr还为空直接越界访问
std::cout<<name<<"线程运行: "<<(*ptr)++<<std::endl;
sleep(1);
}
}
int main()
{
pthread_t tid1,tid2;
pthread_create(&tid1,nullptr,func1,(void*)"pthread-1");
pthread_create(&tid2,nullptr,func2,(void*)"pthread-2");
pthread_join(tid1,nullptr);
pthread_join(tid2,nullptr);
return 0;
}
可以看见这个代码中,我们在func一的线程中初始化了一个局部变量,但是我们可以通过上节课讲的共享的特性,用全局变量指针,来让多个线程之间看到同一份资源。
二、线程终止
我们之前一开始就给大家讲解了线程的创建,大家也就知道了在linux中严格意义上来说是没有线程的概念的,只有轻量级进程。
而我们的pthread_create函数内部其实是封装了clone,clone这个函数实际上是用来创建轻量级进程的。为了产生线程这个概念,我们就在轻量级进程上面进行了一层封装,这也就是pthread_create的由来。
而pthread_create这个函数被各大语言封装成了各大语言(C++,java)线程接口,但是由于底层是pthread_create,所以我们在编译时都要链接上我们的pthread库。
回顾完我们线程创建的知识,现在我们来了解一下线程退出的几个方法:
我们手动给主副线程最后结束返回return可以知道,主线程使用return会导致所有线程都退出,也就是理论上的进程退出了。而副线程的return只会导致副线程退出。(这就跟我们调用了一个函数return返回一样,不会影响main函数)
那么exit呢?这个函数我们在讲进程退出时说过,这个函数会导致进程之间退出。那么大家就可以预料到了,无论是在主线程还是副线程中调用exit,都会导致所以线程立马退出,也就相当于进程退出。
正如同进程有专门的exit函数来退出,线程也有专门的函数调用来退出线程:
pthread_exit函数的作用就是终止调用它的线程,并可选地返回一个值(线程的退出状态)。
void* func(void* arg)
{
printf("子线程正在运行\n");
pthread_exit((void*)42); // 终止线程并返回值42
}
int main()
{
pthread_t tid;
void* retval;
pthread_create(&tid, NULL, func, NULL);
pthread_join(tid, &retval); // 获取子线程的退出值
printf("子线程返回: %ld\n", (long)retval); // 输出42
return 0;
}
值得一提的是,在多线程编程中,当线程通过 pthread_exit
或 return
返回一个指针时,必须确保该指针指向的内存是全局变量或堆内存(malloc
分配),而不能是线程栈上的局部变量。这是因为线程栈的生命周期与线程绑定,线程退出后,其栈内存会被回收,导致返回的指针指向无效内存(悬垂指针),引发未定义行为(如数据损坏或程序崩溃)。
除了这个函数来退出线程之外,我们还可以取消线程:
pthread_cancel 是 POSIX 线程库中用于请求取消另一个线程的函数。它的作用类似于向目标线程发送一个“终止请求”,但具体是否终止、何时终止以及如何清理资源,取决于目标线程的取消状态和清理处理机制。
这个函数,通常是用我们的主线程调用,来取消副线程,取消的线程的返回值为-1。
void*func(void* argv)
{
while(true)
{
std::cout<<"子线程运行中 "<<std::endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, func, nullptr);
sleep(5);
pthread_cancel(tid);
void* ret=nullptr;
pthread_join(tid, &ret);
std::cout<<"子线程退出信息:"<<(long long int)ret<<std::endl;
return 0;
}
这里我们要注意什么呢?首先,你要取消一个线程,这个线程必须要先被启动了。并且,被你取消的进程一定要用join回收,否则会导致资源泄漏。
所以,我们就一定要谨慎调用这个函数。
三、线程等待
我们之前已经讲过线程等待函数pthread_join,所以我们这里不再过多赘述,我在这里补充一些内容。
那如果我们的主线程要做自己事情呢?
我们之前说过,线程等待会阻塞进程。有没有什么办法让主线程不阻塞呢?
有的:我们可以切换线程等待的状态。
一个线程有两种被等待的状态:
1、joined:线程需要join(默认状态)
2、detach:线程分离(主线程不必等待副线程)
我们可以调用pthread_detach函数使一个副线程的状态变为detach分离状态:
的作用是告诉操作系统:当该线程结束时,系统自动回收其资源,无需其他线程调用 pthread_join来等待它 。线程结束后,系统自动回收其资源,其他线程无法再调用 pthread_join等待它(调用会失败)。
但是这也会给我们带来一些问题:如果我们的主线程先退出了,副线程还没退出,并且副线程此时是分离状态呢?这会导致副线程直接退出(大部分系统下)
所以我们一定要保证主线程比副线程后退出,如果不能保证,就不要分离线程。
四、线程的exec问题
我们想问一下,当我们新建了一个线程之后,还能进行exec吗?此时exec会造成什么后果呢?
exec
会 完全替换当前进程的地址空间,包括所有线程(无论是否分离),而线程共享同一个进程地址空间,所以其他线程的执行会被强制中断,且 没有机会执行清理操作。
所以我们不能使用exec的调用接口。
那如果我们想使用exec函数,该怎么办呢?
答案是:fork!!
没错,就算是在副线程中,我们也可以调用fork接口,创造一个新进程!!
随后,就能在这个进程中使用exec的调用接口了。
当我们在副线程中调用fork,他只会复制当前线程。也就是说,新创建的进程内只会有一个PCB。
总结:
兄弟们,线程这玩意儿就是个共享怪胎,表面上说栈是独立的,但实际上我用个全局指针就能偷看其他线程的栈数据!创建线程底层就是个clone系统调用,各大语言都是套了层皮而已。
线程退出的姿势也可多:主线程return直接带崩全场,副线程return就跟函数返回一样乖巧。pthread_exit能优雅退场还能留个遗言,但记住别返回局部变量的地址,不然分分钟给你来个悬垂指针的惊喜!
pthread_cancel这货就是个线程杀手,一枪崩了目标线程,但记得一定要用join收尸,不然资源泄漏有你受的。如果想不阻塞主线程?detach一下就行,但主线程要是先溜了,子线程直接凉凉。所以要保证退出顺序哦~
最坑爹的是exec,这玩意儿一调用,管你几个线程统统完蛋!想用exec?先fork个新进程再说。不过fork也是个坑货,只复制当前线程,其他线程的锁啊资源啊全都不管了,死锁警告!
总之,玩线程就是在刀尖上跳舞,一个不小心就翻车!
希望对你们有用!