从0开始linux(39)——线程(2)线程控制

news2024/12/1 11:17:10

欢迎来到博主的专栏:从0开始linux
博主ID:代码小豪

文章目录

    • 线程创建
      • 线程标识符
      • 线程参数
      • 多线程竞争资源
    • 回收线程
      • detach
    • 线程退出
      • pthread_cancel

线程创建

线程创建的函数为pthread_create。该函数是包含在posix线程库当中,posix线程是C语言处理线程的一个标准接口,我们的进程创建、杀死和回收线程,都可以通过posix库来完成完成,这些函数包含在<pthread.h>库函数当中,且大部分函数的名字都是以pthread开头,但是要注意该库不是C语言标准库,因此在使用<pthread.h>时,要记得gcc/g++的编译选项当中加上-lpthread

pthread_create函数原型如下:

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

在我们上一篇文章当中就使用了pthread_create函数,但是博主当时并没有介绍完全这些参数,接下来博主会使用三个pthread_create的例子,让大家了解这些参数到底有什么用。

  • thread:输出型参数,创建一个pthread_t类型的变量,并将其地址传入函数当中,可以获得创建的线程的标识符
  • attr:设置线程的属性,使用NULL表示线程设置成默认模式,博主在本篇的例子中创建的都是默认模式的线程
  • start_routine:创建的线程的入口函数,类似于线程自己的main函数
  • arg:传入给新线程的参数

线程标识符

在我们上一篇文章中,我们提到linux将线程描述成一个轻量级进程(LWP),而且通过ps -aL可以查到轻量级进程的LWP号,那么LWP号和线程标识符thread有什么关系呢?

我们话不多说,直接上例子。

void* routine1(void* arg)//新线程入口函数
{
    while(true)
    {
        ::sleep(1);
    }
}

int main()
{
    pthread_t tid1;//线程标识符
    pthread_create(&tid1,nullptr,routine1,nullptr);//创建新线程
    std::cout<<"thread1 tid:"<<tid1<<std::endl;
    while(true)
    {
        sleep(1);
    }
    return 0;
}

接下来我们运行程序,打开另外一个终端,输入指令:ps -aL | head -1;ps -aL | grep thread

在这里插入图片描述
在这里插入图片描述
这很明显了,线程标识符和lwd号根本不是一个东西,但是它们指向的都是同一个线程,那么为什么要这样呢?首先我们搞清楚一个点,那就是<pthread.h>库是C语言封装的库,不是操作系统的系统调用库,因此C语言不必与操作系统在线程描述方面统一起来,因此lwp号是操作系统用于辨识轻量级进程的,而tid是C语言封装用来辨识线程的,虽然在linux当中lwp和线程是同一个东西。但是不妨碍C语言将其视为其他属性。

线程参数

相信大家对pthread_create的参数有点疑惑吧?第一个就是为什么我们写的start_routine(线程的入口地址)是要返回值为void*,参数为void的回调函数?返回值为void就算了,那个void的参数到底有什么用啊,我们平时创建出线程之后,线程自己就跳转到回调函数当中去了,那里轮得到我们自己使用。那么那个void的参数是什么,怎么传,别急,听我娓娓道来。

在pthread_create当中存在一个void的参数也很存疑。即void *arg,这个arg能传一个void类型的指针,这个指针其实就是给回调函数start_routine的参数,由于它是一个void*的指针,因此我们可以用它指向任何数据,整形、浮点型、任何的c/c++的内置类型,甚至可以是结构体,类的对象。

我们简简单单的定义一个类:

class thread
{
public:
    thread(std::string name,int a,int b):_name(name),_a(a),_b(b)
    {;}
    void Excute(){
        std::cout<<_name<<'-'<<_a<<'-'<<_b<<std::endl;
    }

private:
    std::string _name;
    int _a;
    int _b;
};

接下来我们在main函数当中创建指向该类的对象的指针,并且将该指针转为void*类型。作为参数传递给pthread_create。

int main()
{
    pthread_t tid;
    thread* objptr=new thread("thread-1",11,26);
    pthread_create(&tid,nullptr,routine1,(void*)objptr);

    while(true)
    {
        sleep(1);
    }
    return 0;
}

而我们的routine1函数的参数不是要求是void类型的吗?实际上该参数就是我们传入的objptr,在routine1函数当中,我们可以将void的指针转换成对象类型的指针thread*。这样我们就可以在线程执行的函数当中,传入数据了。

