[ Linux ] 线程控制(线程创建,等待,终止)

news2024/11/28 16:38:53

在上一篇我们了解了Linux下线程的相关概念。而本篇的主要内容是线程控制。线程控制包括线程的创建,线程的终止,线程等待等问题,以及线程分离和Linux常见线程安全问题。

目录

1.线程控制

1.1POSIX线程库

1.2 创建线程

1.2.1 创建线程编码

1.2.2 代码相关解释

1.2.3 创建多个线程

1.2.4 线程ID及进程地址空间布局

1.2.5 pthread_t

1.3 线程等待

1.3.1为什么需要线程等待

1.3.2 pthread_join介绍及其编码

1.3.3 join的第二个参数 value_ptr

1.4 线程终止

1.4.1pthread_exit 介绍和编码

1.4.2 pthread_cancel 介绍和编码

1.5 线程控制总结

1.6 验证 线程异常问题


1.线程控制

线程控制和我们之前学习过的进程控制类似,包括线程创建终止等待。我们会从完成编码和验证两个方面完善线程控制。

1.1POSIX线程库

在上一篇博文我们就提到过,操作系统并没有直接提供相关的接口。而是由大多数程序员为我们开辟好了一个原生的线程库。因此我们要对线程进行控制。就要进入这个线程库。

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的。例如pthread_create,pthread_join。
  • 要使用这些函数库,要通过引入头文件<pthread.h>
  • 链接这些线程函数库时要使用编译器命令的“-lpthread”选项

1.2 创建线程

pthread_create

  • 功能:创建一个新的线程
  • 原型

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void arg);

  • 参数
    • thread:返回线程id
    • attr:设置线程的属性,attr为NULL表示使用默认属性
    • start_routine:是个函数地址,线程启动后要执行的函数(回调函数)
    • arg:传给线程启动函数的参数
  • 返回值:成功返回0,失败返回错误码
  • 错误检查:
    • 传统的一些函数是成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误
    • pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样)。而是将错误代码通过返回值返回
    • pthreads同样也提供了线程内的errno变量,以支持其他使用errno的代码。对于pthreads函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量开销更小。

1.2.1 创建线程编码

了解了线程创建的函数和参数使用方式之后,我们在代码中来体现一番:

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

void *startRoutine(void *args)
{
    while(true)
    {
        cout<<"线程正在运行......"<<endl;
        sleep(1);
    }
    return nullptr;
}


int main()
{
    //创建tid
    pthread_t tid;

    //创建线程
    int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");
    
    //主线程
    while(true)
    {
        cout<<"我是主线程,我正在运行......"<<endl;
        sleep(1);
    }
    return 0;
}

我们来查看一下当前的线程

1.2.2 代码相关解释

首先我们创建了一个线程id(tid),这个tid是一个整数,我们来看看线程id是什么?

我们打印完发现这个tid怎么这么大,这里所谓的id是什么?这个值是什么?我们到后面会说。现在我们可以先把这个数字转成16进制看看。

//将tid转乘16进制
static void printTid(const pthread_t& tid)
{
    printf("tid: 0x%x\n",tid);

}

库内我们还有一个可以获取自己id的函数pthread_self(). -- 谁掉这个函数就把线程id返回给谁

所以我们修改一下代码,当前在获取一下线程id

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

//将tid转乘16进制
static void printTid(const char *name,const pthread_t& tid)
{
    printf("%s 正在运行 ,tid: 0x%x\n",name,tid);

}
void *startRoutine(void *args)
{
    const char *name = static_cast<const char *>(args);
    while(true)
    {
        printTid(name,pthread_self());
        //cout<<"线程正在运行......"<<endl;
        sleep(1);
    }
    return nullptr;
}


int main()
{
    //创建tid
    pthread_t tid;
    //创建线程
    int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");
    //printTid(tid);
    //cout<<"tid :" <<tid<<endl;
    //主线程
    while(true)
    {
        printTid("man thread:",pthread_self());
        //cout<<"我是主线程,我正在运行......"<<endl;
        sleep(1);
    }
    return 0;
}

