Linux系统编程:线程控制

目录

一. 线程的创建

1.1 pthread_create函数

1.2 线程id的本质

二. 多线程中的异常和程序替换

2.1 多线程程序异常

2.2 多线程中的程序替换

三. 线程等待

四. 线程的终止和分离

4.1 线程函数return

4.2 线程取消 pthread_cancel

4.3 线程退出 pthread_exit

4.4 线程分离 pthread_detach 

五. 总结


一. 线程的创建

1.1 pthread_create函数

函数原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(start_routine)(void*), void *args)

函数功能:创建新线程

函数参数:

        thread -- 输出型参数,用于获取新线程的id

        attr -- 设置线程属性,一般采用nullptr,表示为默认属性

        start_routine -- 新创建线程的入口函数

        args -- 传入start_routine函数的参数

返回值:成功返回0,失败返回对应错误码

关于pthread系列函数的错误检查问题:

  • 一般的Linux系统调用相关函数,都是成功返回0,失败返回-1。
  • 但函数pthread系列函数不是,这些函数都是成功返回0,失败返回错误码,不对全局错误码进行设置。

代码1.1演示了如何通过pthread_create函数创建线程,在主函数中,分别以%lld和%x的方式输出子线程id,图1.1为代码的运行结果。

代码1.1:创建线程

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

// 新建线程的入口函数
void *threadRoutine(void *args)
{
    while(true)
    {
        std::cout << (char*)args << std::endl;
        sleep(1);
    }

    return nullptr;
}

int main()
{
    pthread_t tid;   // 接收子线程id的输出型参数

    // 调用pthread_create函数创建线程
    // tid接收新线程的id,nullptr表示新线程为默认属性
    // 新线程的入口函数设为threadRoutine,参数为"thread 1"
    int n = pthread_create(&tid, nullptr, threadRoutine, (char*)"thread 1");

    if(n != 0)  // 检验新线程是否创建成功
    {
        std::cout << "error:" << strerror(n) << std::endl;
        exit(1);
    }

    while(true)
    {
        printf("main thread, tid = %lld 0x%x\n", tid, tid);
        sleep(1);
    }

    return 0;
}
图1.1  代码1.1的运行结果

1.2 线程id的本质

如1.2所示,在Linux的线程库pthread中,提供了用于维护每个线程的属性字段,包括描述线程的结构体struct pthread、线程的局部存储、线程栈等,用于对每个线程的维护。

每个线程在线程库中用于维护它的属性字段的起始地址,就是这个线程的id,换言之,线程id就是动态库(地址空间共享区)的一个地址,Linux为64位环境,因此,代码1.1输出的线程id会很大,这个值就对应地址空间共享区的位置。

为了保证每个线程的栈区是独立的,Linux采用的方法是线程栈在用户层提供,这样每个线程都会在动态线程库内部分得一块属于自身的“栈区”,这样就可以保证线程栈的独立性,而主线程的栈区,就使用进程地址空间本身的栈区。

Linux保证线程栈区独立性的方法: 

  • 子线程的栈区在用户层提供。
  • 主线程栈区采用地址空间本身的栈区。

线程id的本质:地址空间共享区的一个地址。

图1.2  线程id的图解

二. 多线程中的异常和程序替换

2.1 多线程程序异常

在多线程程序中,如果某个线程在执行期间出现了异常,那么整个进程都可能会退出,在多线程场景下,任意一个线程出现异常,其影响范围都是整个进程

如代码2.1创建了2个子线程,其中threadRun2函数中人为创造除0错误引发异常,发现整个进程都退出了,不会出现只有一个线程终止的现象。

结论:任意一个线程出现异常,其影响范围都是整个进程,会造成整个进程的退出。

代码2.1:多线程程序异常

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

void *threadRoutine1(void *args)
{
    while(true)
    {
        std::cout << (char*)args << std::endl;
        sleep(1);
    }
    return nullptr;
}