void* routine1(void*arg)
{
    thread* obj=static_cast<thread*>(arg);//将void*类型转换成thread*
    obj->Excute();
    return nullptr;
}

接下来我们编译并执行该程序。
在这里插入图片描述
可以发现,线程入口函数的参数,其实就是我们传入给pthread_create的void*参数arg。

多线程竞争资源

线程之间的数据和代码是共享的,这意味着我们在运行多线程的进程时,会很难控制它们的运行情况。我们可以创建多个线程,让他们共同对一个全局变量进行修改,并且使用同一个函数进行打印,看看会发生什么。

int shared_arg=100;//共同使用的全局变量
void* sharedroutine(void* arg)//共同使用的函数
{
    std::string name(static_cast<char*>(arg));
    while(true)
    {
        shared_arg++;
        std::cout<<"tid:"<<name<<"   arg:"<<shared_arg<<std::endl;
        ::usleep(10);
    }
}

int main()
{
    pthread_t tid1;//线程1标识符
    pthread_t tid2;//线程1标识符
    pthread_t tid3;//线程1标识符
    pthread_t tid4;//线程1标识符
    pthread_create(&tid1,nullptr,sharedroutine,(void*)"thread-1");//创建新线程1
    pthread_create(&tid2,nullptr,sharedroutine,(void*)"thread-2");//创建新线程2
    pthread_create(&tid3,nullptr,sharedroutine,(void*)"thread-3");//创建新线程3
    pthread_create(&tid4,nullptr,sharedroutine,(void*)"thread-4");//创建新线程4s
    while(true)
    {
        sleep(1);
    }
    return 0;
}

我们编译并执行程序。
在这里插入图片描述
可以发现,定义在全局当中变量shared_arg在所有的线程当中是共享的,线程1对其修改后,线程2、线程3、线程4的shared_arg也会跟着修改。那么此时有人就问了,局部变量就不共享吗?实际上也是共享的,只是局部变量会存在于栈区间上,而线程之间的栈区间是独立的,除非你能让线程找到其他线程栈区间上的变量。但是博主想不到该怎么做(haha)。

而且我们发现线程之间既然发生了写入重复的错误,那么这是如何导致的呢?我们了解过,线程会共享进程中的资源,其中就包括文件资源,而linux当中一切皆文件,包括显示器,因此如果我们向显示器写入数据,如果线程1和线程2同时都在向显示器写入数据,都会发现这种写入重复的情况。实际上不仅仅是显示器,只要是多个线程向任何一种文件写入数据,都有可能发生这种情况。

那么我们可以看到,线程对于公开资源的使用其实是需要我们控制的,因为你也不想发现程序执行完后,明明逻辑写的对的,但是一打开文件、发现写的数据都是乱七八糟的吧?而线程的控制方法,就是线程的互斥和同步,这一点我们后面再说。

回收线程

由于线程有其独立的内核数据结构,而内核数据结构是实打实存在于内存当中的,因此如果当线程结束后,没有对线程进行回收,这个内核数据结构就会一直存在于内存当中,造成内存泄漏,这个道理和进程回收很像。

那么为什么线程不会自动回收呢?这是因为我们的线程在结束之后是会有返回值的,这个返回值可以被用户所使用,因此操作系统不敢自动回收线程,因为操作系统也不确定我们用户到底用不用这个返回值,也不确定我们何时用线程的返回值。因此操作系统就只好一直帮助我们保存。直到用户主动回收为止。

那么线程回收的函数是什么?

int pthread_join(pthread_t thread, void **retval);
  • thread:pthread_t类型的参数,即我们想要回收的线程的线程描述符
  • retval:输出型参数,还记的我们线程的入口函数,其返回值是void*的吧?这个retval就是接收线程的返回值的。

如果pthread_join正常回收,其返回值为0,若pthread_join回收异常,则返回值为非0。但是博主在linux环境下没有找到引起pthread_join回收失败返回值为非0的情况。因为大部分时候如果线程出现了错误,进程就直接被杀死了。那么博主就不试了。

要注意,pthread_join会引起进程阻塞,而且线程不能自己回收自己,否则会造成deadlock(死锁),因为线程回收自己,会导致阻塞,而线程自己阻塞了,就永远无法结束,也就无法回收了,不能回收又会一直阻塞……(俄罗斯套娃)

关于回收,我们先简单认识这个函数,线程到底如何回收会更好?这一点我们后面再说。

话不多说,直接上例子。

void* routine1(void*arg)
{
    thread* obj=static_cast<thread*>(arg);//将void*类型转换成thread*
    obj->Excute();
    return (void*)10;
}