很明显这两个线程的tid是不一样的。

1.2.3 创建多个线程

现在我们已经学会了创建线程了,那么我们如果创建多个线程呢?

我们可以和创建多进程一样,可以打循环创建。

1.2.4 线程ID及进程地址空间布局

  1. pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
  2. 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
  3. pthread_create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴,线程库的后续操作,就是根据线程ID来操作线程的。
  4. 线程库NPTL提供了pthread_self()函数,可以获得线程自身的ID。 pthread_t pthread_self(void);

1.2.5 pthread_t

pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。

我们在vs code中查看pthread_t的类型可以发现,当前的pthread_t其实就是一个无符号长整型的整数。

1.3 线程等待

我们在主执行流(main)内创建线程之后,我们也要等待线程,类似于进程部分的父进程等待子进程。不等待的话可能会引发内存泄漏问题。

1.3.1为什么需要线程等待

为什么要有线程等待主要有两点原因:

  1. 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  2. 创建新的线程不会复用刚才退出线程的地址空间

因此有可能引发内存泄漏的问题。

1.3.2 pthread_join介绍及其编码

如何等待一个线程呢?我们可以使用pthread_join函数,首先我们先了解一下这个函数

  • 功能:等待线程结束
  • 原型:int pthread_join(pthread_t thread, void **value_ptr);
  • 参数
    • thread:线程ID
    • value_ptr:它指向一个指针,后者指向线程的返回值
  • 返回值:成功返回0,失败返回错误码。

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

//将tid转乘16进制
static void printTid(const char *name,const pthread_t& tid)
{
    printf("%s 正在运行 ,tid: 0x%x\n",name,tid);

}
void *startRoutine(void *args)
{
    const char *name = static_cast<const char *>(args);
    int cnt = 5;
    while(true)
    {
        printTid(name,pthread_self());
        //cout<<"线程正在运行......"<<endl;
        sleep(1);
        if(!(cnt--))  break;
    }
    cout<<"线程退出啦........"<<endl;
    return nullptr;
}


int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");

    sleep(10);

    pthread_join(tid,nullptr);
    return 0;
}

我们再写一个监控脚本 查看当前线程个数

while :; do ps -aL | head -1 && ps -aL | grep mythread; sleep 1; done

因此线程退出的时候必须要join,如果不join就会造成类似于进程那样的内存泄漏问题。

1.3.3 join的第二个参数 value_ptr

我们查看文档发现,join的第二个参数是一个二级指针,而且是一个输出型参数,指向的是线程的返回值。我们所写的回调函数的返回值是一个void*,如果我们返回一个void*的值,主线程可以接受的。我们使用编码来进行验证。

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


//将tid转乘16进制
static void printTid(const char *name,const pthread_t& tid)
{
    printf("%s 正在运行 ,tid: 0x%x\n",name,tid);


}
void *startRoutine(void *args)
{
    const char *name = static_cast<const char *>(args);
    int cnt = 5;
    while(true)
    {
        printTid(name,pthread_self());
        //cout<<"线程正在运行......"<<endl;
        sleep(1);
        if(!(cnt--))  
        {
            break;
            // int *p = nullptr;
            // *p = 100;//野指针问题
        }
    }
    cout<<"线程退出啦........"<<endl;
    return (void*)111;
}



int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");
    (void)n;


    void *ret = nullptr;// void* -> 字节
    pthread_join(tid,&ret);//void **retval是一个输出型参数
    cout<<"main thread join sucess , *ret:" <<(long long)ret<<endl; 

    sleep(10);
    //主线程
    while(true)
    {
        printTid("man thread:",pthread_self());
        //cout<<"我是主线程,我正在运行......"<<endl;
        sleep(1);
    }


    return 0;
}

我们能够发现主线程是可以收到线程的退出码的。

1.4 线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 从线程函数return,这种方法对于主线程不适用,从mian函数return相当于调用exit。
  2. 线程可以调用pthread_exit终止自己
  3. 一个线程可以调用pthread_cancel终止同一进程中的另一个线程

