目录

前言

2.线程控制

1.验证理论

2.引入pthread线程库

3.linux线程控制的接口

3.线程id及进程地址空间布局

4.线程栈


前言

  本篇是紧接着上一篇的内容,在有了相关线程概念的基础之上,我们将要学习线程控制相关话题!!

2.线程控制

1.验证理论

先来验证一下我们上面的理论

创建线程可用pthread_create函数(不是系统调用)

image-20250610204544065

第一个参数传pthread_t类型变量来获取新线程的id;第二个参数为线程属性(设置为nullptr就可以);第三个参数是传返回值为void*,参数为void *的函数指针;第四个参数就是想传递给第三个参数的指针/参数

image-20250611101127995

然后我们正常链接是过不了的,因为这不属于系统调用,我们需要在链接时加上pthread第三方库名称才行,因为是第三方库,所以需要带上l选项—— -lpthread

image-20250611101918960

image-20250611102311046

使用命令:ps -aL来查看所有线程

image-20250611102852031

其中pid是一样的,证明这两线程(两执行流)属于同一个进程;TTY表示终端,它们都属于同一个终端,都往显示器上打印;而LWP则是light weight process——轻量级进程,所以这两执行流的轻量级进程号分别是902075和902076,LWP和pid相等的那个是主线程

image-20250611103518261

CPU调度的时候看的是LWP,调度只看轻量级进程,我们之前学的getpid虽然拿的是pid,但是在调度的时候拿的还是lwp,因为单进程的话,pid就是lwp嘛

细节问题:

  1. 关于调度的时间片问题:进程的时间片是等分给不同的线程的,因为时间片也是共享的(不可能说创建一个线程就拷贝一份时间片,那样如果有恶意程序不断分裂线程就会导致时间片是一直累加的)

  2. 我们可以验证一下线程异常的情况

    #include <iostream>
    #include <pthread.h>
    #include <unistd.h>
    using namespace std;
    ​
    void *threadrun(void *args)
    {string name = (const char *)args;while (true){sleep(1);cout << "我是新线程: name: " << name << ",pid: " << getpid() << endl;int a = 10;a /= 0;}return nullptr;
    }
    ​
    int main()
    {pthread_t tid;pthread_create(&tid, nullptr, threadrun, (void *)"thread-1");while (true) // 主线程往这里执行,新线程转而去执行我们的threadrun了{cout << "我是主线程..." << ",pid: " << getpid() << endl;sleep(1);}return 0;
    }

    image-20250611111123815

可以看到当新线程发生异常之后,系统终止的是整个进程——任何一个线程崩溃,都会导致整个进程崩溃,一个崩溃会影响其他人,所以健壮性低

  1. 为什么正常打印出的消息会混杂在一起

    image-20250611111700913

这是多线程程序输出混乱问题,因为多个线程(主线程、新线程 )共享标准输出流,CPU 调度线程时,没有加保护时,若一个线程输出未完成就切换到另一个线程继续输出,就会导致消息混杂

2.引入pthread线程库

为什么会有这个库,这个库是什么东西?

image-20250611115557732

我们上面的pthread_create封装的就是底层系统的clone

image-20250611115740873

我们在c++阶段也学习过创建线程的方式,在linux下其本质也是封装了pthread库,在windows下是封装了windows创建线程的接口,目的是为了保证语言的跨平台、可移植性,所以语言的跨平台或者可移植性一般都是大力出奇迹,所有平台全部干一份,然后条件编译形成库(所有的热门偏后端的语言基本多线程都这样封装的)

image-20250611163742612

3.linux线程控制的接口

前面我们所提及的pthread库其实叫做POSIX线程库

• 与线程有关的函数构成了⼀个完整的系列,绝⼤多数函数的名字都是以“pthread_”打头的

• 要使⽤这些函数库,要通过引⼊头⽂ <pthread.h>