我们接着用上面使用过的代码,但是将其改造了一下,routine1的返回值从nulllptr变成了arg。

int main()
{
    pthread_t tid;
    thread* objptr=new thread("thread-1",11,26);
    pthread_create(&tid,nullptr,routine1,(void*)objptr);

    void* threadret=nullptr;
    int n=pthread_join(tid,&threadret);
    if(n==0) std::cout<<"回收tid:"<<tid<<"成功"<<std::endl;
    unsigned long ret=(unsigned long)threadret;
    std::cout<<ret<<std::endl;
    

    return 0;
}

在主线程当中,我们用void**类型的参数接收routine1函数的返回值,并且将其转换成unsigned long类型的变量。由于我们将其以void*类型返回了,因此要转换成unsigned long之后,才能看到返回值10。

接下来我们编译并运行。
在这里插入图片描述

可以发现确实能将线程回收,而且routine1当中的返回值确实是能通过pthread_join获得。

通常情况下,回收线程的工作都是交给主线程来完成,如果主线程结束,那么其他线就会继续运行,因此大部分情况下,主线程一定会比其他线程晚退出。

detach

由于主线程在回收线程时,需要阻塞等待待回收的线程结束,那么如果线程直接没有结束,那么主线程就会一直等待,这可能不是你想要的代码逻辑,那么有没有办法让主线程等待线程时,不阻塞等待。

线程有两种状态,其中一种是joined,也是我们创建新线程的默认状态,还有一种叫做detach。我们可以使用函数pthread_detach使线程进入detach状态,当线程处于detach状态下,对于该线程的回收操作将会失效。

int pthread_detach(pthread_t thread);

这个函数就不展示用例,我们向pthread_detach函数,传入线程标识符thread,thread就会进入detach状态。处于detach状态的线程,会在线程退出后,自动被操作系统回收。如果我们尝试用pthread_join回收detach状态下的线程,那么pthread_join只会返回一个非0的值,说明回收线程的操作失败了。detach的线程是不会被其他线程回收的。

线程退出

线程的退出方法有两种,一种是直接在线程的执行函数当中return,这个方法我们一直在前面的例子当中使用,因此不多bb。

第二种方法是在线程的执行函数使用pthread_exit函数。该函数原型如下:

void pthread_exit(void *retval);
  • retval:线程结束的返回值,在主线程当中可以使用pthread_joined接收到该返回值。

我们可以在线程的任意一个执行位置使用pthread_exit。当线程执行到该位置时,就会直接退出,这是和return结束线程不一样的地方,pthread_exit可以在任意的位置退出,即使线程处于调用中的函数。

void thread_to_do()
{
    std::cout<<"线程正在执行thread_todo"<<std::endl;//如果真推出了,显示器打印的信息只有这个
    pthread_exit((void*)10);
}

void* routine1(void*arg)
{
    thread_to_do();
    std::cout<<"线程从thread_todo执行回来"<<std::endl;//如果没有退出,那么还会打印这个信息
    return (void*)10;
}

我们让线程在执行thread_to_do函数,接着在thread_to_do函数当中调用pthread_exit函数,如果真如我们所言,pthread_t能让线程无论在执行任何函数时,都能无视函数栈帧直接退出,那么屏幕上只会打印出“线程正在执行……”。而不会打印后面的信息。
在这里插入图片描述

那么有人可能会这么觉得,我们使用exit()函数是不是也能让线程退出呢?这句话对,但也不对,对是因为当线程执行到exit时,确实会退出,但是不对的地方在于,不仅仅执行exit()函数的线程会退出,进程中所有的一切线程,也会随之退出。因此exit其实是不能作为线程退出来使用的,因为它本身的作用是让进程退出,而非线程。因此在程序当中使用exit函数时,一定要注意你退出的要是进程还是线程。

pthread_cancel

严格来说,pthread_cancel函数并不属于线程退出,而是将一个特定的线程直接删除,被删除的线程需要被回收,一般这个工作都是让主线程来完成。我们先来看看pthread_cancel的函数原型。

int pthread_cancel(pthread_t thread);
  • thread:这是一个pthread_t类型的参数,即线程标识符,删除thread对应线程标识符的线程。

这个功能很简单,但是我们有一个问题是要解决的,那就是被删除的线程不是要回收吗?那么回收不就是能接收到线程的返回值,那么它的返回值是多少呢?

void* routine1(void*arg)
{
    thread* obj=static_cast<thread*>(arg);//将void*类型转换成thread*
    while(true)
    {
        obj->Excute();
        sleep(1);
    }
    return (void*)10;
}

