Linux——线程控制

目录

前言

一、线程创建

1.创建线程

2.线程传递结构体 

3.创建多线程 

4.收到信号的线程

二、线程终止

三、线程等待

四、线程分离

五、取消线程

六、线程库管理的原理

七、站在语言角度理解pthread库

八、线程的局部存储


前言

前面我们学习了线程概念和线程创建,今天我们学习线程控制,如何操控一个线程完成任务,同时能取消线程、等待线程,分离线程。

一、线程创建

1.创建线程

功能:创建一个新的线程

  • 参数 thread:返回线程ID
  • attr:设置线程的属性,attr为NULL表示使用默认属性
  • start_routine:是个函数地址,线程启动后要执行的函数
  • arg:传给线程启动函数的参数
  • 返回值:成功返回0;失败返回错误码
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
 
void* TreadToutine(void *arg)
{
    const char* threadname = (const char*) arg;
    while(1)
    {
        cout<<"我是一个新线程"<<threadname<<endl;
        sleep(1);
    }
}
 
int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,TreadToutine, (void*)"thread 1");
 
    //主线程
    while(1)
    {
        cout<<"我是主线程"<<endl;
        sleep(1);
    }
    return 0;
}

linux没有真正的线程概念,他的线程是复用的进程代码,只是做了一些区分。线程客观的可以叫做轻量级进程。因此Linux只会提供轻量级进程创建的函数调用,不会直接提供线程创建的接口。因此我们使用pthread原生线程库,编译时需要手动链接库文件(-lpthread)。

这样编译后就可以运行了。

从上面代码可以看出,给线程传递的信息可以是char*,由于pthread_create函数的最后一个参数为void*,同时线程去运行的函数参数也是void*,因此我们任意类型都可以传递过去,进行一下强转即可。

2.线程传递结构体 

比如现在我想传递很多内容过去,叫线程帮我们处理 

如下,我们传递了结构体 

线程成功收到结构体,并做出了处理。

3.创建多线程 

 创建多线程也很简单,只需要循环创建即可。

4.收到信号的线程

如果进程创建的线程有一个发生了异常,收到了信号,会导致整个进程都被终止,因为线程是进程创建出来的,发送信号是发给了进程,进程如果退出,那么该进程所有的资源也都得被回收。而线程本身就是进程资源的一部分。

二、线程终止

我们知道线程去执行的函数返回类型为void*,当线程执行结束,return时,线程就自动终止了

如果我们返回时调用exit()函数 ,那么整个进程都会被终止

同时,pthread.h库还给我们提供了 pthread_exit() 接口,我们使用该接口也可以终止线程。

pthread_exit()

作用:终止一个运行的线程

参数retval:返回void*的全局变量

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

代码如下,两种方法都可以退出 

 运行结果如下,线程被退出,不再打印消息。

三、线程等待

线程退出默认要被等待,如果不等待,就会发生类似于僵尸进程的问题。因此我们需要用pthread_join()函数进行等待

pthread_join()

功能:等待线程结束

  • 参数1::thread:线程ID
  • 参数2:value_ptr:它指向一个指针,后者指向线程的返回值(void**指向的线程返回值void*)
  • 返回值:成功返回0;失败返回错误码

代码如下,让子线程程循环5次后退出并传参常量字符串,主线程去join等待,并将等待的结果输出。

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

class Add
{
public:
    Add(string name, int a, int b)
        : _name(name), _a(a), _b(b)
    {}

public:
    string _name;
    int _a;
    int _b;
};

void *TreadToutine(void *arg)
{
    Add *a1 = (Add *)arg;
    int cnt = 5;
    while (cnt--)
    {
        cout << "我是一个新线程: " << a1->_name << ",计算结果为" << a1->_a + a1->_b << endl;
        sleep(1);
    }
    //return nullptr;
    pthread_exit((void*)"pthread-1 退出"); //常量区
}

int main()
{
    pthread_t tid;
    Add *td = new Add("thread-1", 10, 20);
    pthread_create(&tid, NULL, TreadToutine, td);

    // 主线程
    cout << "我是主线程,子线程的tid: "<< tid << endl;
    void* msg = nullptr;
    pthread_join(tid,&msg);
    cout<<"等待成功,子线程退出信息: "<<(char*)msg <<endl;
    sleep(1);
    return 0;
}

等待成功,同时输出了消息。注意等待是阻塞式等待,子线程退出后才会执行后续代码。

四、线程分离

我们知道,线程是需要被等待的,不然会发生类似于僵尸进程的现象,那么如果我想让线程一直去运行,比如说一直帮我播放音乐,那么主线程就会一直等待,不可能执行后面的代码。

