【Linux】线程概念 && 线程控制(接口详谈)&& 僵尸线程的产生和解决


线程概念

1. 线程和进程的关系

在Linux中,除了进程的概念,其实还有一个 轻量级进程(LWP) 的概念,也就是线程。(Linux操作系统内核中是没有线程概念的)

  • 线程依附进程存在,没有进程就没有线程;
  • 多线程的出现是为了提高进程的运行效率;
  • 线程也可以称为执行流,其也在执行用户代码;
  • 进程是资源分配的基本单位,线程是调度的基本单位;

2. 重识PID,认识TGID

在之前讲解 进程概念 的博客中,说到:

  • 一个进程是OS通过 PCB进程控制块 来进行管理的;
  • 在Linux中,PCB在底层上是通过一个名为task_struct的结构体组织;
  • task_struct中,有这个进程的PID(进程标识符)
  • 而且每个进程的PID都是不同的(以此区分);

【图示】
在这里插入图片描述

但其实,上述的概念是不正确的,或者说是有偏颇的。有了多线程后,需要重新更新一些概念:

  • 一般来说,PCB(进程控制块)结构包含了许多 TCB(线程控制块)

  • 每个进程至少有一个线程,就是 主线程(执行main函数的执行流)

    我们之前所变编写的代码,全部是只有主线程的单线程进程,因此用ps命令或者getpid()等接口查询进程id时,内核返回给我们的PID也正是这个TGID;

  • 对于多线程进程,除了主线程,其余线程被称为 工作线程(程序员创建的线程)

  • 在Linux操作系统中没有线程概念(只有轻量级进程),没有专门的线程TCB控制块,而是用进程的PCB去模拟线程这个概念;

因此我们可以简单的认为,在Linux中

  • 轻量级进程(lwp)= 线程每个线程的ID就是PID
  • 轻量级进程组,也叫线程组(thread group)= 进程每个进程的ID就是TGID
  • 每一个线程都有一个task_struct结构体来描述,且每个线程都有自己的标识符(PID);
  • 主线程的 PID = TGID;
  • 一个进程内的不同线程,其TGID相同,因为同属于一个线程组;
  • 同一个进程的所有线程公用同一块虚拟地址空间;

【图示】
在这里插入图片描述


3. 一个进程内线程的关系

一个线程的PCB中的除了PID、TGID、内存指针,还有许多,对于同一个进程下的线程来说,有些内容是共享的,有些内容是独有的;

  • 一个进程下的不同线程,指向同一块虚拟内存;
  • 每个线程 在虚拟内存的共享区 都有一段自己的空间,存放了每个线程独有内容;

共享内容:
在这里插入图片描述

独有内容:

在这里插入图片描述


4. 线程优缺点

在这里插入图片描述
在这里插入图片描述


线程控制

  • 下列线程控制接口的头文件均为<pthread.h>
  • 注意涉及线程的代码,编译时需添加参数 -lpthread

1. 线程创建

原型:pthread_create()

(1)原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);

(2)参数:

  • thread:一个出参,获取线程标识符;
    在这里插入图片描述
  • attr:设置线程的属性,一般值为NULL使用默认属性即可;
    在这里插入图片描述
  • start_routine:线程的入口函数地址,一般由程序员自定义函数并传参;
    在这里插入图片描述
  • arg:传给上述线程启动函数start_routine的参数;
    在这里插入图片描述
    在这里插入图片描述

(3)返回值:
在这里插入图片描述

接口测试

几条Linux指令:

  • ps -aux | grep 源文件名可以查看该进程的PID(这里的PID其实就是主线程的PID,也就是进程的TGID);
    在这里插入图片描述

  • pstack + PID 可以查看该进程的调用堆栈;
    在这里插入图片描述

  • top -H -p PID 可以动态观察该进程的全部线程信息;
    在这里插入图片描述


(1)创建多线程进程,工作线程启动函数参数传递临时变量(错误示范);

【测试源码】

//Test_1:传递临时变量作为参数给工作线程入口函数

#include<iostream>
#include<pthread.h>
#include<unistd.h>

using namespace std;