• 链接这些线程函数库时要使⽤编译器命令的“-lpthread”选项

  1. 创建线程函数pthread_create(我们在上面也谈过的)

    image-20250611192543920

image-20250611194420935

[^]  pthread_t类型其实是一个无符号长整型 

在我们的新线程被创建出来之后,主线程和新线程谁先运行是不确定的,这一点在我们的fork创建子进程之后的父子进程之间的谁先运行的时候也是一样的

这里其实我们的参数3属于是回调函数的范畴了,说到回调函数,这里就不得不讲一下了

关于回调函数:

1. 回调函数的角色

pthread_create 是创建线程的系统调用,需要一个 “线程执行逻辑”,但它没办法直接把逻辑写死在函数里(要支持不同业务场景)。所以设计成让调用者传入一个函数指针,这个被传入的函数(routine)就是 “回调函数”—— 由 pthread_create “回调” 执行,实现线程的自定义逻辑。

2. 代码里的关键关联

pthread_create(&tid, nullptr, routine, (void *)"thread-1");

routine 的函数签名要求:必须符合pthread约定的线程函数原型void* ()(void)

即:

  • 返回值是 void*(可用来给主线程返回数据)

  • 参数是 void*(能兼容任意类型的入参,比如这里传字符串 "thread-1"

调用时机pthread_create 成功创建线程后,新线程会自动执行 routine 函数,把 (void *)"thread-1" 作为参数传入。

解耦思想pthread_create 只负责 “线程创建 + 触发回调”,具体线程要做什么(routine 里的逻辑)交给调用者实现,灵活适配不同需求。

总结:回调函数是一种 “反向调用” 设计,pthread_create 预先留好 “函数指针的坑”,你填自己的 routine 逻辑,让线程按你的逻辑跑。核心是解耦框架(pthread)和业务逻辑(你要线程做的事),让代码更灵活。

线程创建好之后,新线程要被主线程等待,不然就会产生类似僵尸进程的问题,导致内存泄漏

  1. 我们可以通过pthread_join函数来让主线程进行等待

    image-20250611193018939

[^]  参数1是传新线程id,参数2则是获取上面pthread_create参数3的返回值(不关心新线程执行的怎么样,也就是不关心新线程执行的退出结果可以传nullptr,需要获取则要取地址传入一个指针变量才能拿到返回值为void*的变量) 

新线程return

image-20250611210615413

主线程接收

image-20250611210711690

打印的结果就是123

image-20250611211136615

我们可能会有一个疑惑,那就是在进程等待时会有异常相关的字段,为啥线程这里的join却没有呢?答:那是因为等待的目标线程如果异常了,整个进程都退出了,包括主线程,所以join异常是没有意义的,压根就看不到;join都是基于线程健康跑完的情况,不需要处理异常信号,异常信号是进程要处理的话题!!

如果我们获取一下新线程的tid,会发现它压根就不是我们查看的线程lwp

image-20250611195047434

image-20250611195120710

因为lwp是轻量级进程的概念,而我们在用户上不要看到这个,因为封装要封装彻底(不然用户本来只需要专注于线程就好了,这样一搞岂不是还需要去了解轻量级进程嘛),我们这里获取到的线程id就不会是lwp!!

  1. 我们使用pthread_self函数来获取调用了这个函数的线程的id

    image-20250611200033981

我们通过这个函数来看看pthread_create返回的线程id是否与新线程通过pthread_self获取到的自己的id相等,进而验证pthread_create返回的线程id就是新线程的id

#include <iostream>
#include <pthread.h>
#include <thread>
#include <unistd.h>
using namespace std;
​
void FormatId(pthread_t tid)
{printf("新线程通过pthread_self获取到的自己的id为: %ld\n", tid);
}
​
void *routine(void *args)
{string name = static_cast<const char *>(args);// 获取该线程的id来验证一下在主线程中pthread_create中取得的id是否一致pthread_t tid = pthread_self();FormatId(tid);int cnt = 5;while (cnt){cout << "我是一个新线程, 我的名字是: " << name << endl;cnt--;sleep(1);}return nullptr;
}
​
void showid(pthread_t id)
{printf("pthread_create返回的线程id为: %ld\n", id);
}
​
int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, routine, (void *)"thread-1");showid(tid);(void)n;
​// 主线程进行等待pthread_join(tid, nullptr);
​return 0;
}