在这种情况下,我们可以让线程分离,也就是主线程不再关心创建的子线程的死活,他要运行就运行,不运行了操作系统会回收。不过一般建议主线程最后再退出

可以使用pthread_detach()函数进行线程的分离。

pthread_detach()

作用:分离线程

分离线程很简单,直接调用pthread_detach()就可以,我们不过多展示,下面代码是先分离线程,再等待线程看看会发生什么。 

发现等待线程的返回值为22,不是0证明等待失败,22的意思是该线程不需要等待。

这是我们是在主线程进行分离的,子线程也可以被分离, 由于子线程默认看不到自己的tid,因此可以调用pthread_self()函数获取自己的tid。

pthread_self

作用:让线程获取自己的tid

如下是子线程选择分离。 

小总结:

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。 

五、取消线程

主线程可以取消线程,也就是让子线程退出,可以调用pthread_cancel()函数进行终止线程。

pthread_cancel()

功能:向线程发送取消请求

  • 参数1:thread,线程ID
  • 返回值:成功返回0;失败返回错误码

代码如下,先取消进程,再等待线程,同时查看线程退出码

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

void *TreadToutine(void *arg)
{
    while(1)
    {
        cout << "我是一个新线程" << endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, NULL, TreadToutine, (void *)"pthread-1");
    sleep(3);

    // 取消线程
    int n = pthread_cancel(tid);
    cout << "线程取消成功,n: " << n << endl;
    // 等待线程
    void *ret = nullptr;
    n = pthread_join(tid, &ret);
    cout << "等待线程返回值n: " << n << ",线程返回值: " << (int64_t)ret << endl;
    return 0;
}

运行看到,线程返回值为0,取消成功,等待返回值为0,等待成功。我们看到线程没有阻塞在等待函数这里,而是直接往后运行,同时进程返回为-1。

这是因为如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数 PTHREAD_ CANCELED

而如果线程先被脱离,再取消,结果怎么样呢?

发现也是能被取消的,但是线程等待是22(等待失败)。因为系统直接回收了。 

小总结:

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

2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_CANCELED。

3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。

4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ptr参数。 

六、线程库管理的原理

我们对线程的操作一直要使用tid,那么tid里面的内容到底是什么呢?

其实他是一个地址,我们转成16进程来看一下。

确实是很像是地址,但这跟LWP(Light Weight Process)也不一样啊。该如何理解呢? 

  • 首先,我们知道pthread. h不是操作系统的接口,而是原生线程库。那么用户创建的线程,操作系统无法管理,则需要线程库来进行管理。他从系统中获取轻量级进程相关属性,从用户中也获取一些属性,这样就先描述起来了,再通过数据结构将线程组织起来,就将线程管理好了。
  • 我们也知道,线程要有独立属性,独立的主要有硬件上下文和栈空间,其中硬件上下文跟操作系统有关,而栈空间则是要从用户中来。栈不是只有一个吗?为什么每一个线程都有自己的栈空间呢?这其实是操作系统帮我们处理了的,操作系统会在堆区创建空间,来充当线程独立的栈。pthread库会获取到栈空间,并将他管理维护好,而默认地址空间中的栈,由主线程使用。

那么线程库如何管理呢,在哪管理呢? 

  • 在进程地址空间中,mmap(共享区)加载了动态库,其中我们使用的pthread库就在该区域,他会管理好每一份线程,每一份线程都在其中有自己的属性集。
  • struct pthread里存在很多线程属性,线程局部存储,还有线程栈,这个栈指向的是堆空间的区域,每当有新线程被创建,都会在后面继续创建这种数据结构。就这样将多个线程统一的描述组织起来了,可以进行管理了。因此我们调用pthread相关函数,相当于对该空间进行访问、处理。

那么现在,我们也可以理解 pthread_t tid 是什么了,他不就是每一个线程在进程地址空间的起始地址嘛,我们pthread_create 对tid进行写入,因为需要创建对应的数据结构,找到起始地址,然后返回,后续用户要继续对线程进行控制,等待啊,终止啊,分离啊,取消啊。都需要传入tid,也就是能找到在进程地址空间的位置后,才可以处理。

七、站在语言角度理解pthread库

我们之前学的pthread库,是Linux提供的原生线程库,在语言层面,比如C++/JAVA\PYTHON,他们也会提供给我们线程库。

我们写了一份代码,使用的是C++提供的线程库 thread

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

void myrun()
{
    while(1)
    {
        cout<<"我是一个新线程"<<endl;
        sleep(1);
    }
}

 int main()
 {
    thread t(myrun);
    t.join();
 }

编译后运行,发现说多线程操作被禁止了,这是因为我们没有链接pthread库。