// 工作线程的入口函数
void* pthread_start(void* arg)
{
    int* i = (int*)arg;
    // 工作线程死循环不退出
    cout << "I'm a work thread!" << *i  << endl;
    sleep(1); 
}
int main()
{
    for(int i = 0; i < 5; ++i)
    {
        //1.创建一个工作线程
        pthread_t thread;
        
        //这种直接将main的栈上的局部变量作为参数传递给工作线程非常不安全
        //有可能当变量i跳出for循环时,变量i地址的访问已不合法
        //但工作线程仍会访问局部变量i
        int ret = pthread_create(&thread, NULL, pthread_start, (void*)&i);
        if(ret < 0)
        {
            cout << "pthread_create Error!" << endl;
            return 0;
        }
        // 如果让主线程每隔1s再创建新线程,这样新线程就可以在i还未更新的情况得到CPU资源
        //sleep(1);
    }
    //2.主线程循环不退出,观察线程信息
    while(1)
    {
        cout << "I'm a main thread!" << endl;
        sleep(1);
    }
    return 0;
}

【测试结果】
在这里插入图片描述

【修改测试】
在这里插入图片描述
但是!,上述代码有非常严重的隐藏,那就是为工作线程的入口函数,传递了临时变量

这种直接将main的栈上的局部变量作为参数传递给工作线程非常不安全;

上述用例中,主线程的变量i 跳出for循环后,对变量i 的访问已不合法,但工作线程仍可以通过参数地址访问(因为工作线程无法得知该空间是否有效);

【结论】

  • 对于工作线程入口函数的参数,我们可以传递:main()函数中保证有效的临时变量该文件全局变量堆上动态开辟的变量
  • 更推荐使用堆上变量作为参数传递给工作线程入口函数(下面测试);

(2)创建多线程进程,工作线程启动函数参数传递堆上变量

【测试源码】

//Test_2:传递堆参数给工作线程入口函数

#include<iostream>
#include<pthread.h>
#include<unistd.h>

using namespace std;

// 工作线程的入口函数
void* pthread_start(void* arg)
{
    int* i = (int*)arg;
    // 工作线程死循环不退出
    cout << "I'm a work thread!" << *i  << endl;
        
    //堆上开辟的变量在工作线程使用完后记得释放
    //因为主线程并不清楚工作线程什么时候用完该变量,因此只能由工作线程释放    
    delete i;
    sleep(1);
    return NULL;
}
int main()
{
    for(int i = 0; i < 5; ++i)
    {
       //1.创建一个工作线程
        pthread_t thread;
        
        //这里在堆上动态开辟
        int* p = new int;
        *p = i;
        int ret = pthread_create(&thread, NULL, pthread_start, (void*)p);

        if(ret < 0)
        {
            cout << "pthread_create Error!" << endl;
            return 0;
        }
        //这里不让主线程阻塞等待工作线程,所有工作线程抢占执行
        //sleep(1);
    }
    //2.主线程循环不退出,观察线程信息
    while(1)
    {
        cout << "I'm a main thread!" << endl;
        sleep(1);
    }
    return 0;
}

【输出结果】:各线程并不是按照顺序执行(抢占执行),但打印的数值并不重复;
在这里插入图片描述
【结论】

  • 若给工作线程的入口函数传递了堆上变量,一定要 在工作线程内释放该堆空间参数
  • 因为主线程并不清楚工作线程什么时候用完该变量,因此只能由工作线程释放 ;

2. 线程终止

  • 一般用来终止工作线程,而不是主线程(主线程终止,整个进程退出)

原型-1:pthread_exit()

(1)原型:void pthread_exit(void *value_ptr)

  • 作用:谁调用谁退出
  • 该接口一般由工作线程自己调用该函数后终止自己;

(2)参数:value_ptr

  • 一个出参,线程退出时,传递给等待线程的推出信息;
  • value_ptr不要指向一个局部变量,一定是在堆上开辟的空间;
  • 因为当其它线程得到这个返回指针时线程函数已经退出了;

(3)返回值:
在这里插入图片描述

测试-1

创建一个工作线程,测试接口pthread_exit();

【测试源码】

#include<iostream>
#include<pthread.h>
#include<unistd.h>  //sleep()函数使用
using namespace std;

// 测试线程退出接口pthread_exit():谁调用谁退出

