当前位置: 首页 > news >正文

Linux线程学习

目录

    • 线程跟进程的区别
    • 线程控制
      • 创建线程
      • 获取线程自身ID
      • 线程终止
        • 通过reurn方式
        • 通过exit()
        • 通过pthread_exit函数
        • 通过pthread_cancel函数
      • 线程等待
      • 线程分离
      • 线程互斥
        • 初始化互斥量,两种方法:
        • 互斥量销毁
        • 互斥量加锁和解锁
        • 简单上手使用锁改进售票系统
    • 线程缺点

前言: 我写的关于进程学习的文章

线程跟进程的区别

在这里插入图片描述
对于线程,要创建PCB,进程地址空间,页表,并且代码和数据要分开存放
能不能有一种更简便的方式呢?
在这里插入图片描述

本质是两个指针指向同一个数据结构对象
那么可不可以通过技术手段把代码部分拆分成多份,比如有三个函数,之前是一个进程运行,现在三个“进程”一人跑一个,把堆区、栈区等想办法让三个“进程”共享,不该共享的分成每人各一份。

CPU执行时,只会执行一个进程的一部分代码,访问该进程的一部分数据,执行任务部分的一部分,这种比传统进程更轻的概念叫线程。
创建一个线程的成本只需要参与资源分配,不需要像进程一样创建各种结构,所以线程是比进程更轻量化的一种执行流
线程在进程的地址空间中运行,那么引出线程的另一个概念:线程是在进程内部执行的一种执行流

线程(TCB)与线程(PCB)所需要的属性(pid,优先级)都是差不多的,没必要再新建一个结构,那么那PCB充当TCB,就可以把调度需要的管理,算法,代码等复用。

操作系统对于线程的概念是:线程是比进程更轻量化的一种执行流
实际操作系统,对于线程,有不同的实现方法,只要满足了线程的概念,那么实现出来的就是线程
我们上面说的PCB充当TCB,就是Linux的实现方法
操作系统难学的原因也是在于,书本只给出了概念,却没有具体的执行方案,当有了一种执行方案,就便于理解了。

PCB充当TCB,那么CPU看到的只有PCB,怎么区分谁是进程,谁是线程?
不需要区分,就像快递员不会看你的快递,他的任务只是送快递
对于CPU来说,它的任务就是根据这个PCB,找到进程地址空间,进而找到对应的代码数据,执行即可。

在这里插入图片描述
创建进程时,默认一份PCB,一份地址空间,一份页表,代码数据,统称为进程。
线程就不需要申请这么多资源,只需要创建新的PCB
概念:进程是承担系统资源的基本实体
系统每创建一个进程就分配系统的一部分资源,往后线程的创建只是分配进程内部的资源

对于在我进程的博客中,同样是进程=内核数据结构+代码数据,不过那是一个内部只有一个执行流的进程
而在今天线程的学习中,这个进程就可以说是内部有多个执行流的进程了

总结:左边大框就是进程,每一个PCB就是一个轻量级进程,也就是线程的概念——线程是CPU调度的基本单位

理解了线程和进程的区别,那么线程之间怎么区分?
看的是LWP,类似于PID,当LWP=PID,就意味着这是最先的线程
在这里插入图片描述

线程控制

创建线程

功能:创建一个新的线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg)
参数:thread:返回线程IDattr:设置线程的属性,attr为NULL表示使用默认属性start_routine:是个函数地址,线程启动后要执行的函数arg:传给线程启动函数的参数返回值:成功返回0;失败返回错误码

注意:pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。所以第一个参数thread是输出型参数。

pthread_t 其实只是一个无符号长整型
在这里插入图片描述
start_routine 函数是线程启动后要执行的函数,这个函数的返回值类型必须是 void* , 因为使用 void* 作为返回类型,可以让线程函数返回任意类型的数据(通过指针转换)

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>// 新线程
void *ThreadRoutine(void *arg)
{//const char *threadname = (const char *)arg;std:string name = static_cast<const char*>(arg);//类型转换while (true){std::cout << "new thread is running ,thread name:" << name << std::endl;sleep(1);}
}int main()
{// 已经有进程了pthread_t tid;pthread_create(&tid, nullptr, ThreadRoutine, (void *)"thread 1");// 主线程while (true)std::cout << "main thread,sub thread:" << tid<< std::endl;sleep(1);}return 0;
}

主线程能打印出新线程的id,那怎么打印出主线程的id?
再认识一个新函数

获取线程自身ID

功能:获得线程自身的ID
pthread_t pthread_self(void);
int main()
{// 已经有进程了pthread_t tid;pthread_create(&tid, nullptr, ThreadRoutine, (void *)"thread 1");// 主线程while (true)std::cout << "main thread,sub thread:" << tid<< "main thread id:" << pthread_self() << std::endl;sleep(1);}return 0;
}

在这里插入图片描述
如果换成16进制,显示各自的id
在这里插入图片描述
thread id本质其实是一个地址

线程终止