c++提供的线程库封装了pthread.h。因此我们编译时仍然需要链接 pthread库。

到现在,我们可以知道,语言上也许线程库的使用不一定相同,但是他们底层都是用的linux原生线程库。

在Linux下做了封装,那么这段代码我们可以在Linux中运行。

如果thread头文件在Windows下,封装了Windows线程的操作,那么也可以在Windows下运行。这大大提高了文件的可移植性。

八、线程的局部存储

我们定义一个全局变量,创建线程,让新线程对全局变量做++,观察新线程和主线程全局变量是否发生变化。

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

int g_val = 100;

void *TreadToutine(void *arg)
{
    while (1)
    {
        cout << "我是一个新线程,g_val: " << g_val << ",&g_val: " << &g_val << endl;
        g_val++;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, NULL, TreadToutine, (void *)"Thread 1");
    while (1)
    {
        cout << "我是一个主线程,g_val: " << g_val << ",&g_val: " << &g_val << endl;
        sleep(1);
    }
    pthread_join(tid,nullptr);
}

 我们可以看到,全局变量值一样,地址也一样,我们现在知道全局变量是被所有进程共享的。

如果我们给全局变量前添加上__thread,GCC/G++编译器提供的一个扩展,用于声明线程局部存储变量。

现在运行,主线程和新线程g_val不一样,地址也不一样。

因为我们添加的__thread 会在G++编译时,给每个线程的局部存储空间里将变量拷贝进程,私有一份,于是每个线程自己管理自己的那一份资源。不再与外部共享。 

 只是__thread只能修饰内置类型,如string这种自定义类型无法处理。

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

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

相关文章

异地文件如何共享访问?

异地文件共享访问是一种让不同地区的用户能够快速、安全地共享文件的解决方案。人们越来越需要在不同地点之间共享文件和数据。由于复杂的网络环境和安全性的问题&#xff0c;实现异地文件共享一直是一个挑战。 为了解决这个问题&#xff0c;许多公司和组织研发了各种异地文件共…

Spring Boot接收从前端传过来的数据常用方式以及处理的技巧

一、params 传参 参数是会拼接到url后面的请求 场景规范:url后面的key值<=3个参数的时候,使用params 传参 支持的请求方式:get(正规的是get方式)、post 都行 例如: http://localhost:8080/simpleParam?name=Tom&age=10 在postman里面的体现为 后端接收的接口…

20240402,<<,>>,控制流:while语句 ,for语句

……学很少&#xff0c;学很慢还是比不学强点是吧&#xff0c;救命 昨天不是很懂<<,>> 输入输出 iostream, 输入流 istream 输出流ostream&#xff0c;COUT,CIN,CERR,CLOG #include <iostream> int main() {std::cout << "enter two numbers:&…

成员变量、局部变量

变量分类 定义位置不同 成员变量定义在类中&#xff0c;成员方法之外 局部变量定义在局部范围内&#xff0c;如方法参数&#xff0c;方法内部&#xff0c;循环结构中等 作用范围不同&#xff08;空间&#xff09; 成员变量在整个类内有效&#xff0c;与声明位置无关 局部变…

图神经网络实战(7)——图卷积网络(Graph Convolutional Network, GCN)详解与实现

图神经网络实战&#xff08;7&#xff09;——图卷积网络详解与实现 前言1. 图卷积层2. 比较 GCN 和 GNN2.1 数据集分析2.2 实现 GCN 架构 小结系列链接 前言 图卷积网络 (Graph Convolutional Network, GCN) 架构由 Kipf 和 Welling 于 2017 年提出&#xff0c;其理念是创建一…

未来画卷:当AI短片撼动视界,虚拟与现实的界限模糊

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

IDEA中连接SQLserver数据库(DataGrip相同连接)

IDEA中连接SQLserver数据库(DataGrip相同连接) 1. 打开IDEA-database组件 2. 新建SQL server连接 3. 填写信息进行连接 填写连接名称&#xff0c;连接主机IP&#xff0c;端口&#xff0c;默认端口1433&#xff0c;数据库用户名密码&#xff0c;默认数据库用户名是sa 第一次连接…

Spark 部署与应用程序交互简单使用说明

文章目录 前言步骤一&#xff1a;下载安装包Spark的目录和文件 步骤二&#xff1a;使用Scala或PySpark Shell本地 shell 运行 步骤3:理解Spark应用中的概念Spark Application and SparkSessionSpark JobsSpark StagesSpark Tasks 转换、立即执行操作和延迟求值窄变换和宽变换 S…

Redis高可用与持久化

