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

🕺作者: 主页

我的专栏
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

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

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

相关文章

燃气管网监测系统|全面保障燃气安全

根据新华日报的报道&#xff0c;2023年上半年&#xff0c;我国共发生了294起燃气事故&#xff0c;造成了57人死亡和190人受伤&#xff0c;燃气事故的发生原因有很多&#xff0c;其中涉及到燃气泄漏、设备故障等因素。因此&#xff0c;加强燃气安全管理&#xff0c;提高城市的安…

一文让你了解网络刷卡器的特点和优势

网络刷卡器一款高性能的多协议电子标签读写器&#xff0c;保持高识读率的同时实现对电子标签的快速读写处理&#xff0c;广泛应用于物流追踪、个人身份识别、人员管理、智能停车场、门禁考勤、公交一卡通、餐饮、金融等多个领域。 特点和优势&#xff1a; 1&#xff09;低功耗、…

python 路径变更后 pip 运行报错

python 路径变更后 pip 运行报错 Fatal error in launcher: Unable to create process using "d:\python-3.6.6\python .exe" "D:\python-3........出现这种原因是因为生产 Scripts\pip.exe中存在绝对路径&#xff0c;因此当python变更过路径后所有 Scripts目…

新型的铁塔基站“能源管家”

安科瑞 崔丽洁 引言&#xff1a;随着5G基站的迅猛发展&#xff0c;基站的能耗问题也越来越突出&#xff0c;高效可靠的基站配电系统方案&#xff0c;是提高基站能耗使用效率&#xff0c;实现基站节能降耗的重要保证&#xff0c;通过多回路仪表监测每个配电回路的用电负载情况&a…

微信小程序广告banner、滚动屏怎么做?

使用滑块视图容器swiper和swiper-item可以制作滚动屏&#xff0c;代码如下&#xff1a; wxml: <swiper indicator-dots indicator-color"rgba(255,255,255,0.5)" indicator-active-color"white" autoplay interval"3000"><swiper-ite…

VR全景技术在城市园区发展中有哪些应用与帮助

引言&#xff1a; 在数字化时代的浪潮中&#xff0c;虚拟现实&#xff08;VR&#xff09;全景技术逐渐融入各个领域&#xff0c;也为城市园区展示带来了全新的可能性。 一&#xff0e;VR全景技术简介 虚拟现实全景技术是一种通过全景图像和视频模拟真实环境的技术。通过相关设…

一篇文章搞明白js运行机制——事件循环

1、解释 JavaScript 的执行机制。 JavaScript 的执行机制基于事件循环。事件循环包括一个任务队列&#xff08;Task Queue&#xff09;和一个微任务队列&#xff08;Microtask Queue&#xff09;。当一个函数被调用时&#xff0c;它被添加到微任务队列中。事件循环每次迭代都会…

腾讯云轻量应用服务器优惠购买攻略,怎么购买腾讯云优惠划算?

腾讯云轻量应用服务器&#xff08;Lighthouse&#xff09;是一款专为中小型企业和个人开发者打造的云服务器产品。它具备简单易用、成本低廉和高性能等特点&#xff0c;为用户提供了便捷高效的云服务器解决方案。 腾讯云轻量应用服务器采用了简单直观的图形化界面&#xff0c;…

linux grub2 不引导修复 grub2-install:error:/usr/lib/grub/x86_64-efi/modinfo.sh

系统部署在物理机上&#xff0c;开机后一直pxe不进系统&#xff0c;怀疑GRUB丢失。 查看bios 里 采用uefi 启动方式&#xff0c; 无硬盘系统引导选项&#xff0c; 且BMC设置为硬盘永久启动也无效。 挂载光驱ISO进入救援模式,sda为系统盘&#xff0c;重装grub报错 grub2-inst…

ProtoBuf的学习和使用(C++)

ProtoBuf的学习和使用---C ⼀、初识ProtoBuf序列化和反序列化的概念ProtoBuf是什么?ProtoBuf工作特点 二、主要学习思路三、快速上手四、proto3语法详解1.字段规则2.消息类型的定义与使⽤实际操练 3.enum枚举类型enum注意事项enum实操 4.Any类型Any类型实操 5.oneof类型oneof类…