通过reurn方式
void *ThreadRoutine(void *arg)
{std:string name = static_cast<const char*>(arg);//类型转换while (true){std::cout << "new thread is running ,thread name:" << name << std::endl;sleep(1);}return nullptr;
}
通过exit()

注意这种方式不仅会终止该线程,其他线程也会被一起终止,所以也叫进程终止。

void *ThreadRoutine(void *arg)
{std:string name = static_cast<const char*>(arg);//类型转换while (true){std::cout << "new thread is running ,thread name:" << name << std::endl;sleep(1);}exit(5);
}
通过pthread_exit函数
功能:线程终止
void pthread_exit(void *value_ptr);参数:value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
void *ThreadRoutine(void *arg)
{std:string name = static_cast<const char*>(arg);//类型转换while (true){std::cout << "new thread is running ,thread name:" << name << std::endl;sleep(1);}pthread_exit(nullptr);
}
通过pthread_cancel函数
功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码

线程等待

线程退出,没有等待,会导致类似进程的僵尸问题

功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);参数:thread:线程IDvalue_ptr:它指向一个指针,后者指向线程的返回值返回值:成功返回0;失败返回错误码

这种等待是阻塞等待

void *ThreadRoutine(void *arg)
{std:string name = static_cast<const char*>(arg);//类型转换while (true){std::cout << "new thread is running ,thread name:" << name << std::endl;sleep(1);}pthread_exit(10);
}

这是线程执行的函数,那么主线程等待完成后怎么拿到结束线程的退出信息?
主要依靠输出型参数 void **value_ptr
在这里插入图片描述

pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, (void *)"thread-1");void *ret = nullptr;
int n = pthread_join(tid, &ret);
std::cout << "main thread join done,"
<< " n: " << n << " thread return: " << (int64_t)ret << std::endl;

难道就不考虑线程出异常无法返回的情况吗?
是的,跟进程异常不同,进程之间是相互独立的。而线程一旦异常,主线程也得挂,所以就没必要接收异常信息,默认不会出异常。

线程分离

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

所以主线程不想等待时,就可以把线程设为分离状态,退出时自动释放资源。

int pthread_detach(pthread_t thread);
返回值为 0:这表明函数调用成功,线程已顺利设置为分离状态。
返回值为非零错误码:这意味着函数调用失败,常见的错误码有以下几种:EINVAL:传入的thread线程 ID 无效。ESRCH:找不到与thread对应的线程。EDEADLK:可能出现了死锁,例如尝试分离当前正在运行的线程。使用案例:
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, (void *)"thread-1");pthread_detach(tid);

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:

pthread_detach(pthread_self());

线程互斥

// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
void *route(void *arg)
{char *id = (char*)arg;while ( 1 ) {if ( ticket > 0 ) {usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;} else {break;}}
}
int main( void )
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, "thread 1");pthread_create(&t2, NULL, route, "thread 2");pthread_create(&t3, NULL, route, "thread 3");pthread_create(&t4, NULL, route, "thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}
一次执行结果:
thread 4 sells ticket:100
...
thread 4 sells ticket:1
thread 2 sells ticket:0
thread 1 sells ticket:-1
thread 3 sells ticket:-2

像tickets- -这样的操作,对应的汇编指令其实至少有三条:

  1. 读取数据
  2. 修改数据
  3. 写回数据

线程就有可能在执行if逻辑判断之后,还没有执行tickets- -之前就被切换下去了,而多个线程都出现这样的情况时,他们都被重新调度时,重新加载自己的上下文数据时,继续向后执行,但此时ticket已经为0,但线程不知道,它只知道要继续让tickets-1,于是多个线程这样做,就让ticket甚至减到了-2

当线程在执行一个对资源访问的操作时,要么做了这个操作,要么没有做这个操作,只要两种状态,不会出现做了一半这样的状态,我们称这样的操作是原子性的。
(就比如你妈让你写作业,要么给我把作业写完了再出去玩,要么就一个字也别写直接滚出门,就这两种状态,不会出现你写了一半,然后你妈让你出去玩的这种情况,这样的操作就是原子性的)

如果我们想让多个执行流串行的访问临界资源,而不是并发或并行的访问临界资源,这样的线程调度方案就是互斥式的访问临界资源!(串行就是指只要一个线程开始执行这个任务,那么他就不能中断,必须得等这个线程执行完这个任务,你才能切换其他线程执行其他的任务)
所以就有了锁的概念
加锁就意味着一个线程持有锁之后,不管是它被调度还是被切换走了,它都是带着锁的,只要它持有锁,其他线程无法运行临界资源,效率自然会比并行慢

Linux提供的锁叫互斥量。

初始化互斥量,两种方法:

方法1,静态分配:编译期初始化,通过宏定义直接赋值完成。
简单直接,无需手动销毁互斥量(程序退出时自动回收)。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

只能使用默认属性(普通锁、非递归、非进程共享),无法自定义互斥量属性(如递归锁、进程共享等)。
适用于简单场景,不需要特殊属性的互斥量。

方法2,动态分配:
需要手动销毁互斥量

int pthread_mutex_init(pthread_mutex_t *restrict mutex, 
const pthread_mutexattr_t *restrict attr);
参数:mutex:要初始化的互斥量attr:NULL

可通过第二个参数 attr 自定义互斥量属性(需配合 pthread_mutexattr_t 相关函数设置)。
例如:创建递归锁(允许同一线程多次加锁)、跨进程共享的锁(用于多进程同步)等。
灵活性更高,适用于复杂场景

互斥量销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex)
  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_mutex_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