一、Redis高可用 在web服务器中&#xff0c;高可用是指服务器可以正常访问的时间&#xff0c;衡量的标准是在多长时间内可以提供正常服务&#xff08;99.9%、99.99%、99.999%等等&#xff09;。 但是在Redis语境中&#xff0c;高可用的含义似乎要宽泛一些&#xff0c;除了保证…

智能视频翻译和配音处理工具:Pyvideotrans

pyVideoTrans&#xff1a;一键字幕识别翻译配音带新语言字幕和配音的视频 - 精选真开源&#xff0c;释放新价值。 概览 Pyvideotrans是一款卓著的智能化视频处理系统&#xff0c;专精于视频翻译与配音艺术&#xff0c;以其卓越的技术实力实现对原始视频中音频信息的精准捕捉、…

笔记本电脑win7 Wireless-AC 7265连不上wifi6

1.背景介绍 旧路由器连接人数有限&#xff0c;老旧&#xff0c;信号不稳定更换了新路由器&#xff0c;如 TL-XDR5430易展版用户电脑连不上新的WIFI网络了&#xff0c;比较着急 核心问题&#xff1a;有效解决笔记本连接wifi上网问题&#xff0c;方法不限 2.环境信息 Windows…

4.2总结(快速幂 || 抽象方法,抽象类,接口)

JAVA学习小结 一.抽象方法和抽象类 抽象类不一定有抽象方法&#xff0c;但有抽象方法的一定是抽象类 格式&#xff1a;public abstract 返回值类型 方法名&#xff08;参数列表&#xff09; public abstract class 类名 {} 抽象类和抽象方法的意义&#xff1a;统一子类具有相…

Android 的网络加载

发起网络请求的过程 当用户在应用程序中输入网址或关键字时&#xff0c;应用程序会发起网络请求。这个过程大致如下&#xff1a; 应用程序将请求发送到服务器&#xff0c;服务器返回响应数据。应用程序接收到响应数据后&#xff0c;将其转换为应用程序可识别的数据格式。应用…

单片机中的RAM vs ROM

其实&#xff0c;单片机就是个小计算机。大计算机少不了的数据存储系统&#xff0c;单片机一样有&#xff0c;而且往往和CPU集成在一起&#xff0c;显得更加小巧灵活。 直到90年代初&#xff0c;国内容易得到的单片机是8031&#xff1a;不带存储器的芯片&#xff0c;要想工作&a…

Spark 的结构化 APIs——RDD,DataFrame, Dataset, SparkSQL 使用和原理总结

前言 在本文中&#xff0c;我们将探索 Spark 的结构化 APIs&#xff08;DataFrames and Datasets)。我们还将看下 Spark SQL 引擎是如何支撑高级的结构化 APIs 的。当Spark SQL在早期的Spark 1.x 中首次引入时, 随后是DataFrames 继承了Spark 1.3中 SchemaRDDs &#xff0c;此…

就业班 第二阶段 2401--4.1 day10 shell之“三剑客”+Expect

十一、shell 编程-grep egrep 支持正则表达式的拓展元字符 &#xff08;或grep -E&#xff09; #egrep [0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3} file1.txt [rootnewrain ~]# num11 1、运用正则&#xff0c;判断需要[[ ]] [rootnewrain ~]# [[ $num1 ~ ^[0-9]$ ]] &a…

STM32 | 通用同步/异步串行接收/发送器USART带蓝牙(第六天原理解析)

STM32 第六天 一、 USART 1、USART概念 USART:(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步串行接收/发送器 USART是一个全双工通用同步/异步串行收发模块,该接口是一个高度灵活的串行通信设备 处理器与外部设备通信的两种方式: u并行通信(…

在c# 7.3中不可用,请使用9.0或更高的语言版本

参考连接&#xff1a;在c# 7.3中不可用,请使用8.0或更高的语言版本_功能“可为 null 的引用类型”在 c# 7.3 中不可用。请使用 8.0 或更高的语言版本-CSDN博客https://blog.csdn.net/liangyely/article/details/106163660 [踩坑记录] 某功能在C#7.3中不可用,请使用 8.0 或更高的…

python file怎么打开

Python open() 方法用于打开一个文件&#xff0c;并返回文件对象&#xff0c;在对文件进行处理过程都需要使用到这个函数&#xff0c;如果该文件无法被打开&#xff0c;会抛出 OSError。 注意&#xff1a;使用 open() 方法一定要保证关闭文件对象&#xff0c;即调用 close() 方…

纯CSS实现未读消息显示99+

在大佬那看到这个小技巧&#xff0c;我觉得这个功能点还挺常用&#xff0c;所以给大家分享下具体的实现。当未读消息数小于100的时候显示准确数值&#xff0c;大于99的时候显示99。 1. 实现效果 2. 组件封装 <template><span class"col"><sup :styl…