FTP网络问题排查

Linux探测路径MTU&#xff1a; ping大包&#xff1a; [test]$ ifconfig eth0: flags4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1280 [test]$ ping -M do -s 1252 172.18.98.3 PING 172.18.98.3 (172.18.98.3) 1252(1280) bytes of data. 1260 bytes from 172.18.98.3: …

早上好问候语,清晨阳光暖心的早安问候语录

1、在我们每一个人的生命里&#xff0c; 会遇到各种各样的陪伴。陪伴很温暖&#xff0c; 相识不易&#xff0c;陪伴更是一种缘分&#xff01;祝福每一位朋友&#xff0c; 在人生的岁月里都能相伴而行&#xff0c; 一起度过美好的快乐时光&#xff01; 2、天天问候&#xff0c;真…

日志及其框架

日志技术的概述 日志 生活中的日志&#xff1a; 生活中的日志就好比日记&#xff0c;可以记录你生活的点点滴滴。 程序中的日志&#xff1a; 程序中的日志可以用来记录程序运行过程中的信息&#xff0c;并可以进行永久存储。 以前记录日志的方式&#xff08;输出语句&#…

为什么要安装田间气象站?

随着农业科技的发展&#xff0c;越来越多的农民朋友开始关注如何利用科技手段来提高农业生产效益。其中&#xff0c;安装田间气象站成为了许多农民朋友的选择之一&#xff0c;为什么会有这种情况呢&#xff1f;安装田间气象站会带来哪些优势呢&#xff1f; 一、了解气候变化 气…

响应式珠宝首饰展示网站模板源码带后台

模板信息&#xff1a; 模板编号&#xff1a;6201 模板编码&#xff1a;UTF8 模板颜色&#xff1a;黑白 模板分类&#xff1a;服饰、箱包、礼品、玩具 适合行业&#xff1a;珠宝饰品类企业 模板介绍&#xff1a; 本模板自带eyoucms内核&#xff0c;无需再下载eyou系统&#xf…

实现资产与财务的无缝对接,易点易动固定资产管理系统帮您消除数据难题

在现代企业中&#xff0c;固定资产管理和财务管理是两个密不可分的环节。然而&#xff0c;许多企业面临着固定资产和财务数据不一致、不准确的问题&#xff0c;给企业的决策和运营带来了困扰。为了解决这一难题&#xff0c;易点易动固定资产管理系统应运而生。该系统通过实现资…

基于C#开发的任天堂 Switch 开源模拟器

今天给大家推荐一款基于C#开发的任天堂 Switch 开源模拟器&#xff0c;可方便开发人员来测试游戏&#xff0c;也用于娱乐。 01 项目简介 Ryujinx 是一个开源的任天堂 Switch 模拟器&#xff0c;可以在 PC 上模拟运行 Switch 游戏。采用C#开发&#xff0c;基于 .NET Core技术框…

3.6、linux调试器:gdb

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 背景 程序员在做项目写代码时&#xff0c;每完成一个功能&#xff0c;需要自己先测试一下&#xff0c;看看能不能跑之类的&#xff0c;然后交给测试人员&#xff0c;但是测试人员就代表着用户&#xff0c;用户不需要调试&am…

区块链链游合约系统开发项目模式技术方案

​随着区块链技术的发展&#xff0c;链游合约系统开发逐渐成为了一个备受关注的项目。本文将探讨区块链链游合约系统开发项目的技术方案&#xff0c;包括项目背景、开发目标、技术架构、系统流程、安全措施等方面的内容。 一、项目背景 链游是一种基于区块链技术的游戏&#xf…

压测必经之路,Jmeter分布式压测教程!

01、分布式压测原理 Jemter分布式压测是选择其中一台作为调度机&#xff08;master&#xff09;&#xff0c;其他机器作为执行机&#xff08;slave&#xff09;&#xff1b;当然一台机器也可以既做调度机&#xff0c;也做执行机。 调度机执行脚本的时候&#xff0c;master将会…