《Linux从练气到飞升》No.26 Linux中的线程控制

news2024/9/21 16:35:52

🕺作者: 主页

我的专栏
C语言从0到1
探秘C++
数据结构从0到1
探秘Linux
菜鸟刷题集

😘欢迎关注:👍点赞🙌收藏✍️留言

🏇码字不易,你的👍点赞🙌收藏❤️关注对我真的很重要,有问题可在评论区提出,感谢阅读!!!

文章目录

    • 前言
    • 1 线程创建
    • 2 线程等待
    • 3 线程终止
      • 3.1 pthread_exit 线程退出函数
      • 3.2 pthread_cancel 取消线程函数
    • 4 线程分离
    • 5 线程ID及进程地址空间布局

前言

随着计算机技术的不断发展,多线程编程已经成为了程序设计中的一种重要方式。在Linux系统中,线程控制是多线程编程的核心内容之一。线程是一种轻量级的执行单元,它能够提高程序的并发性和响应速度,同时也能够有效地利用系统资源。

在Linux系统中,线程的控制主要涉及到线程的创建、等待、唤醒和销毁等方面。本文将详细介绍Linux中的线程控制机制,包括线程的状态转换、线程间的同步与互斥、线程的优先级以及线程的调度等方面。通过本文的学习,读者可以深入理解Linux中的线程控制原理,掌握线程编程的基本技巧和方法,为编写高效稳定的多线程程序打下坚实的基础。

1 线程创建

这里需要用到之前讲过的线程创建的函数 pthread_create函数

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

返回值与参数说明:

  • 线程创建成功返回0 失败返回错误码 在线程库中 几乎所有的返回值都是成功返回0 失败返回错误码
  • thread:获取创建成功的线程ID 该参数是一个输出型参数
  • attr: 用于设置创建线程的属性 如果我们传入NULL则设置为默认属性
  • start_routine:这是一个函数地址 传入我们想要这个线程执行的函数
  • arg: 传给线程例程的参数 (默认是void* 类型 记得类型强转不然会报警告)

代码演示:
下面代码的作用是创建一个子线程,这个线程不停的打印参数发过去的消息,同时我们的主线程不停的打印另外的消息 在子线程中还调用了一个pthread_self()函数 它的作用是返回当前线程的id

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

void* run_thread(void* args)
{
  char* msg = (char*)args;
  while(1)
  {
    printf("I'm a new pthread my tid is: %lu\n" , pthread_self()); 
    sleep(1);
  }
}

int main()
{
  pthread_t tid;
  pthread_create(&tid ,NULL ,run_thread ,(void*)"thread 1");
  while(1)
  {
    printf("im main thread i create the new pid is:%lu\n", tid);
    sleep(1);
  }
  return 0;
}

运行结果可能出现下面的情况:
924b495ba87e40cb982ce444e8c396b3.png
这是因为程序找不到库文件导致的,我需要在编译的命令后面加上-l pthread这样就可以让它正常运行了

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

结果如下:
image.png
可以观察到主线程创建的子线程的pid就是新线程的pid
并且做到之前从没做到过的事
两个死循环
但是线程的健壮性不够强,只要有一个线程崩溃它就都会崩溃
演示一下,让新线程运行一段时间后出现一个野指针问题 观察实验现象
演示代码如下:

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

void* run_thread(void* args)
{
  char* msg = (char*)args;
  while(1)
  {
    printf("im a new pthread my tid is: %lu\n" , pthread_self()); 
    sleep(3);
    int* p = NULL;
    *p = 20;
  }
}

int main()
{
  pthread_t tid;
  pthread_create(&tid ,NULL ,run_thread ,(void*)"thread 1");
  while(1)
  {
    printf("im main thread i create the new pid is:%lu\n", tid);
    sleep(1);
  }
  return 0;
}

结果如下
image.png
可以清楚看到新线程奔溃后,另一个线程也崩溃了

2 线程等待