void *threadRoutine2(void *args)
{
    while(true)
    {
        std::cout << "thread 2, 除0错误!" << std::endl;
        int a = 10;
        a /= 0; 
    }
    return nullptr;
}

int main()
{
    pthread_t tid1, tid2;

    //先后创建线程1和2
    pthread_create(&tid1, nullptr, threadRoutine1, (void*)"thread 1");
    sleep(1);
    pthread_create(&tid2, nullptr, threadRoutine2, (void*)"thread 2");

    while(true)
    {
        std::cout << "main thread ... ... " << std::endl;
        sleep(1);
    }
    
    return 0;
}
图2.1  代码2.1的运行结果

2.2 多线程中的程序替换

与多线程中线程异常类似,多线程中某个线程如果进行了程序替换,那么并不会出现这个线程去运行新的程序,其他线程正常执行原来的工作的情况,而是整个进程都被替换去执行新的程序。

代码2.2在threadRoutine1函数中通过execl去执行系统指令ls,运行代码我们发现,在子线程中进行程序替换后,主线程也不再继续运行了,进程执行完ls指令,就终止了。

结论:多线程程序替换是整个进程都被替换,而不是只替换一个线程。

代码2.2:多线程程序替换

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

void *threadRoutine1(void *args)
{
    while(true)
    {
        std::cout << (char*)args << std::endl;
        execl("/bin/ls", "ls", nullptr);   // 子线程中进行程序替换
        exit(0);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    
    // 创建线程
    int n = pthread_create(&tid, nullptr, threadRoutine1, (void*)"thread 1");
    if(n != 0)  
    {
        // 检验线程创建成功与否
        std::cout << strerror(n) << std::endl;
        exit(1);
    }

    while(true)
    {
        std::cout << "main thread" << std::endl;
        sleep(1);
    }

    return 0;
}
图2.2  代码2.2的运行结果

三. 线程等待

线程等待与进程等待类似,主线程需要等待子线程退出,以获取子线程的返回值。如果主线程不等待子线程,而主线程也不退出,那么子线程就会处于“僵尸状态”,其task_struct一直得不到释放,引起内存泄漏。

  • 通过pthread_join函数,可以实现对线程的等待。
  • 线程等待只能是阻塞等待,不能非阻塞等待

pthread_join函数 -- 等待线程

函数原型:int pthread_join(pthread_t thread, void **ret);

函数参数:

        thread -- 等待线程的id

        ret -- 输出型参数,获取线程函数的返回值

返回值:成功返回0,失败返回错误码

在代码3.1中, 线程函数threadRoutine中在堆区new了5个int型数据的空间,并赋值为1~5,线程函数返回指向这块堆区资源的指针,主线程等待子线程退出,主线程可以看到这块资源。注意线程函数返回值的类型为void*,使用返回值的时候要注意强制类型转换。

代码3.1:pthread_join线程等待

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

void *threadRoutine(void *args)
{
    std::cout << (char*)args << std::endl;
    int *pa = new int[5];
    for(int i = 0; i < 5; ++i)
    {
        pa[i] = i + 1;
    }
    return (void*)pa;
}

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

    int *pa = nullptr;
    // 等待线程退出,pa接收线程函数返回值
    pthread_join(tid, (void**)&pa);
    
    // 获取线程函数返回值指向的空间内的资源
    std::cout << "thread exit" << std::endl;
    for(int i = 0; i < 5; ++i)
    {
        printf("pa[%d] = %d\n", i, pa[i]);
    }

    delete[] pa;
    
    return 0;
}
图3.1  代码3.1的运行结果

四. 线程的终止和分离

可以实现线程终止的方法有:

  • 线程函数return。
  • 由另一个线程将当前线程取消pthread_cancel。
  • 线程退出pthread_exit。

4.1 线程函数return

pthread_create函数的第三个参数start_routine为线程函数指针,新创建的线程就负责执行这个函数,如果这个函数运行完毕return退出,那么,线程就退出了。

