Linux中线程创建,线程退出,线程接合
线程的简单了解
之前我们了解过 task_struct 是用于描述进程的核心数据结构。它包含了一个进程的所有重要信息,并且在进程的生命周期内保持更新。我们想要获取进程相关信息往往从这里得到。
- 在Linux中,线程的实现方式与进程类似,每个线程都有一个task_struct结构体,用于存储线程的信息。线程的task_struct结构体比进程的task_struct结构体要小,包含的信息更少。
- 进程是操作系统资源分配的最小单位,而线程是操作系统调度的最小单位。
- 线程之间的切换通常比进程切换更高效,因为线程共享进程的资源,不需要像进程切换那样保存和恢复大量的资源信息。

windows中的线程和Linux中线程区别
在Windows操作系统,内核中有真线程,名为TCB :线程控制块。需要维护进程与线程之间的调度关系算法,这过于复杂。
在Linux中,由于线程的控制块与进程控制块相似性非常高,所以直接复用了PCB的结构体——task_struct ,用PCB模拟线程的TCB。所以Linux没有真正意义上的线程,而是用进程方案模拟的线程。这样做的好处是复用代码和结构更简单,好维护,效率更高,也更安全。
线程的特性
多线程的优点
- 同时执行多个任务: 多线程允许程序同时执行多个任务,而不是按顺序一个接一个地执行。
- 提高响应速度: 对于需要处理大量并发请求的程序(如Web服务器),多线程可以显著提高程序的响应速度。
- 更小开销,更快的切换: 线程切换的开销也比进程切换要小,这使得多线程程序可以更高效地进行任务切换。
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现,可以有效提高计算效率,但是注意:线程不是越多越好,正常情况下最合适的原则是:进程/线程与cpu个数/核数保持一致
多线程的缺点
- 共享资源竞争: 多个线程共享进程的地址空间,当它们同时访问和修改共享资源时,可能会出现竞争条件,导致数据不一致或程序错误。
- 同步机制复杂: 为了解决线程安全问题,需要使用线程同步机制(如互斥锁、条件变量等),这些机制会增加编程的复杂性,容易出错。
- 上下文切换开销: 线程切换需要保存和恢复线程的上下文,这会消耗一定的CPU时间。过多的线程切换可能会降低程序的效率。
- 线程间依赖: 线程之间可能存在依赖关系,一个线程的执行可能会影响到其他线程的执行。如果处理不当,可能会导致程序出现意外错误。
- 调试困难:多线程程序的执行顺序是不确定的,这使得程序的调试变得更加困难。由于线程的执行受到多种因素的影响,一些错误可能很难复现,增加了调试的难度。
PROSIX线程库
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
要使用这些函数库,要通过引入头文件 <pthread.h>
并且链接这些线程函数库时要使用编译器命令的“-lpthread”选项
之前我们使用的都是linux中的基础标准库,这些标准库在编译的时候会自动帮我们进行链接,我们只需要包含一个头文件,不需要手动链接。但是对于线程库的话默认不会帮我们链接,除了需要我们程序中包含对应头文件,还需要编译的时候手动链接。
包含头文件和编译时链接区分
- 包含头文件(#include): 这是在源代码文件中做的,用于告诉编译器程序中使用了哪些函数、变量、类型等。头文件通常包含函数声明、宏定义、结构体定义等。
- 编译时链接: 这是在编译命令中做的,用于告诉链接器将程序中使用的函数和变量与它们在库文件中的具体实现链接起来。库文件通常包含编译好的函数和变量的二进制代码。
包含头文件的作用:
- 让编译器理解代码: 头文件相当于一个“接口说明书”,告诉编译器程序中使用了哪些“零件”(函数、变量等),以及这些“零件”的规格(参数类型、返回值类型等)。
- 提供类型检查: 编译器可以根据头文件中的声明来检查程序中函数和变量的使用是否正确,避免类型错误。
编译时链接的作用:
- 生成可执行文件: 链接器将程序中使用的函数和变量与它们在库文件中的实现“组装”起来,生成最终的可执行文件。
- 链接外部代码: 程序中使用的某些函数和变量可能不是由自己编写的,而是由其他人或组织提供的,这些代码通常放在库文件中。链接器将这些外部代码链接到程序中,使得程序可以使用这些外部功能。
线程创建
pthread_create函数介绍
函数作用:创建一个新的线程。这个线程在创建后会并行执行指定的线程函数
头文件:#include <pthread.h>
函数原型:
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine) (void *),void *arg);
参数:
- thread: 一个指向
pthread_t类型变量的指针,用于存储新创建线程的 ID。 - attr: 一个指向
pthread_attr_t类型变量的指针,用于设置新线程的属性。如果设置为NULL,则使用默认属性。通常我们设置成NULL就行 - start_routine: 一个函数指针,表示新线程要执行的函数。该函数必须接受一个
void *类型的参数,并返回一个void *类型的值。 - arg: 一个指向
void *类型变量的指针,表示传递给start_routine线程函数的参数。如果我们不需要传递任何数据给线程函数,完全可以将它设置为 NULL
返回值:
- 成功: 返回 0。
- 失败: 返回一个非零的错误码,表示创建线程失败的原因。
线程函数的定义:
线程函数必须符合 void *(*start_routine)(void *) 的函数签名,即接收一个 void * 类型的参数并返回一个 void * 类型的值。
简单的线程创建例子
例子比较简单,主要就是创建了一个新的线程,然后主线程和新线程同时执行,主线程输出26个英文字母,新线程输出数字0-9。
编译代码的时候记得加上编译链接选项:-lpthread
gcc a.c -o a -lpthread
#include <stdio.h>
#include <pthread.h>void *thread_function(void *arg) {int i=0;while(1){fprintf(stderr,"%d",i);i++;if(i==10){i=0;}}
}int main() {pthread_t thread_id;int ret = pthread_create(&thread_id, NULL, thread_function, NULL);if (ret != 0) {perror("pthread_create failed");return 1;}printf("Thread created successfully\n");int i=0;while(1){fprintf(stderr,"%c",'a'+i);i++;if(i==26){i=0;}}return 0;
}
最终看到的效果就是主线程和新线程双线执行输出。
线程间共享资源与不共享资源
共享资源
- 堆(Heap): 存储进程中动态分配的对象。
- 代码段(Code Segment): 存储程序的指令。
- 数据段(Data Segment): 存储进程的全局变量和静态变量。
- 文件描述符表: 存储进程打开文件的信息。
- 信号处理函数: 用于处理进程接收到的信号。
不共享资源
- 栈(Stack): 每个线程都有自己的栈,用于存储局部变量、函数调用信息等。
- 寄存器(Registers): 每个线程都有一组寄存器,用于存储线程执行过程中的临时数据。
- 线程 ID: 每个线程都有一个唯一的线程 ID,用于标识线程。
使用共享资源时需要注意的问题:
- 竞态条件(Race Condition): 多个线程同时访问和修改共享资源时,可能会导致数据不一致的问题。
- 死锁(Deadlock): 多个线程互相等待对方释放资源,导致程序无法继续执行的问题。
解决竞态条件和死锁问题的方法:
- 互斥锁(Mutex): 用于保护共享资源,同一时刻只允许一个线程访问。
- 条件变量(Condition Variable): 用于线程之间的同步,当一个线程等待某个条件满足时,可以使用条件变量进行阻塞。
- 信号量(Semaphore): 用于控制同时访问共享资源的线程数量。
多线程共享资源同时访问出错例子
场景:假设有一个共享的计数器变量 counter,初始值为 0。现在有多个线程同时对 counter 进行加 1 操作。
预期结果:由于有 10 个线程,每个线程执行 100000 次加 1 操作,因此最终的 counter 值应该为 10 * 100000 = 1000000。
实际结果:实际运行结果通常会小于 10000。
原因分析:
当多个线程同时访问 counter 变量时,由于线程切换的存在,可能会导致以下情况:
- 线程 A 读取
counter的值。 - 线程 A 被切换出去,线程 B 开始执行。
- 线程 B 读取
counter的值。 - 线程 B 将
counter的值加 1。 - 线程 B 被切换出去,线程 A 继续执行。
- 线程 A 将之前读取的
counter值加 1,并写回。
这样,线程 A 和线程 B 都只进行了一次加 1 操作,但 counter 的值只增加了 1,而不是 2。这种情况称为竞态条件。
解决方法:可以使用互斥锁(Mutex)来保护共享资源 counter,确保同一时刻只有一个线程可以访问它。
通过使用互斥锁,可以保证每个线程对 counter 的加 1 操作都是原子性的,从而避免竞态条件,得到正确的结果。
#include <stdio.h>
#include <pthread.h>#define NUM_THREADS 10
#define INCREMENTS 100000int counter = 0;void *increment_counter(void *arg) {for (int i = 0; i < INCREMENTS; i++) {counter++;}return NULL;
}int main() {pthread_t threads[NUM_THREADS];for (int i = 0; i < NUM_THREADS; i++) {pthread_create(&threads[i], NULL, increment_counter, NULL);}//作用是等待多个线程执行结束。它通常出现在多线程程序中,//用于确保主线程在所有子线程完成任务后才退出。for (int i = 0; i < NUM_THREADS; i++) {pthread_join(threads[i], NULL);}printf("Expected counter value: %d\n", NUM_THREADS * INCREMENTS);printf("Actual counter value: %d\n", counter);return 0;
}