其中方法一正是我们1.3.3所提到的。这里我们再来了解剩下两个函数,线程调用pthread_exit()函数终止自己和线程调用pthread_cancel终止同进程内的另一个线程。

1.4.1pthread_exit 介绍和编码

pthread_exit函数

  • 功能:线程终止
  • 原型:void pthread_exit(void *value_ptr);
  • 参数:value_ptr:value_ptr不要指向一个局部变量
  • 返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

需要注意的是,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能是线程函数在栈上分配,因为当其他线程得到这个返回指针时线程函数已经退出了。有可能造成野指针问题。

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

//将tid转乘16进制
static void printTid(const char *name,const pthread_t& tid)
{
    printf("%s 正在运行 ,tid: 0x%x\n",name,tid);

}
void *startRoutine(void *args)
{
    const char *name = static_cast<const char *>(args);
    int cnt = 5;
    while(true)
    {
        printTid(name,pthread_self());
        //cout<<"线程正在运行......"<<endl;
        sleep(1);
        if(!(cnt--))  
        {
            break;
            // int *p = nullptr;
            // *p = 100;//野指针问题
        }
    }
    cout<<"线程退出啦........"<<endl;
    //return (void*)111;
    //pthread_exit
    pthread_exit((void*)2222);
}


int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");
    (void)n;

    void *ret = nullptr;// void* -> 字节
    pthread_join(tid,&ret);//void **retval是一个输出型参数
    cout<<"main thread join sucess , *ret:" <<(long long)ret<<endl; 

    return 0;
}

1.4.2 pthread_cancel 介绍和编码

这个方法不太常用,但是还是介绍一下

pthread_cancel

  • 功能:取消一个执行中的线程
  • 原型:int pthread_cancel(pthread_t thread);
  • 参数:
    • thread :线程ID
  • 返回值:成功返回0,失败返回错误码。

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

//将tid转乘16进制
static void printTid(const char *name,const pthread_t& tid)
{
    printf("%s 正在运行 ,tid: 0x%x\n",name,tid);

}
void *startRoutine(void *args)
{
    const char *name = static_cast<const char *>(args);
    while(true)
    {
        printTid(name,pthread_self());
        //cout<<"线程正在运行......"<<endl;
        sleep(1);
    }
    cout<<"线程退出啦........"<<endl;
    //return (void*)111;
    //pthread_exit((void*)2222);
}


int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");
    (void)n;

    sleep(3);//代表main thread对应的工作
    //3.给线程发送取消请求 如果线程是被取消的,退出结果是:-1
    pthread_cancel(tid);
    cout<<"new thread been canceled"<<endl;

    void *ret = nullptr;// void* -> 字节
    pthread_join(tid,&ret);//void **retval是一个输出型参数
    cout<<"main thread join sucess , *ret:" <<(long long)ret<<endl; 
    
    return 0;
}

我们发现返回的结果是-1,这里我们需要知道如果线程是被取消的,退出结果是:-1。

-1 是库里面给我提供的一个宏

#define PTHREAD_CANCELED ((void *) -1)

1.5 线程控制总结

至此我们了解了线程的创建,线程的等待,以及线程终止的三种方式。在线程等待中,我们要调用pthread_join函数,调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

  1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数-1(PTHREAD_ CANCELED)
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

1.6 验证 线程异常问题

至此我们了解了线程创建和线程等待,那么如果线程异常了会怎么办呢?这和线程的健壮性相关,在上篇线程介绍中我们提到过线程异常,我们当时说

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

现在我们验证一下线程异常。

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

//将tid转乘16进制
static void printTid(const char *name,const pthread_t& tid)
{
    printf("%s 正在运行 ,tid: 0x%x\n",name,tid);

}
void *startRoutine(void *args)
{
    const char *name = static_cast<const char *>(args);
    int cnt = 5;
    while(true)
    {
        printTid(name,pthread_self());
        //cout<<"线程正在运行......"<<endl;
        sleep(1);
        if(!(cnt--))  
        {
            int *p = nullptr;
            *p = 100;//野指针问题
        }
    }
    cout<<"线程退出啦........"<<endl;
    return nullptr;
}


