Linux系统编程——线程控制
目录
一、前言
二、线程控制
1、线程创建与等待
2、获取线程的id
1)pthread_create参数获取线程id
2)pthread_self()获取线程id
3、线程状态
4、线程与进程共享信号处理方法
5、线程退出
1)pthread_exit()
2)pthread_cancel()
被取消的线程的信息
6、线程分离
一、前言
在上一篇文章中我们已经学习了线程的概念,线程的创建和等待,并且已经从根本上了解了线程和进程的相同点及不同点。在学习进程时,我们也学习了进程的相关控制接口,而线程作为更轻量级的进程,其自然也有着控制接口。
二、线程控制
1、线程创建与等待
在线程这一篇做介绍时,我们已经演示过了两个接口的使用:pthread_create 和 pthread_join ,事实上这两个接口都不是系统调用的接口,而是第三方库 pthread 中的接口,这也就是为什么我们在使用了这两个接口之后,编译的时候需要手动链接第三方库的原因。
在Linux操作系统中,线程是轻量级的进程,也就是说他还是进程,所以Linux并没有提供创建线程的系统调用,因为在操作系统眼中根本没有独立的线程这个概念。
所以Linux操作系统给用户提供的创建线程的系统调用接口是: vfork()
该系统调用的作用是创建一个子进程,其与父进程使用同样的一块进程地址空间,其实本质上就是我们所说的线程。
还有一个系统调用接口:clone()
Linux下的 clone() 系统调用是一个非常底层的接口,它用于创建一个新的进程。 clone() 函数提供了比 fork() 和 vfork() 更细粒度的控制,允许子进程与父进程共享各种资源。它是实现线程的一种方式.但是该接口的使用太过于麻烦,我们暂时不做考虑。
2、获取线程的id
我们先来对线程id和LWP(轻量级进程)编号做一下区分,上篇文章中,我们在创建线程后在运行时,我们使用了 ps -aL 来查看了系统中的线程
其中有一列是LWP。表示的是各自线程拥有的各自的轻量级进程编号,那么它和我们说的线程id又有什么区别和联系呢?
- 线程id:线程ID是操作系统为每个线程分配的一个唯一标识符。这个ID用于区分不同的线程。用于线程间通信、同步机制(如互斥锁、条件变量等)、日志记录等场合。
- 轻量级进程编号:在Linux中,线程实际上是作为轻量级进程实现的。LWP可以理解为一个具有独立调度能力的实体,它共享了创建它的进程的资源(如内存空间、文件描述符等),但有自己的执行上下文(如寄存器状态、栈等)。LWP也有自己的ID,这个ID实际上与线程ID相同,因为Linux中的每个线程都是作为一个LWP实现的。LWP编号主要用于内核层面的线程管理和调度。
1)pthread_create参数获取线程id
在使用 pthread_create() 接口的时候,我们就说过该接口的第一个参数是一个输出型参数,其实就是为了接收线程的id的。在我们使用该接口创建完线程之后,它就已经将创建成功的线程的id存储在了参数中。
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
using std::cout;
using std::endl;
using std::cerr;
using std::string;
void* pthreadFun1(void* argc){
string str=(char*)argc;
while(true)
{
cout<<str<<"My pid::"<<getpid()<<endl;
sleep(1);
}
}
int main()
{
pthread_t pth1;
pthread_create(&pth1,nullptr,pthreadFun1,(void*)"I am pthread1!");
sleep(1);
cout<<"Pthread id::"<<pth1<<endl;
while(true){
cout<<"Main pthread has gone!"<<"My pid::"<<getpid()<<endl;
sleep(1);
}
pthread_join(pth1,nullptr);
return 0;
}
2)pthread_self()获取线程id
使用系统调用接口:pthread_self
该接口的作用是:获取调用此接口的线程的id,并将id作为返回值。
接下来我们就可以封装一个函数,专门用来输出该线程和该线程的id。
#include<iostream>
#include<string>
#include<pthread.h>
#include<unistd.h>
using std::cout;
using std::endl;
using std::string;
void TidPrint(const char* str,const pthread_t tid)
{
cout<<"Thread Name::"<<str<<"ID::"<<tid<<endl;
}
void* tidFun(void* agrc)
{
char* str=(char*)agrc;
int count=5;
while(count)
{
TidPrint(str,pthread_self());
sleep(1);
count--;
}
return (void*)"thread is over!";
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,tidFun,(void*)"I am thread!");
sleep(1);
while(true)
{
cout<<"I am main thread!My pid::"<<getpid()<<endl;
sleep(1);
}
pthread_join(tid,nullptr);
return 0;
}
3、线程状态
在Linux中,我们不能像查看进程一细致地查看线程地状态,那么如果对于一个线程退出了却没有回收此时会发生什么呢?
#include<iostream>
#include<string>
#include<pthread.h>
#include<unistd.h>
using std::cout;
using std::endl;
using std::string;
void TidPrint(const char* str,const pthread_t tid)
{
cout<<"Thread Name::"<<str<<"ID::"<<tid<<endl;
}
void* tidFun(void* agrc)
{
char* str=(char*)agrc;
int count=5;
while(count)
{
TidPrint(str,pthread_self());
sleep(1);
count--;
}
cout<<"thread is over!"<<endl;
int *ret=new int(123);
return (void*)ret;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,tidFun,(void*)"I am thread!");
sleep(15);//设置时间长一点,确保线程先执行完自己的代码
void* ret=nullptr;
pthread_join(tid,&ret);
cout<<"Main thread has joined thread1,ready to print tread ret"<<endl;
sleep(2);
cout<<"print thread ret::"<<*((int*)ret)<<endl;//将ret变量所指向空间的值打印出来
delete (int*)ret;//释放空间
return 0;
}
详析代码:
int *ret=new int(123);
return (void*)ret;
这里动态开辟了有一个足够的内存存储int类型的对象,并且用123来初始化。由于 new 运算符返回的是指向新开辟的空间的指针,所以赋值给int* 类型的变量ret,最后将这个指针返回。
void* ret=nullptr;
pthread_join(tid,&ret);
这里是在确保线程已经执行完自己的代码之后,我们在这里回收它,之前讲到过pthread_join()接口一共有两个参数,第一个参数是需要等待的线程,第二个参数是接收线程退出的结果。线程在执行完tidFun函数后的返回结果是一个指针。所以我们需要提前定义一个指针变量用于接收线程退出的结果。
下面我们看结果,我们通过命令行监控脚本
结果表明了在线程退出(return)但是还没有被join的时候,从右侧的监控行里能看到线程会立马消失,这难道能说明线程是被操作系统自动回收的吗?我们不需要join吗?当然不是,如果我们不手动进行线程回收,就会造成内存泄漏问题。
4、线程与进程共享信号处理方法
#include<iostream>
#include<string>
#include<pthread.h>
#include<unistd.h>
using std::cout;
using std::endl;
using std::string;
void TidPrint(const char* str,const pthread_t tid)
{
cout<<"Thread Name::"<<str<<"ID::"<<tid<<endl;
}
void* tidFun(void* agrc)
{
char* str=(char*)agrc;
while(1)
{
TidPrint(str,pthread_self());
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,tidFun,(void*)"I am thread!");
while(1)
{
cout<<"process pid::"<<getpid()<<endl;
TidPrint("Main thread",pthread_self());
sleep(1);
}
return 0;
}
5、线程退出
我们在介绍进程的时候,知道进程退出的三种情况
- 代码跑完,结果正确,正常退出
- 代码跑完,结果不正确,正常退出
- 代码没跑完,进程异常退出
线程的退出情况也分为这几种,这三种情况统称为 执行流的退出情况。
在父子进程中,子进程无论是否正常退出都会向父进程发送退出信息。而在线程中,只有线程正常退出且回收的时候,主线程才会接收到来自线程的退出信息。这是为什么呢?显然这是因为线程异常之后,其实就是进程异常,进程异常之后,进程就会退出,此时接不接收线程的退出信息已经没有任何意义了。
1)pthread_exit()
一般情况下,线程退出除了我们上面例子中用到的在回调函数tidFun中return的情况,操作系统也为用户提供了线程退出的接口:pthread_exit()
需要注意的是 pthread_exit() 接口也是由第三方库pthread提供的,它的作用是 以指定的退出信息退出线程
#include<iostream>
#include<string>
#include<unistd.h>
#include<pthread.h>
using std::cout;
using std::endl;
void threadPrint(const void* threadName,const pthread_t tid)
{
cout<<"The threadname::"<<threadName<<"tid::"<<tid<<"pid::"<<getpid()<<endl;
}
void* tid1Fun(void* agrc)
{
char* threadName=(char*)agrc;
int cnt=5;
while(true)
{
threadPrint(threadName,pthread_self());
sleep(2);
cnt--;
if(cnt==0)
{
pthread_exit((void*)123);
}
}
}
int main()
{
pthread_t tid1;
pthread_create(&tid1,nullptr,tid1Fun,(void*)"I am tid1!");
sleep(10);
void* ret=nullptr;
pthread_join(tid1,&ret);
cout<<"Main thread join success!"<<endl;
sleep(2);
cout<<"thread exit result::"<<(long long)ret<<endl;
return 0;
}
运行结果
可以看到线程退出的时候的确是按照我们设定的方式退出的。
2)pthread_cancel()
该接口的作用是向指定的线程发送取消请求。此接口可以在同一进程内,只要任意线程调用,就会向任意线程发送,即同一进程内的所有线程都可以向任意线程发送取消信号,甚至是向主线程发送也可以,也可以向自己发送请求。并且如果线程被取消,则此线程的退出信息就是-1.
#include<iostream>
#include<string>
#include<unistd.h>
#include<pthread.h>
using std::cout;
using std::endl;
void threadPrint(const void* threadName,const pthread_t tid)
{
cout<<"The threadname::"<<threadName<<"tid::"<<tid<<"pid::"<<getpid()<<endl;
}
void* tid1Fun(void* agrc)
{
char* threadName=(char*)agrc;
int cnt=5;
while(true)
{
threadPrint(threadName,pthread_self());
sleep(2);
cnt--;
if(cnt==0)
{
pthread_exit((void*)123);
}
}
}
int main()
{
pthread_t tid1;
pthread_create(&tid1,nullptr,tid1Fun,(void*)"I am tid1!");
sleep(2);
pthread_cancel(tid1);
cout<<"Main thread has cancel tid1!"<<endl;
void* ret=nullptr;
pthread_join(tid1,&ret);
cout<<"Main thread join success!"<<endl;
sleep(2);
cout<<"thread exit result::"<<(long long)ret<<endl;
return 0;
}
可以看到结果也是如预期所料,由主线程取消了线程tid1,且最后看到的线程的退出信息是-1.
但是这里还存在一个问题就是主线程向创建的线程发送取消信号时是有可能发生错误的。上面的代码是在创建新线程2s之后发送取消信号的。如果没有这两秒的等待动作呢?
可以看到进程直接异常退出了。
被取消的线程的信息
我们在同一进程内任意线程中调用取消信号接口可以给该进程内任意线程发送取消信号,如果该线程被取消成功,则该线程的退出信息就成了-1,那么这个-1是怎么来的呢?
我们知道Linux下线程实际上是由PCB模拟来的,PCB中都维护着自己执行流的退出信息。无论我们是return也好,调用pthread_exit()接口也好,实际上最后都会修改PCB中维护的退出信息。
pthread_cancel()接口也是一样,如果取消线程成功,操作系统就会修改线程中PCB的退出信息,将其改为 PTHREAD_CANCELED ,这是一个由pthread第三方库所提供的宏,如下
6、线程分离
我们知道对于线程我们为了回收资源不造成内存泄漏,默认情况下都是要进行join的,但是对于我们需要关心线程返回值的情况,必须使用pthread_join()接口函数。如果我们并不关系该线程的返回值,那么其实我们可以不用手动回收线程,可以让其自动回收,这就是线程分离。
该接口的作用是 将线程与主线程分离,主线程就不管该分离线程的返回值、退出和资源回收情况 。这个接口一般是线程自己调用或者主线程调用。
由该接口的作用我们也知道分离和join是冲突的,一旦 线程被分离,其就会被自动回收。
下面我们使用C和C++混编的形式使用以下线程分离接口:pthread_detach()
#include<iostream>
#include<cstring>
#include<string>
#include<unistd.h>
#include<pthread.h>
using std::cout;
using std::endl;
using std::string;
void TidPrint(const void* threadname,const pthread_t tid)
{
printf("%s is running,tid::%lu,pid:;%d\n",threadname,tid,getpid());
}
void* DetachFun(void* agrc)
{
pthread_detach(pthread_self());
string threadname=(char*)agrc;
int cnt=1;
while(cnt--)
{
TidPrint(threadname.c_str(),pthread_self());
sleep(1);
}
printf("%s has detached!\n",threadname.c_str());
return nullptr;
}
int main()
{
pthread_t tid1,tid2,tid3,tid4;
pthread_create(&tid1,nullptr,DetachFun,(void*)"Thread 1");
pthread_create(&tid2,nullptr,DetachFun,(void*)"Thread 2");
pthread_create(&tid3,nullptr,DetachFun,(void*)"Thread 3");
pthread_create(&tid4,nullptr,DetachFun,(void*)"Thread 4");
sleep(2);
int joinable= pthread_join(tid1,nullptr);
cout<<strerror(joinable)<<endl;
joinable=pthread_join(tid2,nullptr);
cout<<strerror(joinable)<<endl;
joinable=pthread_join(tid3,nullptr);
cout<<strerror(joinable)<<endl;
joinable=pthread_join(tid4,nullptr);
cout<<strerror(joinable)<<endl;
return 0;
}
在上面的代码中,我们让每一个线程都执行DetachFun函数,并且在该函数中分离线程,为了查看现象,我们在主线程的最后再使用 pthread_join()接口看看是否能回收到已经分离的线程。
可以看到在最后主线程回收已经分离的线程时报错了,符合预期。这说明线程在分离过后已经被自动回收了,主线程回收不了只能报错。
值得注意的是上面我们创建了四个线程,且sleep保证到让所有创建的线程都执行完自己的代码(分离完)之后,主线程才往下执行。但是我们知道主线程和创建出来的新线程执行代码的先后顺序是不一定的,那么就会出现这么一种情况,我们创建完新线程之后,其还没有来得及分离自己,主线程就已经将它回收(join)了,所以我们就要考虑确保先让创建出来的新线程执行完自己的代码部分,再让主线程执行。或者干脆在主线程中分离线程。
在线程分离之后,主线程便不再管分离的线程了,即使是主线程先退出了,分离的线程还可能在运行。所以存在线程分离时,我们一般不退出主线程,而是常驻内存。事实上线程分离也可以看作线程的第四种退出方式——延时退出。