// 工作线程的入口函数
void* pthread_start(void* arg)
{
    int count = 3;
    while(count--)
    {
        // 工作线程死循环不退出
        cout << "I'm a work thread!" << endl;
        sleep(1);
    }
    // 调用退出接口
    pthread_exit(NULL);
    // 若成功退出,后面代码该线程不会执行
    while(1)
    {
        cout << "work thread still run, pthread_exit() Error!" << endl;
        sleep(1);
    }
}

int main()
{
    //1.创建一个工作线程
    pthread_t thread;
    int ret = pthread_create(&thread, NULL, pthread_start, NULL);
    if(ret < 0)
    {
        cout << "pthread_create Error!" << endl;
        return 0;
    }
    //2.主线程循环不退出,观察线程信息
    while(1)
    {
        cout << "I'm a main thread!" << endl;
        sleep(1);
    }
    return 0;
}

【测试结果】
在这里插入图片描述


【总结】

  • 任何一个线程中调用exit函数都会导致进程结束。进程一旦结束,那么进程中的所有线程都将结束;
  • 主线程中,main函数里直接return或是调用了exit函数,则主线程退出,且整个进程也会终止(即进程中的所有线程终止);
  • 主线程中调用pthread_exit, 则仅主线程结束,进程不会结束,进程内的其他线程也不会结束;

原型-2:pthread_cancel()

(1)原型:int pthread_cancel(pthread_t thread)

  • 作用:一个线程(主线程或工作线程)调用该函数可终止同一进程的另一个线程或自己线程
  • 该线程被其他线程突然终止,不会有退出信息(因此可能导致僵尸线程);
  • 一个线程使用该函数终止自己的线程,不会像 pthread_exit 函数立刻终止;
  • 主线程使用该函数终止自己的线程,是有一个过程的;

(2)参数:thread:被终止线程的线程标识符(线程ID);

(3)返回值:
在这里插入图片描述

测试-2

  • 接口pthread_t pthread_self(void) 获取自己的线程标识符;

(1)工作线程调用pthread_cancel()接口终止自己;
【测试源码】

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>

using namespace std;

// 测试线程退出接口pthread_cancel(pthread_t thread):通过线程标识符结束任意线程
//
// case_1.工作线程自己调
// tip:接口pthread_t pthread_self(void) 获取自己的线程标识符

// 工作线程的入口函数
void* pthread_start(void* arg)
{
    int count = 5;
    while(count--)
    {
        // 工作线程死循环不退出
        cout << "I'm a work thread!" << endl;
        sleep(1);
    }
    // 调用退出接口退出自己线程
    pthread_cancel(pthread_self());
    // 若成功退出,后面代码该线程不会执行
    while(1)
    {
        cout << "work thread still run, this pthread not exit now!" << endl;
        // sleep(1);
    }
}

int main()
{
    //1.创建一个工作线程
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, pthread_start, NULL);
    if(ret < 0)
    {
        cout << "pthread_create Error!" << endl;
        return 0;
    }
    //2.主线程循环不退出,观察线程信息
    while(1)
    {
        printf("I'm a main thread!\n");  //注意,这里不能使用cout打印!否则看不到主线程的打印内容
        sleep(1);
    }
    return 0;
}

【测试结果】
在这里插入图片描述
【Tip】cout不是线程安全的,printf是线程安全的 !涉及多线程的打印最好使用printf()接口;

上述例子中,如果将主线程的打印语句修改为cout << I'm a main thread!" << endl;
在这里插入图片描述

我们就会发现,该程序的结果发生问题:
在这里插入图片描述
在这里插入图片描述

原因分析:

  • 在多线程环境下,I/O流对于不同线程来说也是一种互斥资源;
  • cout输出流内部并没有实现线程安全机制,因此当主线程和工作线程均为cout输出流打印时,会造成线程访问输出流异常;

解决方案:

  • 让主线程或工作线程的任意一个打印方式改为printf()接口即可;
  • 因为printf是线程安全的,也就是自己做了线程同步的处理
  • 最好所有线程均采用printf()接口方式,因为std::coutprintf混用,在多线程环境下可能会导致coredump;

若想了解详细原因可以参考这篇讲解:C/C++的流(stream)对象c++ cout 多线程

