Linux线程控制
本篇我将学习如何使用多线程。要使用多线程,因为Linux没有给一般用户直接提供操作线程的接口,我们使用的接口,都是系统工程师封装打包成原生线程库中的。那么就需要用到原生线程库。因此,需要引入-lpthread,即连接原生线程库。
原生线程库:#include <pthread.h>
自动化构建工具:
mythread:mythread.cgcc -o $@ $^ -lpthread
.PHONY:clean
clean:rm -f mythread
创建线程
创建线程。
功能:创建一个新的线程
原型:int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void*(*start_routine)(void*), void* arg);
参数:
thread : 返回线程ID.
attr : 设置线程的属性,attr为NULL表示使用默认属性.
start_routine : 是个函数地址,线程启动后要执行的函数.
arg : 传给线程启动函数的参数.
返回值:成功返回0;失败返回错误码.
获取调用它的线程id。即哪个线程调用了它,就能够获得自己的id。
函数原型:pthread_t pthread_self(void);
功能:获取一个线程id,即谁调用它,就获取谁的线程id
头文件:#include <pthread.h>
参数:无
返回值:成功返回这个id。这个函数总是成功的!
创建一个线程,代码如下:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>void *thread_run(void *args)
{while(1){//使用pthread_self()获取idprintf("我是新线程[%s],我的线程ID是: %lu\n",(const char*)args,pthread_self());sleep(1);}
}int main()
{pthread_t tid;//第一个参数为线程id,第二个为线程属性,设置为NULL默认值//第三个参数是线程执行的方法,第四个是传给第三个参数的参数pthread_create(&tid,NULL,thread_run,(void*)"new thread");while(1){printf("我是主线程,我创建的线程ID是: %lu,我的线程ID是: %lu\n",tid,pthread_self());sleep(1);}return 0;
}
结果如下,能看到两个线程的ID不一样,那就证明了单个进程中存在着两个线程。

创建多个线程,代码如下:
使用数组来存放线程id。注意此时thread_run()函数被重入了!
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>void *thread_run(void *args)
{while(1){//使用pthread_self()获取id// printf("我是新线程[%s],我创建的线程ID是: %lu\n",(const char*)args,pthread_self());sleep(3);}
}int main()
{//创建五个线程,保存在数组中pthread_t tid[5];//第一个参数为线程id,第二个为线程属性,设置为NULL默认值//第三个参数是线程执行的方法,第四个是传给第三个参数的参数int i = 0;for(i = 0;i<5;++i){pthread_create(tid+1,NULL,thread_run,(void*)"new thread");}while(1){printf("我是主线程,我的thread ID:%lu\n",pthread_self());printf("######################begin########################\n");for(i = 0;i<5;++i){ printf("我是主线程,我创建的线程[%d]ID是: %lu,我的线程ID是: %lu\n",i,tid[i],pthread_self());}printf("######################end######################\n");sleep(1);}return 0;
}
结果如下:

通过ps -aL查看当前线程。可以看到,PID和LWP相同的就是主线程,其它的都是新线程。LWP是线程id。

线程等待
一般而言,线程也是需要等待的,如果不等待,就可能会导致类似于"僵尸进程"的问题。
功能:等待线程结束
原型:int pthread_join(pthread_t thread, void** value_ptr);
参数:
thread : 线程ID
value_ptr : 它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
写一个简单的测试,主线程在等待,10秒后打印111.
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>void *thread_run(void *args)
{int num = *(int*)args;while(1){//使用pthread_self()获取idprintf("我是新线程[%d],我创建的线程ID是: %lu\n",num,pthread_self());sleep(10);break;}//随便返回一个值用于测试return (void*)111;
}#define NUM 1
int main()
{pthread_t tid[NUM];int i = 0;for(i = 0;i<NUM;++i){pthread_create(tid+1,NULL,thread_run,(void*)&i);sleep(1);}//指针变量可以当某个数据的容器void *status = NULL;//获得退出信息pthread_join(tid[0],&status);printf("ret: %d\n",(int)status);return 0;
}
线程只能一个个等。
线程终止
线程终止的方案有:
1.函数中的return。对于这个方案有两种情况:第一种情况是在main函数中的return,此时代表进程和主线程都退出了。第二种情况是其它函数中的return,代表该线程的退出。
2.使用函数pthread_exit().
功能:线程终止
原型:void pthread_exit(void* value_ptr);
参数:
value_ptr : z指的是退出后的返回值,也就是return X。value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>void *thread_run(void *args)
{int num = *(int*)args;while(1){//使用pthread_self()获取idprintf("我是新线程[%d],我创建的线程ID是: %lu\n",num,pthread_self());sleep(2);break;}pthread_exit((void*)123);
}#define NUM 1
int main()
{ pthread_t tid[NUM];int i = 0;for(i = 0;i<NUM;++i){pthread_create(tid+1,NULL,thread_run,(void*)&i);sleep(1);}//指针变量可以当某个数据的容器void *status = NULL;//获得退出信息pthread_join(tid[0],&status);printf("ret: %d\n",(int)status);return 0;
}
如果使用exit(),那么会将进程和全部线程都终止掉。
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了,函数退出代表函数栈帧被销毁,从而这个内存单元也被销毁了。
3.使用pthread_cancel函数取消目标线程。
功能:取消一个执行中的线程
原型:int pthread_cancel(pthread_t thread);
参数:
thread : 线程ID
返回值:成功返回0;失败返回错误码,退出码为-1
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>void *thread_run(void *args)
{int num = *(int*)args;while(1){//使用pthread_self()获取idprintf("我是新线程[%d],我创建的线程ID是: %lu\n",num,pthread_self());sleep(2);//break;}//pthread_exit((void*)123);//随便返回一个值用于测试//return (void*)111;
}#define NUM 1
int main()
{pthread_t tid[NUM];int i = 0;for(i = 0;i<NUM;++i){pthread_create(tid+1,NULL,thread_run,(void*)&i);sleep(1);}printf("wait sub thread...\n");//等5秒钟sleep(5);printf("cancel sub thread...\n");//取消线程pthread_cancel(tid[0]);//指针变量可以当某个数据的容器void *status = NULL;//获得退出信息pthread_join(tid[0],&status);printf("ret: %d\n",(int)status);return 0;
}
当一个新线程被取消后,退出码为-1,即PTHREAD_ CANCELED。
当把主线程取消,但新线程没有被取消,此时新线程依旧在运行着,并且主线程会进入"僵尸状态"(说明:线程没有僵尸状态这个东东,是有类似僵尸进程的问题)。因此我们一般不能用新线程去取消主线程。

线程分离
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
线程分离后,不需要被join终止,只需运行结束后会自动释放Z。分离后的线程相对于是同一屋檐下的陌生人,即这个线程在跟同一个进程内的线程毫无关系了,此时一定不能对其join,因为会失败。
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。
线程分离一般的应用场景是主线程不退出,新线程处理完任务后退出。
功能:分离线程
原型:int pthread_detach(pthread_t thread);
参数:
thread : 线程ID
返回值:成功返回0;
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>void *thread_run(void *args)
{//分离pthread_detach(pthread_self());int num = *(int*)args;while(1){//使用pthread_self()获取idprintf("我是新线程[%d],我创建的线程ID是: %lu\n",num,pthread_self());sleep(2);break;}//随便返回一个值用于测试return (void*)111;
}#define NUM 1
int main()
{pthread_t tid[NUM];int i = 0;for(i = 0;i<NUM;++i){pthread_create(tid+1,NULL,thread_run,(void*)&i);sleep(1);}printf("wait sub thread...\n");//等5秒钟sleep(5);printf("cancel sub thread...\n");//指针变量可以当某个数据的容器void *status = NULL;int ret = 0;//获得退出信息ret = pthread_join(tid[0],&status);printf("ret: %d,status: %d\n",ret,(int)status);return 0;
}
LPW的解释

在使用ps -aL查看线程情况时,LWP为内核LWP,我们最好不要叫它线程ID,因为在Linux中没有线程这玩意,我们所说的线程,都是进程PCB模拟出来的,属于轻量级进程。
对于LWP,它的值跟我们在测试代码时得出的结果(线程的ID)不一样,一个是原生线程库的,一个是内核的。
下面将好好分析一下,原生线程库中的"线程pid"的本质。
先来说结果,我们通过pthread_self()获取的线程id,其实是虚拟内存地址!

我们都知道,每一个线程都要有运行的临时数据,因此每个线程都要有自己的私有栈结构!也需要拥有描述线程的用户控制块!但是在虚拟地址空间中的栈结构,不可能会分成很多份给每一个线程的,它是属于主线程和进程的!
每一个新线程所拥有的栈结构等等,其实都是由原生线程库提供的!每一个线程跟每一个库提供的线程栈和线程局部存储等组成的用户控制块都是一一对应的,是以1:1的比例对对应着!
那么如何区找到需要找到的线程,就需要用到一个地址去找,并且每一个描述线程的用户控制块都会保存着每一个线程对应的PWD!这个地址就是每一个用户控制块的地址!
总结:
①pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和内核LWP不是一回事,前者是原生线程库中的,一个是内核LWP。
②前面讲的LWP(线程ID)属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
③pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
④线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID。
相关文章:
Linux线程控制
本篇我将学习如何使用多线程。要使用多线程,因为Linux没有给一般用户直接提供操作线程的接口,我们使用的接口,都是系统工程师封装打包成原生线程库中的。那么就需要用到原生线程库。因此,需要引入-lpthread,即连接原生…...
【LeetCode】剑指 Offer(20)
目录 题目:剑指 Offer 38. 字符串的排列 - 力扣(Leetcode) 题目的接口: 解题思路: 代码: 过啦!!! 写在最后: 题目:剑指 Offer 38. 字符串的…...
FutureTask中的outcome字段是如何保证可见性的?
最近在阅读FutureTask的源码是发现了一个问题那就是源码中封装结果的字段并没有使用volatile修饰,源码如下:public class FutureTask<V> implements RunnableFuture<V> {/*** 状态变化路径* Possible state transitions:* NEW -> COMPLET…...
直播回顾 | 聚焦科技自立自强,Bonree ONE 助力国产办公自动化平稳替代
3月5日,两会发布《政府工作报告》,强调科技政策要聚焦自立自强。 统计显示,2022年金融信创项目数同比增长300%,金融领域信创建设当前已进入发展爆发期,由国有大型银行逐渐向中小型银行、非银金融机构不断扩展。信创云…...
深入理解Linux进程
进程参数和环境变量的意义一般情况下,子进程的创建是为了解决某个问题。那么解决问题什么问题呢?这个就需要进程参数和环境变量来进行决定的。子进程解决问题需要父进程的“数据输入”(进程参数 & 环境变量)设计原则:3.1 子进程启动的时候…...
Vue3之组件间的双向绑定
何为组件间双向绑定 我们都知道当父组件改变了某个值后,如果这个值传给了子组件,那么子组件也会自动跟着改变,但是这是单向的,使用v-bind的方式,即子组件可以使用父组件的值,但是不能改变这个值。组件间的…...
Java语法基础(一)
目录 代码注释方法 编码规范 基本数据类型及取值范围 变量和常量的声明与赋值 变量 常量 标识符 基本数据类型的使用 整数类型的使用 浮点类型的使用 布尔类型的使用 字符类型的使用 代码注释方法 单行注释:使用“//”进行单行注释多行注释:使…...
优思学院|零质量控制是什么概念?
零质量控制(Zero Quality Control)是指一个理想的系统,可以生产没有任何缺陷的产品,因此不需要频繁的检查,从而节省时间和金钱。那些追求过程优化并致力于持续过程改进的组织将零质量控制(Zero Quality Con…...
2023-03-09 CMU15445-Query Execution
摘要: CMU15445, Project #3 - Query Execution 参考: Project #3 - Query Execution | CMU 15-445/645 :: Intro to Database Systems (Fall 2022) https://github.com/cmu-db/bustub 要求: OVERVIEW At this point in the semester, you have implemented the internal co…...
vuedraggable的使用
Draggable为基于Sortable.js的vue组件,用以实现拖拽功能。 特性 支持触摸设备 支持拖拽和选择文本 支持智能滚动 支持不同列表之间的拖拽 不以jQuery为基础 和视图模型同步刷新 和vue2的国度动画兼容 支持撤销操作 当需要完全控制时,可以抛出所有变化 可…...
双馈风力发电机-900V直流混合储能并网系统MATLAB仿真
MATLAB2016b主体模型:双馈感应风机模块、采用真实风速数据。混合储能模块、逆变器模块、转子过电流保护模块、整流器控制模块、逆变器控制模块。直流母线电压:有功、无功输出(此处忘记乘负一信号输出),所以是负的。蓄电…...
leader选举过程
启动electionTimer,进行leader选举。 一段时间没有leader和follower通信,就会超时,开始选举leader过程。有个超时时间,如果到了这个时间,就会触发一个回调函数。具体如下: private void handleElectionTimeout() {boo…...
建造者模式
介绍 Java中的建造者模式是一种创建型设计模式,它的主要目的是为了通过一系列简单的步骤构建复杂的对象,允许创建复杂对象的不同表示形式,同时隐藏构造细节.它能够逐步构建对象,即先创建基本对象,然后逐步添加更多属性或部件,直到最终构建出完整的对象. 该模式的主要思想是将…...
IO与NIO区别
一、概念 NIO即New IO,这个库是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多。在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。 二、NIO和IO的主要区别 下表总结了Java I…...
无监督循环一致生成式对抗网络:PAN-Sharpening
Unsupervised Cycle-Consistent Generative Adversarial Networks for Pan Sharpening (基于无监督循环一致生成式对抗网络的全色锐化) 基于深度学习的全色锐化近年来受到了广泛的关注。现有方法大多属于监督学习框架,即对多光谱࿰…...
ArrayList源码分析(JDK17)
ArrayList类简介类层次结构构造无参构造有参构造添加元素add:添加/插入一个元素addAll:添加集合中的元素扩容mount与迭代器其他常见方法不常见方法不常见方法的源码和小介绍常见方法的源码和小介绍积累面试题ArrayList是什么?可以用来干嘛?Ar…...
数字IC/FPGA面试笔试准备(自用待填坑)
文章目录 前言常见的IC问题数字电路基础问题Verilog & SV跨时钟域信号处理类综合与时序分析类低功耗方法STA(静态时序分析)RTL设计(包含手撕代码)总线问题AXIAPBAHB体系结构的问题RISCV的问题一些笔试选择题前言 这是实验室师兄面试过程中整理的面试和笔试题目,目前只有题…...
基于多任务融合的圣女果采摘识别算法研究
基于多任务融合的圣女果采摘识别算法研究 1、简介 本文主要解决圣女果生产销售环节中,现有的流程是采摘成熟的圣女果,再对采摘下的果实进行单独的品质分级,不仅费时费力,而且多增加一个环节,也增加了对果实的二次伤害…...
又一个开源第一!飞桨联合百舸,Stable Diffusion推理速度遥遥领先
AIGC(AI Generated Content),即通过人工智能方法生成内容,是当前深度学习最热门的方向之一。其在绘画、写作等场景的应用也一直层出不穷,其中,AI绘画是大家关注和体验较多的方向。 Diffusion系列文生图模型可以实现AI绘画应用&…...
数据链路层及交换机工作原理
目录 一,帧格式 1.1 帧头类型字段的作用 1.2 MAC地址 1.3 MTU值 二,交换机工作原理 2.1 交换机的端口 2.2 端口状态 三,交换机基本工作模式及命令 3.1 交换机的工作模式: 3.2 命令 一,帧格式 其中类型是指&am…...
【WiFi帧结构】
文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成:MAC头部frame bodyFCS,其中MAC是固定格式的,frame body是可变长度。 MAC头部有frame control,duration,address1,address2,addre…...
安宝特方案丨XRSOP人员作业标准化管理平台:AR智慧点检验收套件
在选煤厂、化工厂、钢铁厂等过程生产型企业,其生产设备的运行效率和非计划停机对工业制造效益有较大影响。 随着企业自动化和智能化建设的推进,需提前预防假检、错检、漏检,推动智慧生产运维系统数据的流动和现场赋能应用。同时,…...
Psychopy音频的使用
Psychopy音频的使用 本文主要解决以下问题: 指定音频引擎与设备;播放音频文件 本文所使用的环境: Python3.10 numpy2.2.6 psychopy2025.1.1 psychtoolbox3.0.19.14 一、音频配置 Psychopy文档链接为Sound - for audio playback — Psy…...
ip子接口配置及删除
配置永久生效的子接口,2个IP 都可以登录你这一台服务器。重启不失效。 永久的 [应用] vi /etc/sysconfig/network-scripts/ifcfg-eth0修改文件内内容 TYPE"Ethernet" BOOTPROTO"none" NAME"eth0" DEVICE"eth0" ONBOOT&q…...
Linux 内存管理实战精讲:核心原理与面试常考点全解析
Linux 内存管理实战精讲:核心原理与面试常考点全解析 Linux 内核内存管理是系统设计中最复杂但也最核心的模块之一。它不仅支撑着虚拟内存机制、物理内存分配、进程隔离与资源复用,还直接决定系统运行的性能与稳定性。无论你是嵌入式开发者、内核调试工…...
Golang——6、指针和结构体
指针和结构体 1、指针1.1、指针地址和指针类型1.2、指针取值1.3、new和make 2、结构体2.1、type关键字的使用2.2、结构体的定义和初始化2.3、结构体方法和接收者2.4、给任意类型添加方法2.5、结构体的匿名字段2.6、嵌套结构体2.7、嵌套匿名结构体2.8、结构体的继承 3、结构体与…...
基于Java+VUE+MariaDB实现(Web)仿小米商城
仿小米商城 环境安装 nodejs maven JDK11 运行 mvn clean install -DskipTestscd adminmvn spring-boot:runcd ../webmvn spring-boot:runcd ../xiaomi-store-admin-vuenpm installnpm run servecd ../xiaomi-store-vuenpm installnpm run serve 注意:运行前…...
华为OD机试-最短木板长度-二分法(A卷,100分)
此题是一个最大化最小值的典型例题, 因为搜索范围是有界的,上界最大木板长度补充的全部木料长度,下界最小木板长度; 即left0,right10^6; 我们可以设置一个候选值x(mid),将木板的长度全部都补充到x,如果成功…...
小木的算法日记-多叉树的递归/层序遍历
🌲 从二叉树到森林:一文彻底搞懂多叉树遍历的艺术 🚀 引言 你好,未来的算法大神! 在数据结构的世界里,“树”无疑是最核心、最迷人的概念之一。我们中的大多数人都是从 二叉树 开始入门的,它…...
LangChain 中的文档加载器(Loader)与文本切分器(Splitter)详解《二》
🧠 LangChain 中 TextSplitter 的使用详解:从基础到进阶(附代码) 一、前言 在处理大规模文本数据时,特别是在构建知识库或进行大模型训练与推理时,文本切分(Text Splitting) 是一个…...