补充:
如果对一个临界区加锁之后,在临界区内部执行时允许线程切换
线程切换后,因为当前线程尚未释放锁,锁仍然被当前线程持有其他线程只能等待线程执行完临界区后解锁,才能申请加锁

简单上手使用锁改进售票系统
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
int ticket = 100;
pthread_mutex_t mutex;void *route(void *arg)
{char *id = (char*)arg;while ( 1 ) {pthread_mutex_lock(&mutex);if ( ticket > 0 ) {usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;pthread_mutex_unlock(&mutex);} else {pthread_mutex_unlock(&mutex);break;}}
}
int main( void )
{pthread_t t1, t2, t3, t4;pthread_mutex_init(&mutex, NULL);pthread_create(&t1, NULL, route, "thread 1");pthread_create(&t2, NULL, route, "thread 2");pthread_create(&t3, NULL, route, "thread 3");pthread_create(&t4, NULL, route, "thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);pthread_mutex_destroy(&mutex);
}

pthread_mutex_t mutex 本质上是一个C语言风格的结构体(struct),它不是面向对象的类,没有成员函数,只能通过 pthread 库提供的函数(如 pthread_mutex_lock、pthread_mutex_unlock 等)操作。
pthread_mutex_t mutex 设置为全局变量而不是仅在 main 函数中定义是因为:
线程的入口函数 route 中需要调用 pthread_mutex_lock 和pthread_mutex_unlock 操作互斥锁
如果 mutex 仅在 main 函数中定义(局部变量),则 route 函数无法直接访问这个锁,导致无法实现线程同步。
如果想把mutex 放在main函数中,就需要pthread_create(&t1, NULL, route, &mutex); // 传递锁的地址,等繁琐些的操作

线程缺点

线程优点很多,但同时也是有缺点的
线程使用时需要小心,错误不好排查
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表
  • 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
  • 当前工作目录
  • 用户id和组id

线程共享进程数据,但也拥有自己的一部分数据:

  • 线程ID
  • 一组寄存器
  • errno
  • 信号屏蔽字
  • 调度优先级

强调了解的是线程之间大部分共享,但也有自己独立的数据。
最重要的两点
因为线程是独立被调度的,所以每个线程都要有自己独立的上下文数据,比如进程切换时,会在寄存器产生大量的临时数据,寄存器只有一套,但每一个进程都有属于自己的上下文数据。体现了线程之间的动态切换。

无论是C还是C++,都伴随着大量的函数调用,调用就要形成栈帧形成临时变量,所以每个线程都要有自己独立的栈结构。体现了动态运行的概念。

http://www.lryc.cn/news/613594.html

相关文章:

  • 分布式面经
  • Redis面试精讲 Day 14:Redis分片策略与一致性Hash
  • Debain12 api方式部署redis服务
  • 51c大模型~合集165
  • Tiger任务管理系统-10
  • Java 中 Object 类的解析:知识点与注意事项
  • 每日五个pyecharts可视化图表-bars(6)
  • [激光原理与应用-170]:测量仪器 - 能量型 - 光功率计的工作原理与内部功能模块组成
  • 语言模型(LM):n-gram模型原理与困惑度(Perplexity)计算详解
  • 快速搭建vue3+flask实现一个异物检测项目
  • 深入理解“进程屏蔽字“(Signal Mask)
  • Qt——入门
  • STM32学习笔记4-OLED外部中断和中断系统
  • 【C#补全计划:类和对象(九)】接口
  • 【Agent】ReAct:最经典的Agent设计框架
  • RP2040下的I2S Slave Out,PIO状态机(三)
  • 解决winform中的listbox实现拖拽时,遇到combox控件会闪烁的问题
  • 数据库事务总结
  • 嵌入式开发硬件——单片机
  • Mac 电脑安装 ADB 环境完整指南
  • windows操作系统定时关机、重启指令记录
  • vue3对比vue2的性能优化和提升 :Vue 3 vs Vue 2
  • 重学React(三):状态管理
  • windows内核研究(内存管理-线性地址的管理)
  • Java集合的遍历方式(全解析)
  • 0807 IO线程的同步互斥
  • latex in overleaf快速通关论文排版
  • FPGA学习笔记——VGA显示静态图片(ROM IP核)
  • 【数据结构入门】双向链表
  • 深入理解 S7-200 SMART 的 “数据语言”:从位到字符串的格式密码