在学习进程控制的时候讲过进程根据需要会进行进程等待 不然可能会造成僵尸进程的问题 而线程同样是通过PCB来保存数据的 所以线程也需要进行线程等待
在Linux操作系统中 我们可以使用pthread_join函数来实现

int pthread_join(pthread_t thread, void **retval);

返回值和参数说明:

  • 返回值:线程等待成功就返回0 失败返回错误码
  • thread: 被等待线程的ID
  • retval:线程退出时的退出码信息 因为线程退出的返回值是一个void*类型的数据 所以我们要使用一个void**类型的数据去接收它

调用该函数的线程将被挂起等待直到ID为thread的线程终止
演示代码:
下面的代码的行为是:先创建一个新线程 这个线程会打印自己的线程id 之后休眠3秒并结束
主线程使用pthread_join函数接收它的返回值并打印
这里要注意的是:我们不能直接打印status 而是要将它强制转换为intptr_t再强制转换为int

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

void* run_thread(void* args)
{
  printf("im new thread my tid is:%lu\n", pthread_self());        
  sleep(3);
  return (void*)101;
}

int main()
{
  pthread_t tid;
  pthread_create(&tid, NULL, run_thread, (void*)"thread 1");
  void* status = NULL;
  printf("waiting ...\n");
  pthread_join(tid, &status);
  printf("wait success, exit code is:%d\n", (int)(intptr_t)status); 
  return 0;
}

结果如下:
image.png
可以看到主线程收到了新线程的退出码101
也就知道了pthread_join函数确实会进行线程等待 而调用这个函数的线程要等到新线程结束才继续执行
如果有异常情况需要处理吗?
答案是不需要 因为当一个线程出现异常情况的时候 操作系统就会发信号给进程 从而杀死进程
这里还要注意的当我们创建了多个线程时 只能一个个等待

3 线程终止

在前面线程等待中看到 线程结束用的是return
return 包含两种情况

  • 一个是主线程使用代表主线程和进程退出
  • 还有一个就是其他线程return 只代表线程退出

3.1 pthread_exit 线程退出函数

线程退出除了使用return以外 还有专门的函数pthread_exit函数

  void pthread_exit(void *retval);

返回值和参数说明:

  • 返回值:void 这很好理解 当线程不存在了 返回值也就没有了意义
  • retval:线程退出时退出码信息

演示代码:
下面代码使用这个函数来退出线程 返回值是520

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

void* run_thread(void* args)
{
  printf("im new thread my tid is:%lu\n", pthread_self());
  sleep(3);
  pthread_exit((void*)520);
}

int main()
{
  pthread_t tid;
  pthread_create(&tid, NULL, run_thread, (void*)"thread 1");
  void* status = NULL;
  printf("waiting ...\n");
  pthread_join(tid, &status);
  printf("wait success, exit code is:%d\n", (int)(intptr_t)status);
  return 0;
}

结果如下:
image.png
可以看到退出码为设置的520

3.2 pthread_cancel 取消线程函数

int pthread_cancel(pthread_t thread);

返回值和参数说明:

  • 返回值:成功返回0 失败返回-1
  • thread:被取消线程的ID
  • 这个函数既可以让线程取消自己 也可以让新线程取消主线程 但是不建议这样做 只建议使用主线程取消新线程

演示代码:
下面代码的作用是创建线程以后马上取消并且查看退出码

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

void* run_thread(void* args)
{
  printf("im new thread my tid is:%lu\n", pthread_self());  
  while(1)
  {
    // 检查取消状态
    if (pthread_cancel(pthread_self()) != 0) {
      printf("Thread is being cancelled\n"); 
      pthread_exit((void*)-1);
    }
    sleep(3);
  }
}

int main()
{
  pthread_t tid;
  pthread_create(&tid, NULL, run_thread, (void*)"thread 1");
  void* status = NULL;
  printf("waiting ...\n");
  sleep(5); // 等待5秒钟
  pthread_cancel(tid); // 发送取消请求
  pthread_join(tid, &status);
  if (status == PTHREAD_CANCELED) {
    printf("Thread was successfully cancelled\n");
  } else {
    printf("Thread was not cancelled\n"); 
  }
  return 0;
}