image-20250611200748779

通过结果可以看到pthread_create返回的线程id就是新线程的id!!

关于上述代码的一些结论:

  1. main函数结束,代表主线程结束,一般也代表着进程结束

  2. 新线程对应的入口函数运行结束,代表当前线程运行结束

  3. 给线程传递参数和返回值,可以是任意类型,不一定非得是内置类型,我们自定义类型对象也可以

线程终止问题:

  1. 线程的入口函数进行return就是线程终止(这种方式用的最多

  2. 注意:线程不能用exit()终止,因为exit是终止进程的

  3. 线程要终止也可用pthread_exit()函数,可以终止调用这个函数的线程

    image-20250612143839801

    [^]: return res等价于pthread_exit(res)

  4. 终止线程还可以使用pthread_cancel函数,一般都是由主线程来用这个函数来取消新线程,用此方式终止线程的退出结果是-1【PTHREAD_CANCELED】

    image-20250612144641775

问题:如果主线程不想再关心新线程,而是当新线程结束的时候,让它自己进行释放,此时我们要如何做呢?

解决方案:设置新线程为分离状态

技术层面:线程默认是需要被等待的,状态是joinable;如果不想让主线程等待新线程,

想让新线程结束之后,自己退出,设置为分离状态(!joinable or detach)

理解层面:线程分离,可以是主线程分离新线程,也可以是新线程把自己分离

注意:分离的线程依旧在进程的地址空间中,进程的所有资源,被分离的线程依旧可以访问,可以操作,只不过主线程不等待新线程了

分离操作:

  1. 可以使用pthread_detach函数进行分离

image-20250612191831630

[^]  新线程分离自己可用pthread_detach(pthread_self()) 

如果线程被设置为分离状态,不需要进行join等待,join会失败

实现一个简单的多线程代码:

#include <iostream>
#include <vector>
#include <pthread.h>
#include <thread>
#include <string.h>
#include <unistd.h>
using namespace std;
​
// 创建多线程
​
const int num = 10;
​
void *routine(void *args)
{string name = static_cast<const char *>(args);delete args;int cnt = 5;while (cnt--){cout << "new线程名字: " << name << endl;sleep(1);}
​return nullptr;
}
​
int main()
{vector<pthread_t> tids;for (int i = 0; i < num; i++){pthread_t tid;// 我们采用下面这种id的做法是有问题不安全的// 因为传的指向id的首地址,各线程看到的是同一个id缓冲区,那么最后线程打出来的id// 都会是最后一次循环时修改覆盖掉前面内容的thread-9// char id[64];// 需要的是,每一次循环,都给对应的线程申请堆空间,这样才能让这一循环中创建的新线程// 独享这块堆空间的起始地址char *id = new char[64];snprintf(id, 64, "thread-%d", i); // 将后面的格式化输出到id缓冲区中int n = pthread_create(&tid, nullptr, routine, id);if (n == 0){tids.push_back(tid);}else{continue;}}
​// 主线程才往下走for (int i = 0; i < num; i++){// 一个一个的等待int n = pthread_join(tids[i], nullptr);if (n == 0){cout << "等待新线程成功" << endl;}}
​return 0;
}

我们可以自主封装一个线程接口类,具体可见:thread/Thread.hpp 

3.线程id及进程地址空间布局

image-20250612202848813

线程的概念是在库中维护的(linux所有的线程都在库中),在库内部就一定会存在多个被创建好的线程,库当然要管理这样线程,管理的方法也还是先描述,再组织

会有struct tcb这样的结构体,当我们调用pthread_create时,这个pthread_create内部就会帮我们在系统当中申请对应的tcb,就如同我们的fopen调用时会在内部申请FILE对象

struct tcb
{//线程应该有的属性,用户需要的线程状态线程id线程独立的栈结构线程栈大小...(而像优先级、时间片、上下文这种用户不需要的与调度有关的属性是被写到内核的lwp->pcb中)
}

image-20250613133353493

image-20250613133215707

[^]  我们上面的tid其实就是线程在库中对应管理块(红框)的起始虚拟地址,当线程return退出后,管理块中的数据并没有被释放,所以得join,传入起始地址用来释放以及通过ret取到管理块中保存的结果 

image-20250613144504309

image-20250613144157199

通过上图中每个管理块都有线程栈,我们可以知道,每个线程都必须有自己独立的栈空间(在申请的管理块当中),主线程则用的是地址空间中的栈,新线程用的是自己申请的管理块中的栈,所以说每个线程都必须有自己独立的栈结构

image-20250613135159367

库中创建好管理块把一些数据给线程,直接等该线程执行对应方法就好了

linux 用户级线程 :内核lwp = 1:1

在前面加上__thread的变量会分别在不同线程的线程局部存储位置开辟一份,名字一样,但是底层的虚拟地址不一样了,就可以实现全局变量变成分别新线程的局部变量了

image-20250615161641476

线程的局部存储有什么用:有时我们需要全局变量,但又不想让这个全局变量被其他线程看到时就可以在这个变量前面加上__thread

(但是线程局部存储只能存储内置类型和部分指针)

4.线程栈

独立的上下文:有独立的PCB(内核)+TCP(用户层,pthread库内部)

独立的栈:每个线程都有自己的栈,要么是线程自己的,要么是库中创建进程时mmap申请出来的

虽然 Linux 将线程和进程不加区分的统⼀到了 task_struct ,但是对待其地址空间的stack还是有些区别的。

• 对于 Linux 进程或者说主线程,简单理解就是main函数的栈空间,在fork的时候,实际上就是复制了⽗亲的stack 空间地址,然后写时拷⻉(cow)以及动态增⻓。如果扩充超出该上限则栈溢出会报段错误(发送段错误信号给该进程)。进程栈是唯⼀可以访问未映射⻚⽽不⼀定会发⽣段错误⸺超出扩充上限才报。

• 然⽽对于主线程⽣成的⼦线程⽽⾔,其 stack将不再是向下⽣⻓的,⽽是事先固定下来的。线程栈⼀般是调⽤glibc/uclibc等的pthread 库接⼝ pthread_create创建的线程,在⽂件映射区(或称之为共享区)。其中使⽤mmap 系统调⽤,这个可以从 glibc的nptl/allocatestack.c 中的 allocate_stack函数中看到:

mem = mmap (NULL, size, prot,MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);

此调⽤中的 size 参数的获取很是复杂,你可以⼿⼯传⼊stack的⼤⼩,也可以使⽤默认的,⼀般⽽⾔就是默认的 8M 。这些都不重要,重要的是,这种stack不能动态增⻓,⼀旦⽤尽就没了,这是和⽣成进程的fork不同的地⽅。在glibc中通过mmap得到了stack之后,底层将调⽤ sys_clone系统调⽤:

image-20250613193005380

因此,对于⼦线程的 stack ,它其实是在进程的地址空间中map出来的⼀块内存区域,原则上是线程私有的,但是同⼀个进程的所有线程⽣成的时候,是会浅拷⻉⽣成者的 task_struct的很多字段,如果愿意,其它线程也还是可以访问到的,于是⼀定要注意

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

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

力扣面试150题--只出现一次的数字

Day 91 题目描述## 思路 交换律&#xff1a;a ^ b ^ c <> a ^ c ^ b 任何数于0异或为任何数 0 ^ n > n 相同的数异或为0: n ^ n > 0 根据以上 很容易想到做法&#xff0c;将数组中所有的数异或起来&#xff0c;得到的就是只出现一次的数 class Solution {public in…

【运维基础】Linux 进程调度管理

Linux 进程调度管理 进程调度器 现代计算机系统中既包含只有单个CPU且任何时候都只能处理单个指令的低端系统到具有几百个cpu、每个cpu有多个核心的高性能超级计算机&#xff0c;可以并行执行几百个指令。所有这些系统都有一个共同点&#xff1a;系统进程线程数量超出了CPU数量…

深度学习篇---层与层之间搭配

在深度学习中&#xff0c;各种层&#xff08;比如卷积层、激活函数、池化层等&#xff09;的搭配不是随意的&#xff0c;而是像 “搭积木” 一样有规律 —— 每一层的作用互补&#xff0c;组合起来能高效提取特征、稳定训练&#xff0c;最终提升模型性能。下面用通俗易懂的方式…

服务器多线主要是指什么?

在数字化的网络环境当中&#xff0c;服务器已经成为各个企业提升线上业务发展的重要网络设备&#xff0c;其中服务器多线则是指一台服务器中能够同时接入多个网络运营商&#xff0c;并且通过智能路由技术实现用户访问请求的自动化分配&#xff0c;大大提高了用户访问数据信息的…

从0到1学PHP(三):PHP 流程控制:掌控程序的走向

目录一、条件语句&#xff1a;程序的 “抉择路口”1.1 if 语句家族&#xff1a;基础与进阶1.2 switch 语句&#xff1a;精准匹配的 “导航仪”二、循环语句&#xff1a;程序的 “重复舞步”2.1 for 循环&#xff1a;有序的 “征程”2.2 while 与 do - while 循环&#xff1a;条…

uni-app框架基础

阐述 MVC 模式1, MVC与MVVMMVC 他是后端的一个开发思想MVVM是基于MVC中的view这层所分离出来的一种设计模式。MVC架构详解MVC&#xff08;Model-View-Controller&#xff09;是一种广泛使用的软件设计模式&#xff0c;主要用于分离应用程序的业务逻辑、用户界面和输入控制。这种…

智慧收银系统开发进销存库存统计,便利店、水果店、建材与家居行业的库存汇总管理—仙盟创梦IDE

在零售与批发行业的数字化转型中&#xff0c;当前库存汇总作为进销存管理的核心环节&#xff0c;直接影响着企业的资金周转、销售决策和客户满意度。现代收银系统已超越传统的收款功能&#xff0c;成为整合多渠道数据、实现实时库存汇总的中枢神经。本文将深入剖析便利店、水果…

selenium(WEB自动化工具)

定义解释 Selenium是一个用于Web应用程序测试的工具。Selenium测试直接运行在浏览器中&#xff0c;就像真正的用户在操作一样。支持的浏览器包括IE&#xff08;7, 8, 9, 10, 11&#xff09;&#xff0c;Mozilla Firefox&#xff0c;Safari&#xff0c;Google Chrome&#xff0…

windows本地使用conda部署Open-webui

前提条件 Open-webui使用python3.11.9 步骤 conda操作也可以参考 安装python torch、transformer、记录 1、conda环境 # 创建环境 conda create --name openwebui python3.11.9# 激活环境 conda activate openwebui# 升级pip版本 pip install --upgrade pip# pip安装openwe…

【Unity笔记04】数据持久化

&#x1f31f; 方案核心思想遵循以下设计原则&#xff1a;数据安全第一&#xff1a;绝不使用明文存储&#xff0c;采用AES加密算法保护数据。性能优化&#xff1a;使用异步I/O操作&#xff0c;避免阻塞主线程导致游戏卡顿。结构清晰&#xff1a;模块化设计&#xff0c;职责分离…

深入理解 HTML5 Web Workers:提升网页性能的关键技术解析

深入理解 HTML5 Web Workers&#xff1a;提升网页性能的关键技术解析引言1. 什么是 Web Workers&#xff1f;Web Workers 的特点&#xff1a;2. Web Workers 的使用方式2.1 创建一个 Web Worker步骤 1&#xff1a;创建 Worker 文件步骤 2&#xff1a;在主线程中调用 Worker3. W…

会议室预定系统核心技术:如何用一行SQL解决时间冲突检测难题

文章目录 一、为什么时间冲突检测是预定系统的核心挑战? 二、黄金法则:两行线段重叠检测法 三、四大冲突场景实战解析(同一会议室) 四、生产环境完整解决方案 1. 基础冲突检测函数 2. 预定API处理流程 3. 高级边界处理技巧 五、性能优化关键策略 六、不同数据库的适配方案 …

13.正则表达式:文本处理的瑞士军刀

正则表达式&#xff1a;文本处理的瑞士军刀 &#x1f3af; 前言&#xff1a;当文本遇上神奇的密码 想象一下&#xff0c;你是一个图书管理员&#xff0c;面对着一堆乱七八糟的书籍信息&#xff1a; “联系电话&#xff1a;138-1234-5678”“邮箱地址&#xff1a;zhang.sangm…

linux下c语言访问mysql数据库

一、连接数据库基础1. 头文件与库文件连接 MySQL 需包含的头文件&#xff1a;#include <mysql/mysql.h> // 部分环境也可用 #include <mysql.h> 编译链接时&#xff0c;Linux 平台需指定库名&#xff1a;-lmysqlclient &#xff0c;用于链接 MySQL 客户端函数库。2…

6. 传输层协议 UDP

传输层负责数据能够从发送端传输接收端.1. 再谈端口号端口号(Port)标识了一个主机上进行通信的不同的应用程序在 TCP/IP 协议中, 用 "源 IP", "源端口号", "目的 IP", "目的端口号", "协议号" 这样一个五元组来标识一个通信…

vue 开发总结:从安装到第一个交互页面-与数据库API

vue 总结 1、安装vue&#xff1a; WinR 输入&#xff1a;cnpm install -g vue/cli 验证是否安装成功&#xff1a;vue --version 2、新建Vue工程 在对应文件夹下右击打开集成终端 输入 vue create query_system&#xff08;新建项目名字&#xff09;名称不能存在大写&#x…

运维笔记:HTTP 性能优化

一、HTTP 协议特性与性能瓶颈1.1 HTTP 协议发展历程HTTP 协议的演进直接影响着 Web 性能&#xff0c;各版本关键特性对比&#xff1a;协议版本发布时间核心特性性能优势局限性HTTP/1.01996 年无状态、短连接简单易实现每次请求需建立 TCP 连接HTTP/1.11999 年长连接、管道化减少…

ubuntu:运行gcfsd-admin守护进程需要认证,解决方法

这里有个锁子&#xff0c;每次进入都要输入密码&#xff0c;怎么解决&#xff1f; 重新挂载 /data 磁盘 sudo umount /data sudo ntfsfix /dev/sda1 sudo mount -o rw /dev/sda1 /data

1.DRF 环境安装与配置

文章目录一. Django Rest_Framework二、环境安装与配置2.1 安装 DRF2.2 创建Django项目2.3 添加 rest_framework 应用三、启动项目一. Django Rest_Framework 核心思想&#xff1a;大量缩减编写 api 接口的代码 Django REST framework 是一个建立在 Django 基础之上的 Web 应…

设计模式(十九)行为型:备忘录模式详解

设计模式&#xff08;十九&#xff09;行为型&#xff1a;备忘录模式详解备忘录模式&#xff08;Memento Pattern&#xff09;是 GoF 23 种设计模式中的行为型模式之一&#xff0c;其核心价值在于在不破坏封装性的前提下&#xff0c;捕获并外部化一个对象的内部状态&#xff0c…