【结论-1】

  • 工作线程调用pthread_cancel()可以关闭自己,但并不是立即像pthread_exit()一样立即执行,而是有一个过程,该过程(很短)中工作线程仍在运行

(2)主线程调用pthread_cancel()接口终止工作线程;

【测试源码】

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>

using namespace std;

// 测试线程退出接口pthread_cancel(pthread_t thread):通过线程标识符结束任意线程
//
// case_2.主线程调,结束工作线程
// 可以成功结束。

// 工作线程的入口函数
void* pthread_start(void* arg)
{
    while(1)
    {
        // 工作线程死循环不退出
        cout << "I'm a work thread!" << endl;
        sleep(1);
    }
}
int main()
{
    //1.创建一个工作线程
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, pthread_start, NULL);
    if(ret < 0)
    {
        cout << "pthread_create Error!" << endl;
        return 0;
    }
    
    getchar();   
    // 作用:让主线程阻塞等待,当得到用户输入的字符后才能继续向下执行
    // 该语句影响主线程后续代码执行,但完全不影响工作线程
    
	pthread_cancel(tid);  //终止线程ID为tid的工作线程

    //2.主线程循环不退出,观察线程信息
    while(1)
    {
        // cout << "I'm a main thread!" << endl;
        printf("I'm a main thread!\n");
        sleep(1);
    }
    return 0;
}

【测试结果】
在这里插入图片描述
查看输入字符前的堆栈情况:
在这里插入图片描述
输入字符后的堆栈情况:
在这里插入图片描述
【结论-2】

  • 通过主线程调用pthread_cancel()关闭一个工作线程,由于该工作线程是突然被关闭的,并没有产生任何退出信息,因此工作线程立即正常退出;

(3)主线程调用pthread_cancel()接口终止主线程自己;

【测试源码-1】工作线程死循环,主线程调用pthread_cancel接口后,仍有其他代码;

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>

using namespace std;

// 测试线程退出接口pthread_cancel(pthread_t thread):通过线程标识符结束任意线程
//
// case_3.主线程调,结束自己线程
// 3.1 工作线程循环,主线程调用结束接口后还有代码
// 3.2 工作线程循环,。。。。。。。。。。没有代码
// 3.3 工作线程可以自己退出。

// 工作线程的入口函数
void* pthread_start(void* arg)
{
    while(1)
    {
        // 工作线程死循环不退出
        printf("I'm a word thread!\n"); 
        sleep(1);
    }
}
int main()
{
    //1.创建一个工作线程
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, pthread_start, NULL);
    if(ret < 0)
    {
        cout << "pthread_create Error!" << endl;
        return 0;
    }
    getchar();   //作用:让主线程阻塞等待,当得到用户输入的字符后才能继续向下执行
    
    pthread_cancel(pthread_self());  //主线程终止自己线程
    //2.主线程循环不退出,观察线程信息
    while(1)
    {
        printf("I'm a main thread ! \n");
        sleep(1);
    }
    return 0;
}

【测试结果-1】
在这里插入图片描述
【结果分析-1】

getchar()前,观察该进程堆栈情况:
在这里插入图片描述
主线程调用cancel关闭自己后,发现:

  • 该进程变为僵尸进程;
  • 进程堆栈调用情况无法查到;
  • 查看进程的线程情况,发现主线程为僵尸状态,工作线程也存在
    在这里插入图片描述

所有主线程调用cancel接口关闭自己线程后,一定会形成僵尸线程?再观察下面两种测试;


【测试源码-2】工作线程非死循环,主线程调用pthread_cancel接口后,仍有其他代码;

在【测试源码-1】的基础上,改变工作线程的while(1)循环条件;
在这里插入图片描述

【测试结果-2】
在这里插入图片描述
【结果分析-2】

实际上,在main函数调用cancel接口之前,该进程正常运行两个线程:
在这里插入图片描述
当main函数调用cancel接口之后,该进程退出,但工作线程仍在运行,主线程仍为僵尸进程
在这里插入图片描述
但当工作线程执行完毕后,该进程立马正常结束:
在这里插入图片描述


【测试源码-3】工作线程死循环,主线程调用pthread_cancel接口后,不执行任何代码;

在【测试源码-1】的基础上,注释掉主线程cancel之后的代码;
在这里插入图片描述