结果如下:
image.png
PTHREAD_CANCELED是什么?
其实它就是 -1 只不过它是宏定义而已 所以用他来检查退出是否正常
查看一下在系统中是怎么定义的:
image.png
可以看到它实际上是被定义为**((void*)-1)**
需要注意的是默认创建线程的时候 线程的属性是joinable属性 它会导致线程退出的时候需要别人来回收自己的退出资源 线程退出了 但是线程在共享区中的空间还没有释放

4 线程分离

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

什么是线程分离?
一个线程如果被设置为分离属性 那这个线程在退出后 不需要其他执行流来回收该资源 而是由操作系统进行回收 它既可以是被线程组内的其他线程分离 又可以是线程自己分离 就和线程取消一样 当这个线程被设置为线程分离 就不能被等待了 这是相冲突的 它分离后就是"孤家寡人"了
我们可以使用pthread_detach函数来进行线程分离

int pthread_detach(pthread_t thread);

虽然前面说它分离后就变成“孤家寡人” 但是如果这个线程崩溃之后 进程同样会崩溃 那线程分离的意义在哪里?
线程分离的主要目的在于允许线程独立于创建它的进程运行,从而避免资源泄漏和提高系统性能。虽然线程分离后确实会变成“孤家寡人”,不再受到主线程的管辖,但是这也意味着对于主线程来说,无需等待这些“孤家寡人”线程的结束,从而能够更快地继续执行自己的任务。
当一个线程被分离后,线程终止时它所占用的系统资源会被自动释放,无需其他线程或者主线程专门等待或者回收资源,进而降低了系统负担。此外,线程分离还可以简化线程的管理,尤其是在多线程程序中,当有大量的线程需要创建时,分离线程可以减少对线程的管理和资源回收的工作量。
演示代码:
在创建完这个线程之后就分离它 并在三秒之后终止主线程和分离的线程

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

void* run_thread(void* args)
{
  printf("im new thread my tid is:%lu\n",pthread_self());
  printf("hello im new thread\n");
  sleep(3);
}

int main()
{
  pthread_t tid;
  pthread_create(&tid, NULL, run_thread, (void*)"thread 1");
  printf("waiting ...\n");
  pthread_detach(tid);
  sleep(3);
  return 0;
}

结果:
image.png

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

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

image.png
演示代码:
让主线程打印自己的进程ID、线程ID和新线程的ID 让新线程打印自己的进程ID和线程ID

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

