目录
写在前面的话
本文章将讲解进程的控制,包括进程的创建,进程的终止以及进程的等待,主要是以代码为主,因为很多情况下这些也只有代码可以说明问题.
在阅读本文前,最好已经清楚地了解了线程的概念,和进程的区别,这样阅读理解起来会比较轻松。
线程创建pthread_create
函数的使用
我们想要创建一个线程,需要用到pthread_create()这个函数,该函数的用法如下:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
-
thread
:指向pthread_t
类型的指针,用于存储新创建 线程的标识符。 -
attr
:指向pthread_attr_t
类型的指针,用于指定线程的属性,默认为NULL
表示使用默认属性。 -
start_routine
:指向线程函数的指针 ,线程函数是新线程所要执行的函数 。线程函数的原型是void *(*start_routine)(void *)
,其中参数和返回值类型可根据实际需求进行定义。 -
arg
:传递给线程函数的参数 ,可以是任意类型的指针。在线程函数内部可以通过参数来获取传递的数据。
返回值:
成功返回0,否则返回错误码。
我们用代码来看一下是如何使用的:
void* threadRoutine(void* args)
{
while(true)
{
cout << (char*)args << ", pid: " << getpid() << endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
//执行threadRoutine回调函数,这个函数的参数为 "thread 1"
pthread_create(&tid,nullptr,threadRoutine,(void*)"thread 1");
while(true)
{
cout << "main thread, pid: " << getpid() << endl;
sleep(2);
}
}
这段代码的作用是创建一个线程,并分别输出进程和线程的pid.
可以看到进程的pid和线程的pid是相同的.
但是CPU的基本调度单位是线程,它是如何区分的呢?
我们再创建一个窗口,输入:
ps -aL | head -1 && ps -aL | grep mythread
然后可以看到:
有一个LWP,这个就是轻量级进程的意思,CPU是以它来区分调度每一个线程的,它们的pid相等,因为都是在同一个进程内部。
线程异常
线程是进程的一个执行分支,因为线程也是在进程地址空间中运行,所以当线程出现异常时(如除0错误,野指针等等),会导致进程也出现异常,进而崩溃退出。当进程退出时,所有的线程也会随之退出。
我们在线程调用的回调函数中加入以下代码:
然后我们再执行,并观察进程和线程的状态:
我们可以发现此时发生了错误,而且进程和线程全部退出了,正如我们所想的那样。
线程等待pthread_join
线程等待是指一个线程暂停执行,等待其他线程完成其任务后再继续执行。
当一个线程被执行完时,需要被等待,然后回收 这个线程。如果主线程不等待,便会引发类似于进程中僵尸问题,造成内存资源泄露。
线程等待的函数是pthread_join,该函数的原型如下:
int pthread_join(pthread_t thread, void **retval);
两个参数:
thread
是要等待的目标线程的标识符.
retval
是一个指向指针的指针,用于获取目标线程的返回值.如果不需要则设为NULL.
这个参数后面会详细说明。
返回值
同样地,成功返回0,失败返回错误码.
我们编写如下代码,顺便看一下函数的使用:
void* threadRoutine(void* args)
{
int i = 0;
while(true)
{
cout << (char*)args << ", pid: " << getpid() << endl;
if(i++ == 5) break;
sleep(1);
}
cout << "new thread quit ..." << endl;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,threadRoutine,(void*)"thread 1");
pthread_join(tid,nullptr);//默认会阻塞等待新线程退出.
cout << "main thread wait done, main thread quit" << endl;
}
这段程序首先创建了一个线程,然后主线程会卡在pthread_join处,等待新创建线程的结束,新线程是循环5次,然后退出,所以我们运行一下:
可以发现5秒后,新线程退出后,主线程也等待到了,就将其回收,然后主线程也结束了。
那第二个参数retval到底是什么呢? 我们把它展开:return val,这样就可以知道大概意思了,是返回值,谁的返回值呢?当然是我们等待的这个线程的返回值!
它(新线程执行的函数)的返回值类型是个指针,我们想要得到它,所以我们需要传入一个指针变量,但是因为我们是想拿到这个返回值,所以必须对这个指针变量取地址,这样修改的就是指针变量本身的值而不是形参了,这样参数就成了二级指针。
总结来说,第二个参数retval就是等待的目标线程的返回值.是一个输出型参数。
我们可以在新线程调用的函数里最后加上一个返回值:
然后我们再在主线程中,将返回的值接收并且将其输出出来:
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,threadRoutine,(void*)"thread 1");
void* ret = nullptr;
pthread_join(tid,&ret);//默认会阻塞等待新线程退出.
//一个细节:需要将ret强转为long long类型,因为void*在linux下是8字节,而int是4字节,所以不能强转为int
cout << "main thread wait done, main thread quit..., new thread return val :" << (long long)ret << endl;
}
此时也便得到了我们返回的值:
这便是线程等待了
线程终止pthread_exit
首先先要说明的是,终止线程一定不要用exit或者_exit, 它们是直接终止进程的。
我们写上后,5秒后整个进程都会退出,后面的线程的代码和主线程的代码都执行不了:
我们想让线程终止,则需要用到函数pthread_exit(),该函数的原型如下:
void pthread_exit(void *retval);
该函数能终止线程,并返回一个返回值.
第一个参数其实便是我们想结束线程的同时 返回的值,这个也很好理解,毕竟线程等待时,那个输出型参数要求的值也是void*的.
这样线程的后续代码就执行不了了,主进程线程等待 到 线程结束后,则继续执行后续的代码:
这样便是正常的线程退出了。
还有一个使线程退出的函数,pthread_cancel,这个是进程主动终止某一个线程的运行,该函数的原型如下:
int pthread_cancel(pthread_t thread);
该参数为要取消的线程的标识符.
pthread_cancel(tid);
cout << "pthread cancel: " << tid << endl;
我们直接运行:
所以结束线程一共有三种方法:
如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED((void*)-1)。
如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数
同时我们可以发现这个tid是一个很长的数字,这个到底是什么呢?
首先它是我们一开始线程创建的时候的一个输出型参数,即把线程的唯一标识符拿出来:
它的本质其实是一个地址。取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
我们看下面这张图:
我们要知道我们上面使用的函数,都是在pthread库中的,所以每次都要先加载到物理内存,然后经过到进程地址空间的共享区。
我们也说过,每个线程也有少部分资源是属于自己的,栈区也包括在内,那么是如何保证栈区是每一个线程独占的呢?
如果进程中只有一个执行流,那么它就独享内核栈区空间.
如果有多个执行流,那么它们这些独立的数据需要被组织管理起来, 这个操作由库帮我们完成,由于库在共享空间中,所以此时库会给每个线程在共享空间内划分一块栈区,供线程使用,这样就保证了栈区的独立,当然还有其它的数据,这是一整个结构体,所以,一般就把这个结构体的首地址作为线程的标识符,所以pthread_t本质上就是个地址。
线程替换
我们知道程序替换的函数是execl系列的函数,但是如果我们线程进行了程序替换,会影响其它的线程吗?
答案是影响,它会完全替换整个进程的执行映像,包括代码段、数据段和堆栈等。因此,进行程序替换会对所有线程产生影响,而不仅仅是执行替换的线程。
例如我们在一个线程中,写一个execl函数,然后主线程进行死循环,看看会发生什么:
void* threadRoutine(void* args)
{
int i = 0;
while(true)
{
cout << (char*)args << ", pid: " << getpid() << endl;
if(i++ == 5) break;
sleep(1);
}
execl("/bin/ls","ls",nullptr);
cout << "new thread quit ..." << endl;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,threadRoutine,(void*)"thread 1");
while(true)
{
sleep(1);
}
return 0;
}
可以发现execl程序替换了以后,后续的所有线程都没有再执行了。
线程分离
默认情况下,新创建的线程是joinable的,即线程退出后,需要对其进行pthread_join(线程等待) 操作,否则无法释放资源,从而造成系统泄漏。
如果不关心 线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。此时便需要线程分离.
该函数为pthread_detach,函数原型如下:
int pthread_detach(pthread_t thread);
参数为要分离的线程标识符,我们一般可以自己分离自己,使用**pthread_self()**获取自己的进程标识符,然后传入.
需要注意:当分离线程后,主线程就不能再使用pthread_join()等待,否则会等待失败。
如果主线程比新线程先退出了,那么会终止所有线程,即使线程分离了,依然会被终止。所以一般都是主线程最后再退出。
至此,linux线程控制的大部分内容就完成了,还有线程互斥和同步,我们将在下一章详细讲解,因为有很多关联的知识点,所以单独一章再讲。