【Linux】线程控制

目录

写在前面的话

线程创建pthread_create

函数的使用

线程异常

线程等待pthread_join

线程终止pthread_exit

线程替换

线程分离


写在前面的话

本文章将讲解进程的控制,包括进程的创建,进程的终止以及进程的等待,主要是以代码为主,因为很多情况下这些也只有代码可以说明问题.

在阅读本文前,最好已经清楚地了解了线程的概念,和进程的区别,这样阅读理解起来会比较轻松。

线程创建pthread_create

函数的使用

我们想要创建一个线程,需要用到pthread_create()这个函数,该函数的用法如下:

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine)(void *), void *arg);
  1. thread:指向 pthread_t 类型的指针,用于存储新创建 线程的标识符。

  2. attr:指向 pthread_attr_t 类型的指针,用于指定线程的属性,默认为 NULL 表示使用默认属性。

  3. start_routine:指向线程函数的指针 ,线程函数是新线程所要执行的函数 。线程函数的原型是 void *(*start_routine)(void *),其中参数和返回值类型可根据实际需求进行定义。

  4. 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;

我们直接运行:

所以结束线程一共有三种方法:

  1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。

  2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED((void*)-1)。

  3. 如果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线程控制的大部分内容就完成了,还有线程互斥和同步,我们将在下一章详细讲解,因为有很多关联的知识点,所以单独一章再讲。

相关推荐
C-20021 分钟前
selinux和防火墙
linux·服务器·网络
Python私教7 分钟前
Ubuntu 22 安装 NVM 详细步骤及常见问题解决方案
linux·运维·ubuntu
艾思科蓝17 分钟前
知识点归纳 | 信号处理入门
linux·卷积神经网络·实时音视频·信息与通信·信号处理·dsp开发·傅里叶分析
挥之以墨41 分钟前
【Nginx】核心概念与安装配置解释
运维·nginx
SG.xf43 分钟前
Haproxy
linux
筱小虾米3 小时前
解决SSL VPN客户端一直提示无法连接服务器的问题
运维·服务器
dntktop5 小时前
内嵌编辑器+AI助手,Wave Terminal打造终端新体验
运维
Peter_chq6 小时前
【计算机网络】多路转接之select
linux·c语言·开发语言·网络·c++·后端·select
太阳风暴6 小时前
Ubuntu-修改左Alt和Win键位置关系
linux·ubuntu·修改键盘·键盘映射