int main()
{
    pthread_t tid;
    thread* objptr=new thread("thread-1",11,26);
    pthread_create(&tid,nullptr,routine1,(void*)objptr);

    sleep(1);
    pthread_cancel(tid);//将tid对应的线程删除
    sleep(5);
    void* threadret=nullptr;
    int n=pthread_join(tid,&threadret);
    if(n==0) std::cout<<"回收tid:"<<tid<<"成功"<<std::endl;
    long ret=(long)threadret;
    std::cout<<ret<<std::endl;
    

    return 0;
}

运行结果为:
在这里插入图片描述
pthread_cancel结束的进程,其返回值为PTHREAD_CANCELED;这是一个宏,我们可以找到这个宏定义。
在这里插入图片描述
也就是说,只要被pthread_cancel结束的线程,其返回值只会是-1。

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

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

相关文章

28.100ASK_T113-PRO Linux+QT 显示一张照片

1.添加资源文件 2. 主要代码 #include "mainwindow.h" #include "ui_mainwindow.h" #include <QImage> #include <QPixmap>MainWindow::MainWindow(QWidget *parent) :QMainWindow(parent),ui(new Ui::MainWindow) {ui->setupUi(this);QIm…

不用下载安装的线上3D编辑器,支持哪些功能?

线上3D编辑器&#xff0c;不用下载软件&#xff0c;直接通过浏览器打开51建模网&#xff0c;上传模型即可进入编辑器&#xff0c;不仅支持对3D模型进行细致的效果配置&#xff0c;如光源设置、背景定制、材质调节等&#xff0c;还具备爆炸效果、热点动画、部件替换等高级交互功…

C语言——指针初阶(三)

目录 一.指针-指针 代码1&#xff1a; 运行结果&#xff1a; 代码2&#xff1a; 运行结果&#xff1a; 代码3&#xff1a; 运行结果&#xff1a; 二.指针数组 例&#xff1a; 往期回顾 一.指针-指针 指针减去指针的前提&#xff1a;两个指针指向同一块空间。 指针减去指针…

vue3项目创建方式记录

目录 创建vue3常用的方式有三种&#xff1a;一.使用vue cli创建二.使用vite创建三.使用vue3官方推荐创建方式&#xff08;create-vue&#xff09; 创建vue3常用的方式有三种&#xff1a; 一.使用vue cli创建 vue create 项目名二.使用vite创建 vite是下一代前端开发与构建工…

基于特征子空间的高维异常检测:一种高效且可解释的方法

本文将重点探讨一种替代传统单一检测器的方法&#xff1a;不是采用单一检测器分析数据集的所有特征&#xff0c;而是构建多个专注于特征子集(即子空间)的检测器系统。 在表格数据的异常检测实践中&#xff0c;我们的目标是识别数据中最为异常的记录&#xff0c;这种异常性可以…

MySQL —— MySQL 程序

目录 前言 一、MySQL 程序简介 二、mysqld -- MySQL 服务器 三、mysql -- MySQL 客户端 1. mysql 客户端简介 2. mysql 客户端选项 &#xff08;1&#xff09;指定选项的方式 &#xff08;2&#xff09;mysql 客户端命令常用选项 &#xff08;3&#xff09;在命令行中使…

Flink CDC 使用实践以及遇到的问题

背景 最近公司在做一些业务上的架构调整&#xff0c;有一部分是数据从mysql采集到Starrocks&#xff0c;之前的一套方法是走 debezium 到 puslar 到 starrocks,这一套下来比较需要配置很多东西&#xff0c;而且出现问题以后&#xff0c;需要修改很多配置&#xff0c;而且现阶段…

数据链路层(三)--点对点通信协议PPP

PPP协议叫做点对点协议&#xff0c;是目前使用的最广泛的数据链路层协议。 1 PPP协议的特点 用户通常需要连接到某个ISP才能接入互联网&#xff0c;PPP协议就是用户计算机和ISP进行通信所使用的数据链路层协议。 1.1 PPP协议应满足的需求 &#xff08;1&#xff09;简单&…

C语言 分支语句(if)

分支语句(if) if语句形式一 适用只有一个分支判断 if(表达式1) //如果 { 语句块1 } if语句形式二 适用有两个分支判断 if(表达式1) //如果 { 语句块1 } else //否则 { 语句块2 } 例:求方程的根 if语句形式三 适用多分支判断 if(表达式1) //多分支 { 语句块1 } else if(表达…