线程退出
pthread_exit 函数介绍
函数作用:结束调用该函数的线程,同时还可以传递一个退出状态值给其他线程
头文件:#include <pthread.h>
函数原型:
void pthread_exit(void *retval);
参数:
retval 参数可以用于传递一个退出状态值给其他线程,通常通过 pthread_join() 函数来接收这个值。如果不需要传递退出状态,可以将 retval 设置为 NULL。
僵尸线程
首先我们来回顾一下僵尸进程:
僵尸进程
- 当一个进程结束运行时,内核不会立即释放它占用的所有资源,而是将其状态设置为僵尸态(Zombie)。
- 僵尸进程会保留一些基本信息(如进程ID、退出状态等),以便父进程可以获取到子进程的退出信息。
- 父进程需要调用
wait()或waitpid()等函数来回收僵尸进程的资源,否则僵尸进程会一直存在,占用系统资源。
然后我们来看一下线程的僵尸态
与进程类似,当一个线程结束运行时,它也会进入一个类似于僵尸态的状态。
- 状态保留: 线程退出后,其占用的大部分资源(如栈空间)会被自动回收,但是线程也会保留一些状态信息,例如退出状态,以便其他线程(通常是主线程)可以通过
pthread_join()函数来获取。
- 回收方式: 线程的“回收”主要通过
pthread_join()函数来实现。当主线程调用pthread_join()函数等待某个线程结束时,实际上就是在“回收”该线程的状态信息。
僵尸态的重要性
- 无论是进程还是线程,僵尸态的存在都是为了让父进程或主线程能够获取到子进程或子线程的退出信息。
- 这些退出信息可能包含执行结果、错误码等,对于程序的调试和错误处理非常有帮助。
线程接合
pthread_join函数介绍
函数作用:阻塞当前线程,直到指定的线程执行完毕,适用于线程间的同步
头文件:#include <pthread.h>
函数原型:
int pthread_join(pthread_t thread, void **retval);
参数:
thread:要等待的线程的线程 ID。这是一个由 pthread_create 创建的线程 ID。
retval:这是一个指向指针的指针,函数会把目标线程的退出状态通过该指针返回。如果目标线程没有返回任何值,可以传递 NULL。
其实我蛮不理解这里为什么使用二级指针的,在我看来pthread_exit 传递的参数是一个一级指针,但是这里pthread_join选择一个一级指针来对应赋值就可以了,不太理解为什么要使用二级指针
返回值:
- 成功: 返回 0。
- 失败: 返回一个非零的错误码。
适用场景
- 同步线程: 当一个线程需要等待另一个线程完成后才能继续执行时,可以使用
pthread_join()函数进行同步。 - 获取线程返回值: 有些线程会返回一个值,表示它们的执行结果。可以使用
pthread_join()函数获取这个返回值。 - 资源回收: 当一个线程结束后,它的资源不会立即被释放。需要调用
pthread_join()函数才能回收这些资源。
线程分离态(了解)
线程的默认状态
默认情况下,新创建的线程都处于非分离态 (Joinable State)。这意味着:
- 资源回收: 当一个线程结束运行时,它所占用的资源(如栈空间)不会立即被释放,而是会保留一段时间,直到有其他线程调用
pthread_join()函数来“回收”该线程。 - 获取退出状态: 其他线程可以通过调用
pthread_join()函数来等待该线程结束,并获取它的退出状态。
什么是分离态?
分离态 (Detached State) 是一种特殊的线程状态。当一个线程被设置为分离态时,它与创建它的线程(通常是主线程)之间的关系就会被“分离”。这意味着:
- 自动资源回收: 当一个分离态线程结束运行时,它所占用的资源会被自动回收,无需其他线程调用
pthread_join()函数。 - 无法获取退出状态: 其他线程无法通过
pthread_join()函数来等待分离态线程的结束,也无法获取它的退出状态。
如何设置线程为分离态?
方法一
pthread_detach(thread_id);
这个函数可以直接将指定线程设置为分离状态。
但是你可能会想如何获得一个线程自身的线程tid呢?其实很简单,有一个函数pthread_self可以很容易帮我们获取到当前线程的tid。
这个函数在后面一篇文章中也会详细讲到,这里只是简单提一下。
函数原型:pthread_t pthread_self(void);
所以我们常常将 pthread_self 配合 pthread_detach 一起使用,像下面这样:
pthread_detach(pthread_self());
方法二
使用pthread_create函数创建线程的时候,有一个参数可以设置新创建的线程的属性。我们可以凭借这个参数来设置线程为分离态,这种方式相比于方法一,更加麻烦,但是有着自己的优点,之后我们会详细讲到,这里不详细阐述。
适用场景
有些情况下,我们对于某些线程来说不关心它的返回状态,并且也不想要使用pthread_join来阻塞等待回收这个死后的僵尸线程。那么此时我们就可以把这个线程设置成分离态度,当线程死亡自动释放,不需要其他线程调用pthread_join来回收这个僵尸进程的资源。
相关文章:
Linux中线程创建,线程退出,线程接合
线程的简单了解 之前我们了解过 task_struct 是用于描述进程的核心数据结构。它包含了一个进程的所有重要信息,并且在进程的生命周期内保持更新。我们想要获取进程相关信息往往从这里得到。 在Linux中,线程的实现方式与进程类似,每个线程都…...
机器视觉检测中,2D面阵相机和线扫相机的区别
2D面阵相机和线扫相机是工业视觉系统中常用的两种相机类型,各有其特点和应用场景。 2D面阵相机 特点: 成像方式:通过二维传感器一次性捕捉整个场景的图像。 分辨率:分辨率由传感器的像素数决定,常见的有百万像素到几千…...
LeetCode 热题 100_N 皇后 (62_51_困难_C++)(递归(回溯))
LeetCode 热题 100_N 皇后(62_51) 题目描述:输入输出样例:题解:解题思路:思路一(递归(回溯)): 代码实现代码实现(思路一(递…...
Winform(C#) 项目保存页面
上一张我们已经实现了TCP和串口页面的数据展示,和保存控件 我们这一章,实现如何去,控制保存。 一、控件展示 CheckBox TextBox Button label Name: checkSaveImage checkDelete txtSaveDays txtSaveImagePath btnSelectIm…...
【LeetCode: LCR 126. 斐波那契数 + 动态规划】
🚀 算法题 🚀 🌲 算法刷题专栏 | 面试必备算法 | 面试高频算法 🍀 🌲 越难的东西,越要努力坚持,因为它具有很高的价值,算法就是这样✨ 🌲 作者简介:硕风和炜,…...
OSPF(开放路径最短优先)
ospf优先级:内部优先级默认为10,外部优先级默认为150 1.ospf的三张表 (1)邻居表 <记录邻居状态和关系> (2)拓扑表 <链路状态数据库> (3)路由表 <对链路状态数据库进…...
JAVA EE初阶 - 预备知识(四)
一、API API 即应用程序编程接口(Application Programming Interface),是一组定义、协议和工具,用于不同软件组件、应用程序或系统之间进行交互和通信。以下从多个方面详细介绍 API: 基本概念 接口规范:A…...
如何解决服务器端口被攻击:全面防护与快速响应
服务器端口被攻击是网络安全中常见的问题之一,尤其是当服务器暴露在公共网络上时,容易成为黑客的目标。攻击者可能通过扫描开放端口、利用漏洞或发动拒绝服务(DoS/DDoS)攻击来破坏服务器的正常运行。本文将详细介绍如何检测、防御…...
golang panic原理
数据结构与底层实现 Goroutine结构体 stack(栈内存范围) 结构体类型,包含 lo(低地址)和 hi(高地址)两个 uintptr 字段,描述 Goroutine 的栈内存区间 [lo, hi)。初始栈大小为 2KB&a…...
scratch猜年龄互动小游戏 2024年12月scratch四级真题 中国电子学会 图形化编程 scratch四级真题和答案解析
scratch猜年龄互动小游戏 2024年12月电子学会图形化编程Scratch等级考试四级真题 一、题目要求 老爷爷的年龄是1-100的随机数,老爷爷询问“请猜猜我的年龄是多少?”,输入年龄,老爷爷会回答"大了"或者"小了,直到最后成功猜出年龄。 1、准备工作 (1)删…...
【Elasticsearch】查询规则_query_rules
1.Query Rules 的定义与作用 Query Rules 是 Elasticsearch 提供的一种功能,允许用户根据预定义的规则动态调整搜索结果。它通过匹配查询的元数据(如用户输入、地理位置、用户兴趣等),对搜索结果进行定制化调整,例如固…...
Git备忘录(三)
设置用户信息: git config --global user.name “itcast” git config --global user.email “ helloitcast.cn” 查看配置信息 git config --global user.name git config --global user.email $ git init $ git remote add origin gitgitee.com:XXX/avas.git $ git pull or…...
用户的声音 | 文档结构化信息提取方案测评:LLM、开源模型部署与云端API,谁是合适选择?
文档预处理之文本化 近日,我们收到来自专业用户的使用心得,浅析结构化信息提取技术、技术选型及一些个人测试。 结构化信息提取的重要性 数据作为大模型时代的核心生产资料,其结构化处理能力直接影响AI系统的实用价值。尽管知识图谱、RAG等…...
vite调试node_modules下面插件
在使用vite进行开发的时候,我们可能想要修改node_modules中插件的源码.特别是集成一个SDK,需要调试去判断问题时,或者研究第三方源码时后; vite默认是走缓存的,所以当修改后不会看到你打印的日志,这个时候有几种方法可以选择; 方式…...
ES12 weakRefs的用法和使用场景
ES12 (ECMAScript 2021) 特性总结:WeakRef 1. WeakRef 概述 描述 WeakRef 是 ES12 引入的一个新特性,用于创建对对象的弱引用。弱引用不会阻止垃圾回收器回收对象,即使该对象仍然被弱引用持有。WeakRef 通常与 FinalizationRegistry 结合使…...
【Python】集合set详细讲解(语法、操作、集合运算、性能、使用场景)
文章目录 1. 语法1.1 使用 {} 定义1.2 使用 set() 定义 2. 特点3. 常用操作3.1 访问元素3.2 查找数据3.3 添加元素3.3.1 add() 方法3.3.2 update()方法 3.4 删除元素3.4.1 remove()方法3.4.2 discard()方法3.4.3 pop()方法3.4.4 clear()方法 3.5 集合运算3.5.1 并集:…...
网络安全大数据架构 网络安全之数据安全
🍅 点击文末小卡片 ,免费获取网络安全全套资料,资料在手,涨薪更快 网络安全和数据安全 从狭义来说,网络安全指网络系统的硬件、软件及其系统中的数据受到保护,不因偶然的或恶意的原因遭到破坏、更改、泄露&…...
(前端基础)CSS(一)
了解 Cascading Style Sheet:层叠级联样式表 CSS:表现层(美化网页)如:字体、颜色、边框、高度、宽度、背景图片、网页定位、网页浮动 css优势: 内容和表现分离网页结构表现统一,可以实现复用…...
Redis数据类型全景解析:从底层编码到应用反模式
一、核心数据类型矩阵 1.1 基础类型对比表 类型底层结构最大容量时间复杂度典型场景StringSDS/Embstr/Raw512MBO(1)读写缓存/计数器ListQuickList(ziplist)2^32-1元素头尾操作O(1)消息队列Hashziplist/hashtable2^32-1键值对O(1)平均对象存储Setintset/hashtable2^32-1成员O(…...
(蓝桥杯——10. 小郑做志愿者)洛斯里克城志愿者问题详解
题目背景 小郑是一名大学生,她决定通过做志愿者来增加自己的综合分。她的任务是帮助游客解决交通困难的问题。洛斯里克城是一个六朝古都,拥有 N 个区域和古老的地铁系统。地铁线路覆盖了树形结构上的某些路径,游客会询问两个区域是否可以通过某条地铁线路直达,以及有多少条…...
后进先出(LIFO)详解
LIFO 是 Last In, First Out 的缩写,中文译为后进先出。这是一种数据结构的工作原则,类似于一摞盘子或一叠书本: 最后放进去的元素最先出来 -想象往筒状容器里放盘子: (1)你放进的最后一个盘子(…...
conda相比python好处
Conda 作为 Python 的环境和包管理工具,相比原生 Python 生态(如 pip 虚拟环境)有许多独特优势,尤其在多项目管理、依赖处理和跨平台兼容性等方面表现更优。以下是 Conda 的核心好处: 一、一站式环境管理:…...
树莓派超全系列教程文档--(62)使用rpicam-app通过网络流式传输视频
使用rpicam-app通过网络流式传输视频 使用 rpicam-app 通过网络流式传输视频UDPTCPRTSPlibavGStreamerRTPlibcamerasrc GStreamer 元素 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 使用 rpicam-app 通过网络流式传输视频 本节介绍来自 rpica…...
线程与协程
1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指:像函数调用/返回一样轻量地完成任务切换。 举例说明: 当你在程序中写一个函数调用: funcA() 然后 funcA 执行完后返回&…...
376. Wiggle Subsequence
376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...
C# 类和继承(抽象类)
抽象类 抽象类是指设计为被继承的类。抽象类只能被用作其他类的基类。 不能创建抽象类的实例。抽象类使用abstract修饰符声明。 抽象类可以包含抽象成员或普通的非抽象成员。抽象类的成员可以是抽象成员和普通带 实现的成员的任意组合。抽象类自己可以派生自另一个抽象类。例…...
OPENCV形态学基础之二腐蚀
一.腐蚀的原理 (图1) 数学表达式:dst(x,y) erode(src(x,y)) min(x,y)src(xx,yy) 腐蚀也是图像形态学的基本功能之一,腐蚀跟膨胀属于反向操作,膨胀是把图像图像变大,而腐蚀就是把图像变小。腐蚀后的图像变小变暗淡。 腐蚀…...
Fabric V2.5 通用溯源系统——增加图片上传与下载功能
fabric-trace项目在发布一年后,部署量已突破1000次,为支持更多场景,现新增支持图片信息上链,本文对图片上传、下载功能代码进行梳理,包含智能合约、后端、前端部分。 一、智能合约修改 为了增加图片信息上链溯源,需要对底层数据结构进行修改,在此对智能合约中的农产品数…...
无人机侦测与反制技术的进展与应用
国家电网无人机侦测与反制技术的进展与应用 引言 随着无人机(无人驾驶飞行器,UAV)技术的快速发展,其在商业、娱乐和军事领域的广泛应用带来了新的安全挑战。特别是对于关键基础设施如电力系统,无人机的“黑飞”&…...
Web后端基础(基础知识)
BS架构:Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端。 优点:维护方便缺点:体验一般 CS架构:Client/Server,客户端/服务器架构模式。需要单独…...