int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");


    pthread_join(tid,nullptr);

    sleep(10);
    while(true)
    {
        printTid("man thread:",pthread_self());
        //cout<<"我是主线程,我正在运行......"<<endl;
        sleep(1);
    }
    return 0;
}

通过结果我们发现,线程如果异常了,整个进程异常退出。线程异常 == 进程异常。线程一旦退出,线程会影响其他线程。 -- 健壮性(鲁棒性)较低.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/88169.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

新库上线 | CnOpenData劳务外包企业工商注册基本信息数据

劳务外包企业工商注册基本信息数据 一、数据简介 随着我国社会主义市场经济的发展&#xff0c;劳务市场中的用工方式也朝着多样化方向演变&#xff0c;劳务外包正是现代化人力资源管理和企业生产实际结合的一种独特的新模式。 在劳务外包过程中&#xff0c;企业将人事管理的部…

Node 文件查找优先级及 Require 方法文件查找策略

Node 文件查找优先级及 Require 方法文件查找策略 一、模块规范 NodeJS对CommonJS进行了支持和实现&#xff0c;让我们在开发node的过程中可以方便的进行模块化开发&#xff1a; 在Node中每一个js文件都是一个单独的模块模块中包括CommonJS规范的核心变量&#xff1a;export…

图数据库知识点2:图思维方式

在上一个知识点中&#xff0c;我们剖析了关系型数据库、数仓湖与图数据库的差异。在本文&#xff0c;我们会着重介绍一个重要的概念——图思维方式&#xff08;Graph-thinking&#xff09;。 关于思维方式 每个人应该如何思考&#xff0c;他/她是如何思考的&#xff0c;这是一…

朝代更替中的上下五千年

《近代中国社会的新陈代谢》 关于作者 陈旭麓是著名历史学家、华东师范大学的 建校元老之一。生前是中国史学会理事、 中国现代史学会副理事长、上海地方史志 研究会副会长。著有《初中本国史》《司马迁的历史观》《近代中国社会的新陈代谢》 《浮想录》等。 关于本书 这本…

Qt / Qml 中捕获(中文)输入法事件(按下 提交)

【写在前面】 最近工作中遇到一个奇怪的问题&#xff1a; 本来想在 TextEdit ( QTextEdit ) 中捕获一下键盘按键按下的事件。 然而&#xff0c;当输入法为英文时( 正常输入字符 )&#xff0c;可以捕获到按键事件&#xff0c;但当我切换到中文时&#xff0c;弹出输入法选框后&am…

web前端网页设计期末课程大作业:企业网页主题网站设计——舞蹈培训11页HTML+CSS+JavaScript

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

从零开始,教你写一个串口调试助手

摘要&#xff1a;相信很多小伙伴都没接触过QT&#xff0c;如果想用QT写一个调试助手&#xff0c;首先是要会一点C语法。只要能看得懂C的代码&#xff0c;就能很快的写一个串口调试助手。 下面先推荐两个视频教程&#xff0c;感兴趣的可以看一看&#xff01; 1、B站Jomse工 B站…

项目建议书怎么写?

Why 为什么需要项目建议书? 行政、国企、事业单位间的买卖需要按国家规定的“政府采购法”进行(个体户,私企间的买卖不需要按“政府采购法”进行),不是个人的消费,有一整套流程,《项目建议书》这套流程中的产物之一。 即国家规定,纳税人的钱不能乱花,买之前要想好买…

南卡和Snowkids电容笔哪款更值得入手?口碑最佳的国产电容笔

随着苹果推出了Apple Pencil之后后&#xff0c;平替电容笔的产品也随之出现了不少&#xff0c;其中大部分都是没很高知名度的牌子&#xff0c;价格也是五花八门&#xff0c;有便宜的也有昂贵的&#xff0c;让消费者有了更多的选择&#xff0c;对于第一次购买电容笔的小白来说但…

【Python】【期末复习题】