如何将WSL的虚拟机安装到任意目录中

目录 引言 下载安装包 解压安装包 手工安装 结语 引言 WSL默认是将虚拟机安装在C盘的用户目录下&#xff0c;如果长时间使用Windows后&#xff0c;可能C盘的空间就会非常吃紧&#xff0c;所以非常希望把虚拟机安装到C盘以外的目录中。本文就介绍一下相关的工作。 这里只讨…

一款.NET开源的Windows资源管理器标签页工具

前言 今天大姚给大家分享一款基于.NET开发的可以让你在Windows资源管理器中使用Tab多标签功能的小工具&#xff1a;QTTabBar。 工具介绍 QTTabBar是一款基于.NET开发的可以让你在Windows资源管理器中使用Tab多标签功能的小工具。从此以后工作时不再遍布文件夹窗口&#xff0c…

传输控制协议(TCP)

传输控制协议是Internet一个重要的传输层协议。TCP提供面向连接、可靠、有序、字节流传输服务。 1、TCP报文段结构 注&#xff1a;TCP默认采用累积确认机制。 2、三次握手、四次挥手 &#xff08;1&#xff09;当客户向服务器发送完最后一个数据段后&#xff0c;发送一个FIN段…

c++哈希表(原理、实现、开放寻址法)适合新手

c系列哈希的原理及实现&#xff08;上&#xff09; 文章目录 c系列哈希的原理及实现&#xff08;上&#xff09;前言一、哈希的概念二、哈希冲突三、哈希冲突解决3.1、开放寻址法3.2、删除操作3.3、负载因子四、代码实现 总结 前言 红黑树平衡树和哈希有不同的用途。 红黑树、…

服务器数据恢复—raid6阵列硬盘被误重组为raid5阵列的数据恢复案例

服务器存储数据恢复环境&#xff1a; 存储中有一组由12块硬盘组建的RAID6阵列&#xff0c;上层linux操作系统EXT3文件系统&#xff0c;该存储划分3个LUN。 服务器存储故障&分析&#xff1a; 存储中RAID6阵列不可用。为了抢救数据&#xff0c;运维人员使用原始RAID中的部分…

Python酷库之旅-第三方库Pandas(250)

目录 一、用法精讲 1181、pandas.tseries.offsets.BusinessMonthEnd.is_on_offset方法 1181-1、语法 1181-2、参数 1181-3、功能 1181-4、返回值 1181-5、说明 1181-6、用法 1181-6-1、数据准备 1181-6-2、代码示例 1181-6-3、结果输出 1182、pandas.tseries.offse…

layui table 纵向滚动条导致单元格表头表体错位问题

我用的时layui2.6.8版本 历史项目维护&#xff0c;bug给我让我做了&#xff0c;本来利用前端手段强解决&#xff0c;后来发现很多table 找了解决办法 打开layui-v2.6.8/lay/modules/table.js 如果打开后时压缩的代码 直接搜索 e.find(".layui-table-patch") …

BWO-CNN-BiGRU-Attention白鲸优化算法优化卷积神经网络结合双向门控循环单元时间序列预测,含优化前后对比

BWO-CNN-BiGRU-Attention白鲸优化算法优化卷积神经网络结合双向门控循环单元时间序列预测&#xff0c;含优化前后对比 目录 BWO-CNN-BiGRU-Attention白鲸优化算法优化卷积神经网络结合双向门控循环单元时间序列预测&#xff0c;含优化前后对比预测效果基本介绍模型描述程序设计…

vue.js学习(day 13)

.sync修饰符 App.vue <template><div class"app"><buttonclick"isShow true">退出按钮</button><!-- :visible.sync > :visible update:visible--><BaseDialog :visible.sync"isShow"></BaseDia…

Android复习简答题

一、基础入门 Android程序架构 &#xff08;1&#xff09;app:用于存放程序的代码和资源等内容。包含很多子目录 libs:存放第三方jar包 src/androidTest&#xff1a;存放调试的代码文件 src/main/androidMainfest.xml 整个程序的配置文件&#xff0c;可配置程序所需要的权…

【娱乐项目】竖式算术器

Demo介绍 一个加减法随机数生成器&#xff0c;它能够生成随机的加减法题目&#xff0c;并且支持用户输入答案。系统会根据用户输入的答案判断是否正确&#xff0c;统计正确和错误的次数&#xff0c;并显示历史记录和错题记录。该工具适合用于数学练习&#xff0c;尤其适合练习基…