但是这种方法对主线程不适用,如果主线程退出,就是进程终止了,全部线程都会退出

结论:如果线程函数return,那么线程就退出了,但主线程return进程就退出了,不适用这种退出方式。

线程函数接收一个void*类型的参数,返回void*类型参数,如果线程函数运行到了return,那么这个线程就退出了,如代码3.1中的threadRoutine,就是采用return来终止线程的。

代码4.1验证了主线程退出的情况,设定线程函数为死循环IO输出,但是主线程在创建完子线程sleep(2)之后return,发现线程函数并没有继续运行,证明了主线程退出不适用于return这种方法来终止。

代码4.1:验证主线程不能通过return退出

// 线程函数死循环
void *threadRoutine1(void *args)
{
    while(true)
    {
        std::cout << (char*)args << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    
    // 创建线程
    int n = pthread_create(&tid, nullptr, threadRoutine1, (void*)"thread 1");
    std::cout << "main thread" << std::endl;
    sleep(2);   // 主线程sleep 2s后退出

    return 5;
}
图4.1 代码4.1的运行结果

4.2 线程取消 pthread_cancel

pthread_cancel函数可用于通过指定线程id,来取消线程。

pthread_cancel -- 取消线程

函数原型:int pthread_cancel(pthread_t thread)

函数参数:thread -- 被取消的线程的id

返回值:成功返回0,不成功返回非0的错误码

一般而言,采用主线程取消子线程的方式来取消线程,一个线程取消自身也是可以的,但一般不会这样做,pthread_cancel(pthread_self()) 可用于某个线程取消其自身,其中pthread_self函数的功能是获取线程自身的id。

  • pthread_self函数 -- 获取线程自身的id。

如果一个线程被取消了,那么就无需在主线程中通过pthread_join对这个线程进行等待,但如果使用了pthread_join对被取消的线程进行等待,那么pthread_join的第二个输出型参数会记录到线程函数的返回值为-1。

结论:如果一个线程被pthread_cancel了,那么pthread_join会记录到线程函数返回(void*)-1。 

在代码4.2中,通过pthread_cancel函数,取消子线程,然后pthread_join等待子线程,输出强转为long long类型的返回值ret,记录到ret的值为-1。

代码4.2:取消子线程并等待取消了的子线程

// 线程函数
void *threadRoutine1(void *args)
{
    while(true)
    {
        std::cout << (char*)args << std::endl;
        sleep(1);
    }
    return (void*)10;
}

int main()
{
    pthread_t tid;
    
    // 创建线程
    pthread_create(&tid, nullptr, threadRoutine1, (void*)"thread 1");
    std::cout << "main thread" << std::endl;
    sleep(2);   

    pthread_cancel(tid);   // 取消id为tid的子线程

    void *ret = nullptr;
    int n = pthread_join(tid, &ret);    // 等待已经取消的线程退出
    
    std::cout << "ret : " << (long long)ret << std::endl;

    return 0;  
}
图4.2 代码4.2的运行结果

4.3 线程退出 pthread_exit

pthread_exit 函数在线程函数中,可用于指定线程函数的返回值并退出线程,与return的功能基本完全相同,注意,exit不可用于退出线程,在任何一个线程中调用exit,都在让整个进程退出。

pthread_exit 函数 -- 让某个线程退出

函数原型:void pthread_exit(void *ret

函数参数:ret -- 线程函数的退出码(返回值)

代码4.3在线程函数中调用pthread_exit终止线程,指定返回值为(void*)111,在主线程中等待子线程,并将线程函数返回值存入ret中,输出(long long)ret的值,证明子线程返回(void*)111。

代码4.3:通过pthread_exit终止线程

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

// 线程函数
void *threadRoutine1(void *args)
{
    int count = 0;
    while(true)
    {
        std::cout << (char*)args << ", count:" << ++count << std::endl;
        if(count == 3) pthread_exit((void*)111);
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    
    // 创建线程
    pthread_create(&tid, nullptr, threadRoutine1, (void*)"thread 1");
    std::cout << "main thread" << std::endl;
    sleep(5);   

    void *ret = nullptr;
    pthread_join(tid, &ret);   
    std::cout << "[main thread] child thread exit, ret:" << (long long)ret << std::endl;

    return 0;  
}
图4.3 代码4.3的运行结果

4.4 线程分离 pthread_detach 

严格意义上讲,pthread_detach并不算线程退出即使一个线程函数中使用了pthread_detach(pthread_self())对其自身进行分离,线程函数在pthread_detach之后的代码也会正常被执行。

pthread_detach一般用于不需要关心退出状态的线程被pthread_detach分离的子线程,即使主线程不等待子线程退出,子线程也不会出现僵尸问题

一般来说,都是线程分离其自身,当然也可以通过主线程分离子线程,但不推荐这么做。

经pthread_detach分离之后的线程,不应当pthread_join等待,如果等待一个被分离的线程,那么pthread_join函数会返回错误码。

结论:(1).pthread_detach用于将不需要关系关系退出状态的子线程分离   (2).被分离的线程不应被等待,如果被等待,那么pthread_join会返回非0错误码。

代码4.4演示了经pthread_detach分离之后线程函数继续运行,等待被分离的线程失败的情景。

代码4.4:线程分离及等待被分离的线程

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

// 线程函数
void *threadRoutine1(void *args)
{
    // 子线程将其自身分离
    pthread_detach(pthread_self());

    int count = 0;
    while(true)
    {
        std::cout << (char*)args << ", count:" << ++count << std::endl;
        if(count == 3) pthread_exit((void*)111);
        sleep(1);
    }

    return (void*)10;
}

int main()
{
    pthread_t tid;
    
    // 创建线程
    pthread_create(&tid, nullptr, threadRoutine1, (void*)"thread 1");
    std::cout << "main thread" << std::endl;
    sleep(5);   

    void *ret = nullptr;
    int n = pthread_join(tid, &ret);    // 等待已经取消的线程退出 

    if(n != 0)  // 检验是否等待成功
    {
        std::cout << "wait thread error -> " << strerror(n) << std::endl;
    }

    return 0;  
}
图4.4 代码4.4的运行结果

五. 总结

  • pthread_create函数可以创建子线程,关于线程的管理方法及属性字段,被记录在动态库里,线程id本质上就是地址空间共享区的某个地址。
  • 由于Linux在系统层面不严格区分进程和线程,CPU调用只认PCB,因此为了保证每个线程栈空间的独立性,子线程的栈由用户层(动态库)提供,主线程的栈区就是地址空间的栈区。
  • 在多线程中,任何一个线程出现异常,影响范围都是整个进程,如果在某个线程中调用exec系列函数替换程序,那么整个进程都会被替换掉。
  • pthread_join的功能为在主线程中等待子线程,如果子线程没有被detach且不被主线程等待,那么子线程就会出现僵尸问题。
  • 有三种方法可以终止线程:(1). 线程函数return,这种方法不适用于主线程。(2). pthread_exit 函数终止线程函数。(3). pthread_cancel 取消线程,被取消的线程不需要被等待,如果等待会记录到线程函数返回(void*)-1。
  • 如果某个子线程的退出状态不需要关心,那么就可以通过pthread_detach分离子线程,分离后的线程不应被等待,如果被等待,那么pthread_join函数就会返回非零错误码。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Kali Linux中的ARP欺骗攻击如何进行

在Kali Linux中进行ARP欺骗攻击是一种常见的网络攻击方法&#xff0c;它允许攻击者篡改局域网中的ARP表&#xff0c;以便将网络流量重定向到攻击者控制的位置。 步骤&#xff1a; 安装必要工具&#xff1a; 首先&#xff0c;确保 已经安装了Kali Linux&#xff0c;并在终端中安…

【Acwing338】计数问题题解

题目描述 举个栗子分类讨论 求a~b中x的个数&#xff0c;可以转换为1~b中x的个数减去1~a-1中x的个数 所以核心是求1~n中x的个数&#xff0c;可以转换为求x在1~n中每一个数的每一位上出现的次数的和 假设要求1~abcdefg&#xff08;这是一个七位数&#xff09;中x1的个数&#…

分布式 - 服务器Nginx:一小时入门系列之TCP反向代理和负载均衡

文章目录 1. HTTP反向代理和TCP反向代理2. http 块和 stream 块3. TCP反向代理配置4. TCP 负载均衡 1. HTTP反向代理和TCP反向代理 Nginx可以作为HTTP反向代理和TCP反向代理。 HTTP反向代理是指Nginx作为Web服务器的代理服务器&#xff0c;接收客户端的HTTP请求&#xff0c;然…

容器镜像生成记

概述 容器docker/k8s发布已有一段时间&#xff0c;不少小伙伴开始上手实践。下面以一个简单的应用为例。来说明如何生成镜像并推送至镜像仓库。 准备工作 镜像仓库注册 以最常见的aliyun镜像仓库为例&#xff1a; 支付宝登录aliyun官网&#xff0c;搜索容器镜像服务&#x…

香港全新的虚拟资产服务商发牌制度

香港证监会2023年2月20日通告&#xff0c;原有虛擬資產交易平台如要符合資格參與當作為獲發牌的安排&#xff0c;必須在2023 年6 月1 日至2024 年2 月29 日期間(即由2023 年6 月1 日37起計九個月內)內&#xff0c;根據《打擊洗錢條例》下的虛擬資產服務提供者制度在網上提交完全…

python爬虫10:selenium库

python爬虫10&#xff1a;selenium库 前言 ​ python实现网络爬虫非常简单&#xff0c;只需要掌握一定的基础知识和一定的库使用技巧即可。本系列目标旨在梳理相关知识点&#xff0c;方便以后复习。 申明 ​ 本系列所涉及的代码仅用于个人研究与讨论&#xff0c;并不会对网站产…

【Linux】多线程概念线程控制

文章目录 多线程概念Linux下进程和线程的关系pid本质上是轻量级进程id&#xff0c;换句话说&#xff0c;就是线程IDLinux内核是如何创建一个线程的线程的共享和独有线程的优缺点 线程控制POSIX线程库线程创建线程终止线程等待线程分离 多线程概念 Linux下进程和线程的关系 在…

adb使用总结

adb连接到模拟器 adb devices 打开模拟器&#xff0c;找到设置。 多次点击版本号&#xff0c;切换到开发者模式 搜索进入开发者选项 开启USB调试 此时在终端输入adb devices就连接上了 使用adb查看安卓手机架构 adb shell getprop ro.product.cpu.abi 进入安卓手机的shell …

流处理详解

【今日】 目录 一 Stream接口简介 Optional类 Collectors类 二 数据过滤 1. filter()方法 2.distinct()方法 3.limit()方法 4.skip()方法 三 数据映射 四 数据查找 1. allMatch()方法 2. anyMatch()方法 3. noneMatch()方法 4. findFirst()方法 五 数据收集…

一分钟学会用pygame制作棋盘背景

一分钟一个Pygame案例&#xff0c;这一集我们来学习一下如何生成一个视频中的棋盘背景效果&#xff0c;非常非常简单。 视频教程链接&#xff1a;https://www.bilibili.com/video/BV17G411d7Ah/ 当然我们这里是用来做页面的背景&#xff0c;你也可以拿来做别的效果&#xff0…

origin导出pdf曲线超出边框

软件版本 软件版本Word2021Origin2021Adobe Acrobat Pro2023 问题描述 Origin导出的emf格式矢量图片&#xff0c;插入到Word中&#xff0c;显示正常&#xff0c;但是在使用Word导出→创建Adobe PDF→创建Adobe PDF导出PDF文件后&#xff0c;图片曲线就会超出边框&#xff0c…

5G NR:RACH流程-- Msg1之生成PRACH Preamble

随机接入流程中的Msg1&#xff0c;即在PRACH信道上发送random access preamble。涉及到两个问题&#xff1a; 一个是如何产生preamble&#xff1f;一个是如何选择正确的PRACH时频资源发送所选的preamble? 一、PRACH Preamble是什么 PRACH Preamble从数学上来讲是一个长度为…

考研408 | 【操作系统】 内存管理

内存的基础 内存和内存的作用&#xff1a; 几个常用的数量单位&#xff1a; 指令的工作原理&#xff1a; 问题&#xff1a;如何将指令中的逻辑地址转换为物理地址&#xff1f; 解决办法&#xff1a;装入的三种方式 1.绝对装入 2.可重定位装入 3.动态重定位 从写程序到程…

TabBar组件如何跳转页面?

1、先引入 2、假数据 const tabs [{key: home,title: 首页,icon: <AppOutline />,badge: Badge.dot,},{key: todo,title: 待办,icon: <UnorderedListOutline />,badge: 5,},{key: message,title: 消息,icon: (active: boolean) >active ? <MessageFill /&…

Django基础5——ORM中间程序

文章目录 一、基本了解二、ORM基本操作2.1 连接数据库2.1.1 使用sqlite数据库2.1.2 使用MySQL数据库 2.2 对数据库操作2.2.1 增&#xff08;前端数据——>数据库&#xff09;2.2.2 查&#xff08;数据库——>前端展示&#xff09;2.2.3 改&#xff08;修改数据&#xff0…

如何评估分类模型的好坏

如何评估分类模型的好坏 评估分类预测模型的质量&#xff0c;常用一个矩阵、三条曲线和六个指标。 一个矩阵&#xff1a;混淆矩阵&#xff1b;三条曲线&#xff1a;ROC曲线、PR曲线、KS曲线&#xff1b;六个指标&#xff1a;正确率Acc、查全率R、查准率P、F值、AUC、BEP值、KS…

行业报告 | 2023人工智能发展白皮书

原创 | 文 BFT机器人 在科技日新月异的今天&#xff0c;人工智能已成为最具革命性的技术之一&#xff0c;有望对人类社会生活产生显著的影响。过去几年&#xff0c;人工智能相关理论研究技术创新、软硬件升级等整体推进&#xff0c;极大地促进了人工智能行业的发展。 进入2022…

盖雅工场获评2023年度苏州市服务型制造示范企业(平台)

苏州市工信局公布 2023年度苏州市服务型制造示范企业&#xff08;平台&#xff09;名单 遴选出服务型制造示范企业34家 服务型制造示范平台19个 苏州盖雅信息技术有限公司 “劳动力管理SaaS云平台服务” 获评2023年度苏州市服务型制造示范平台 全市唯一获评的人力资源服务…

【rust/egui】(五)看看template的app.rs:SidePanel、CentralPanel以及heading

说在前面 rust新手&#xff0c;egui没啥找到啥教程&#xff0c;这里自己记录下学习过程环境&#xff1a;windows11 22H2rust版本&#xff1a;rustc 1.71.1egui版本&#xff1a;0.22.0eframe版本&#xff1a;0.22.0上一篇&#xff1a;这里 SidePanel 侧边栏&#xff0c;如下图 …

UG\NX二次开发 使用BlockUI设计对话框时,如何设置默认的开发语言?

文章作者:里海 来源网站:王牌飞行员_里海_里海NX二次开发3000例,C\C++,Qt-CSDN博客 简介: NX二次开发使用BlockUI设计对话框时,如何设置默认的代码语言? 效果: 方法: 依次打开“文件”->“实用工具”->“用户默认设置”->“用户界面”->“操作记录”->“…