文章目录一、单选题&#xff08;20分&#xff09;二、判断题&#xff08;10分&#xff09;三、填空题&#xff08;10分&#xff09;四、问答题&#xff08;共30分&#xff0c;6题&#xff0c;每题5分&#xff09;五、程序题&#xff08;3题&#xff0c;每题10分&#xff0c;共3…

WIFI智能电子标牌的优势

一、与市面产品对比有什么优化升级? 使用环境标准wifi通信&#xff0c;避免单独安装基站&#xff0c;减少部署的麻烦。 二、刷新一次消耗多少电量&#xff0c;平均每秒消耗多少? 0.09879mAh&#xff0c;每秒平均电流105uA。 三、充满电需要多久&#xff0c;充一次电可以用…

【JS020】原生JS实现AJAX

日期&#xff1a;2022年12月14日 作者&#xff1a;Commas 签名&#xff1a;(ง •_•)ง 积跬步以致千里,积小流以成江海…… 注释&#xff1a;如果您觉得有所帮助&#xff0c;帮忙点个赞&#xff0c;也可以关注我&#xff0c;我们一起成长&#xff1b;如果有不对的地方&#x…

TFT显示模组是什么?tft显示模组有什么功能?

TFT显示模组的出现&#xff0c;让手机屏幕成为一种刚需。但随着技术的发展&#xff0c;越来越多的厂商开始推出折叠屏手机&#xff0c;这也意味着手机屏幕的形态正在发生变化。在此背景下&#xff0c;折叠屏手机市场迎来爆发期。根据中国信通院数据&#xff0c;2020年上半年&am…

Kubernetes包管理器Helm的本质

“本质”类的文章&#xff0c;通常很难带流量。而且写起来非常吃力。那我为什么还要写&#xff1f;写作是对自己的锻炼。写作是让自己的思想更有深度的一种有效方式。如果你觉得这篇文章对你有帮助&#xff0c;也你麻烦你转发这篇文章&#xff0c;这是对我的帮助。谢谢。Kubern…

socket.io 使用protobuf 协议发送消息

一、服务端 1、maven引入netty-socketio <dependency><groupId>com.corundumstudio.socketio</groupId><artifactId>netty-socketio</artifactId><version>1.7.22</version></dependency> 2、服务端java代码 Service public…

艾美捷内皮细胞生长添加剂不同研究工具推荐

艾美捷Relia Tech内皮细胞生长添加剂可用于培养没有饲养细胞的杂交瘤细胞&#xff08;Pintus&#xff0c;1983&#xff09;。这些脑提取物中的活性因子已被鉴定为aFGF的变体&#xff08;Burgesse&#xff0c;1985&#xff09;。该ECGF产品补充了生理浓度的人rec.VEGF-A&#xf…

初识 Markdown编辑器

这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注…

真兰仪表通过深交所注册:拟募资17.4亿 上半年净利下降27%

雷递网 雷建平 12月13日上海真兰仪表科技股份有限公司&#xff08;简称&#xff1a;“真兰仪表”&#xff09;日前通过注册&#xff0c;准备在深交所创业板上市。真兰仪表计划募资17.4亿元。其中&#xff0c;7.76亿用于真兰仪表科技有限公司燃气表产能扩建项目&#xff0c;6.12…

NFA的确定化

一、实验目的 &#xff08;1&#xff09;通过本次实验&#xff0c;加深对正则表达式、NFA、DFA及其识别的语言的理解&#xff1b; &#xff08;2&#xff09;掌握从NFA到DFA的转换&#xff0c;以及用子集法把NFA转换成DFA理论&#xff0c;编程实现将NFA&#xff08;不确定有穷…

SAP 事务代码BD20不能处理状态为51的IDoc

SAP 事务代码BD20不能处理状态为51的IDoc 对于SAP IDoc相关的事务代码比如WE02,WE19,BD87等都比较熟悉&#xff0c;因为使用的比较多。但是对于事务代码BD20却很少使用。 笔者在近期的一个项目上&#xff0c;听到客户的global team有使用该事务代码&#xff0c;设置成了一个job…