【测试结果-3】虽然工作线程死循环,但仍被强制退出:
在这里插入图片描述
【结果分析-3】

  • 主线程执行cancel接口时是有一个过程的,虽然这个过程很短暂;
  • 调用cancel接口后只有return 0;一条语句,因此这个过程中,主线程直接调用了return 0退出该进程;
  • 也就是说该程序根本没有执行cancel接口,而是通过return正常退出

【总结】

  • pthread_cancel()接口执行是有一个过程,该过程(很短)中被关闭的线程仍在运行;
  • 主线程调用pthread_cancel()接口关闭工作线程,被关闭的工作线不会产生退出状态信息;
  • 主线程调用pthread_cancel()接口直接关闭自己,而工作线程正常运行产生的退出状态信息,得不到任何线程的回收,因此就会产生僵尸线程

3. 线程等待

为什么要线程等待?

(1)线程本质:

在Linux中,新建的线程并不是在原先的进程中,而是系统通过 一个系统调用clone()。该系统copy了一个和原先进程完全一样的进程,并在这个进程中执行线程函数。不过这个copy过程和fork不一样。 copy后的进程和原先的进程共享了所有的变量,运行环境。所以线程是共享全局变量和环境的。

(2)僵尸线程产生原因:

  • 线程被创建出来的默认属性是joinable属性,表示该线程退出时,依赖其他线程回收资源(线程的共享区空间之类);

  • 若主线程先退出,那么工作线程的退出状态信息无法被回收,那么就会产生僵尸线程;

  • 僵尸线程危害:

    僵尸线程属于已经退出的线程,但其空间没有被释放,仍然在进程的地址空间内,需要其他线程释放;
    创建新的线程不会复用刚才退出线程的地址空间,因此若所有空间不回收,会造成内存溢出;

  • 为什么要使用pthread_join?

(3)线程等待的作用:

  • 主线程调用pthread_join接口,用来等待一个线程结束,避免出现僵尸线程;
  • 若等待的线程没有退出,当前线程阻塞;

原型: pthread_join()

(1)原型:int pthread_join(pthread_t thread, void **value_ptr);

  • 作用:用来等待一个线程结束,以此避免僵尸线程的出现;
  • 这是一个阻塞调用接口,若等待的线程没有退出,当前线程阻塞,直至线程退出;

(2)参数:

  • thread:被终止线程的线程标识符(线程ID);
  • value_ptr:它指向一个指针,指向退出线程的退出信息;
    在这里插入图片描述

(3)返回值:
在这里插入图片描述

测试

主线程退出前先调用pthread_join回收工作线程;

【测试源码】

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
using namespace std;

// 测试线程等待pthread_join(pthread_t thread,void** retval);
//
// case_3.主线程调,结束自己线程

// 工作线程的入口函数
void* pthread_start(void* arg)
{
    // 工作线程
    int count = 30;
    while(count--)
    //while(1)
    {
        // 工作线程死循环不退出
        printf("I'm a word thread!\n");
        sleep(1);
    }
}

int main()
{
    //1.创建一个工作线程
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, pthread_start, NULL);
    if(ret < 0)
    {
        cout << "pthread_create Error!" << endl;
        return 0;
    }
    getchar();   //作用:让主线程阻塞等待,当得到用户输入的字符后才能继续向下执行
    
    // 在主线程调用cancel接口退出之前,先让主线程调用等待接口
    pthread_join(tid, NULL);

    cout << "main use pthread_cancel()" << endl;   
    pthread_cancel(pthread_self());  //主线程终止自己线程
    sleep(1);
    cout << "pthread_cancel error !" << endl;  
    while(1)
    {
        printf("I'm a main thread! \n");
        sleep(1);
    }
    return 0;
}

【测试结果】:不会产生僵尸线程

在这里插入图片描述


4. 线程分离

什么是线程分离?

在这里插入图片描述

原型: pthread_detach()

(1)原型:int pthread_detach(pthread_t thread);

  • 作用:将某线程设置分离属性
  • 线程组内其他线程对目标线程进行分离,也可以是线程自己分离;

(2)参数:thread:被终止线程的线程标识符(线程ID);

(3)返回值:
在这里插入图片描述


  • 8
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值