void* thread_run(void* arg)
{
    while(1)
    {
        cout<<"i am:"<<(char*)arg<<"pid:"<<getpid()<<" "<<"my thread id is:"<<pthread_self()<<endl;
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    int ret=pthread_create(&tid,NULL,thread_run,(void*)"thread 1");
    if(ret!=0)
    {
        return -1;
    }
    while(1)
    {
        cout<<"i am main thread id:"<<pthread_self()<<" "<<"new thread:"<<tid<<" "<<"pid:"<<getpid()<<endl;
        sleep(2);
    }
    return 0;
}
 

结果如下:
image.png
image.png
线程ID 本质就是一个进程地址空间上的一个地址 也就是虚拟地址
我们可以通过ldd命令查看一下编译好的文件
先介绍一下ldd
ldd 是一个在 Linux 系统中用于显示共享库依赖关系的命令。它列出了程序运行时所需的动态链接库及其路径。使用方法如下:

ldd [选项] 可执行文件或库

例如,要查看名为 code 的可执行文件的依赖关系,可以使用以下命令:

ldd code

image.png
我们所链接的线程库实际上就是一个动态库 既然是库肯定就是一个文件
0f899c9cf85b43f1a07406ae84b202f3.png
当这个文件被加载到物理内存之后会经过页表的映射加载到进程地址空间的共享区中
我们之前说每个进程都有自己的栈空间 实际上这个栈空间和我们所想的是不一样的
线程采用的栈是在共享区开辟的 除此以外线程还有自己的struct pthread 当中包含了对应线程的各种属性
每个线程还有自己的线程局部存储 私有数据 还有线程被切换时的上下文数据
每当我们增加一个线程 在共享区中就会增加一个对应的结构体
269d028c1edf49c7bbeee4bdec595668.png
在上面我们所用的各种线程函数 它的本质就是在库内部对线程属性进行各种操作 最后要执行的代码交给对应的内核级线程(轻量级进程)去执行即可 可以说对线程的管理就是基于共享区的
而这些个LWP就存在一个个结构体中
我们所看到的tid就是一个个结构体的虚拟地址
pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。

image.png
b02f688167054865931509689af795c4.png

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

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

相关文章

目标检测问题总结

目标检测问题总结 目标检测二阶段和一阶段的核心区别目标检测二阶段比一阶段的算法精度高的原因1. 正负样本不平衡2.样本的不一致性 如何解决目标检测中遮挡问题如何解决动态目标检测FPN的作用如何解决训练数据样本过少的问题IOU代码实现NMS代码实现NMS的改进思路 目标检测二阶…

DBC文件解析成C语言

1. 安装python环境 例如Windows安装python3.10版本 下载地址https://cdn.npmmirror.com/binaries/python/3.10.9/python-3.10.9-amd64.exe 2. 安装cantools函数库 打开CMD窗口后&#xff0c;输入pip install cantools 3. 执行dbc文件转为C语言 注意&#xff1a;c文件和…

时间序列预测(2) — 时间序列预测数据集

目录 数据集1&#xff1a;GEFCom2014负荷数据 数据集2&#xff1a;爱奇艺用户留存预测挑战赛数据集 数据集1&#xff1a;GEFCom2014负荷数据 数据集下载&#xff1a; 百度网盘&#xff1a;链接&#xff1a;https://pan.baidu.com/s/1PgCWHx8vYUfGB9UGtCmaVA?pwdktn0 提取码…

儿童HPV感染有哪些症状?皮肤性病科谭巍主任讲述五大要点

HPV&#xff0c;人乳头瘤病毒&#xff0c;是一种常见的感染性强的病毒&#xff0c;相比成人儿童也会感染HPV。家长及时了解儿童感染HPV的症状对于早期发现和治疗至关重要。为了帮助家长们更好预防儿童HPV感染&#xff0c;今日特邀劲松中西医医院皮肤性病科主任谭巍详细介绍儿童…

MoveIt 机械臂运动 学习 01-MoveIt 初次见面

ROS中有针对机器人进行移动操作的一套工具——MoveIt&#xff01;。在主页http://moveit.ros.org 上 包含使用MoveIt&#xff01;的文档、教程、安装说明以及多种机械臂&#xff08;或机器人&#xff09;的示例演示&#xff0c;如一些 移动操作任务&#xff0c;包括抓握、拾取和…

前端框架图谱

以上图谱基于个人经验总结&#xff0c;比如小程序、第三方平台等未在其中有所体现

Web安全之PHP的伪协议漏洞利用,以及伪协议漏洞防护方法

一、背景 今天介绍一个比较冷门的知识&#xff0c;只有在PHP环境中存在的伪协议漏洞&#xff0c;那么什么是PHP伪协议呢&#xff1f;PHP伪协议事实上就是支持的协议与封装协议。可用于类似 fopen()、 copy()、 file_exists() 和 filesize() 的文件系统函数。 除了这些封装协议…

sqlmap requires ‘python-pymysql‘ third-party library

使用sqlmap进行udf提权报错&#xff1a; [14:06:04] [CRITICAL] sqlmap requires python-pymysql third-party library in order to directly connect to the DBMS MySQL. You can download it from https://github.com/PyMySQL/PyMySQL. Alternative is to use a package pyt…

[WSL] 安装MySQL8

安装版本 mysql --version mysql Ver 8.0.35-0ubuntu0.20.04.1 for Linux on x86_64 ((Ubuntu))安装步骤 sudo apt-get update sudo apt-get upgradesudo apt-get install mysql-server apt install mysql-client apt install libmysqlclient-devsudo usermod -d /var/lib/m…

excel中通过ROW函数返回引用的行号

例如&#xff0c;想引用B3的行号&#xff08;行号应该是3&#xff09;&#xff1a; 鼠标点在想输入函数的单元格&#xff1a; 插入-》函数&#xff1a; 选择ROW函数&#xff1a; 点击“继续”&#xff0c;然后点击红框圈出来的按钮&#xff1a; 鼠标点击B3单元格&…

亚马逊鲲鹏系统能注册哪些国家的买家号

亚马逊鲲鹏系统全面支持12个站点&#xff1a;美国站点、加拿大站点、德国站点、英国站点、法国站点、意大利站点、西班牙站点、日本站点、印度站点、新加坡站点、澳大利亚站点、荷兰站点。 对于注册可以用手机号或者用邮箱进行注册&#xff0c;注册时自动模拟真人点击输入账号、…

VB.net TCP服务端监听端口接收客户端RFID网络读卡器上传的读卡数据

本 示例使用设备介绍&#xff1a;WIFI/TCP/UDP/HTTP协议RFID液显网络读卡器可二次开发语音播报POE-淘宝网 (taobao.com) Imports System.Threading Imports System.Net Imports System.Net.Sockets Public Class Form1Dim ListenSocket As SocketDim Dict As New Dictionary(Of…

vscode 内置扩展找不到? 设置之后不知道怎么还原?

搜索上面框起来的就是内置扩展 vscode这一招太恶心。。。

自动化测试项目:浅谈分层设计的思路

本文以笔者当前使用的自动化测试项目为例&#xff0c;浅谈分层设计的思路&#xff0c;不涉及到具体的代码细节和某个框架的实现原理&#xff0c;重点关注在分层前后的使用对比&#xff0c;可能会以一些伪代码为例来说明举例。 接口测试三要素&#xff1a; 参数构造发起请求&a…

3种方法,教你用Pytest更改自动化测试用例执行顺序!

前言 在自动化测试中&#xff0c;自动化测试用例设计原则就是&#xff1a;执行过程时不能存在依赖顺序。那么如果测试用例需要按照指定顺序执行&#xff0c;这个时候应该怎么做呢&#xff1f; 目前单元测试框架中UnitTest没有办法改变测试用例的执行顺序&#xff0c;但是另一…

1688商品采集api接口1688代购商品采集API商品详情数据获取

做小程序商城时&#xff0c;最崩溃的瞬间是什么&#xff1f; 一定是当你有几百件商品&#xff0c;却要一件一件编辑商品名称、规格、上传图片吧…… 为了帮助商家快速上货开店&#xff0c;特意提供了1688的获取商品详情数据的接口&#xff0c;方便商家一键采集淘宝、天猫、京…

【广州华锐视点】海外制片人VR虚拟情景教学带来全新的学习体验

虚拟现实&#xff08;Virtual Reality&#xff0c;简称VR&#xff09;是一种利用电脑模拟产生一个三维的虚拟世界&#xff0c;提供用户关于视觉、听觉、触觉等感官的模拟体验的技术。随着科技的进步&#xff0c;VR已经被广泛应用到许多领域&#xff0c;包括游戏、教育、医疗、房…

Outlook如何恢复已删除邮件

Outlook如何恢复已删除邮件 操作指引&#xff1a; Outlook客户端恢复最近7天删除的邮件&#xff1a; Outlook客户端要求最新版本&#xff0c;如没有如下选项&#xff0c;建议联机更新windows update 网页邮箱恢复最近7天删除的邮件&#xff1a;

极智芯 | GPU架构与计算能力

欢迎关注我的公众号 [极智视界]&#xff0c;获取我的更多经验分享 大家好&#xff0c;我是极智视界&#xff0c;本文分享一下 GPU架构与计算能力。 邀您加入我的知识星球「极智视界」&#xff0c;星球内有超多好玩的项目实战源码和资源下载&#xff0c;链接&#xff1a;https:…