25.线程概念和控制(二)

一、线程周边问题

1.线程的优点

  • 创建一个新线程的代价要比创建一个新进程小得多。

  • 线程占用的资源要比进程少很多。

  • 能充分利用多处理器的可并行数量。

  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。

  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。

  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

核心:
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多:
最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
另外⼀个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制 。简单的说,⼀旦去切换上下文,处理器中所有已经缓存的内存地址⼀瞬间都作废了。还有⼀个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲 TLB**(快表)会被全部刷新,这将导致内存的访问在⼀段时间内相当的低效。** 但是在线程的切换中,不会出现这个问题,当然还有硬件cache。

2.线程的缺点

性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同⼀个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高
编写与调试⼀个多线程程序比单线程程序困难得多。

3.进程和线程对比

进程是资源分配的基本单位。
线程是调度的基本单位。
线程共享进程数据,但也拥有自己的一部分数据:
线程ID
组寄存器,线程的上下文数据(核心)
栈(核心)
errno
信号屏蔽字
调度优先级
栈说明线程有自己的入口函数,栈存放的是临时变量,说明线程是一个动态的概念。
一组寄存器说明线程是被独立调度的。
进程的多个线程共享:
同⼀地址空间,因此Text Segment、Data Segment都是共享的,如果定义⼀个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
文件描述符表
每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
当前工作目录
用户id和组id

二、线程控制

ps -aL可以用来查看用户创建的线程。

task_struct

{

pid_t pid;

pid_t lwp;

}

light weight process:轻量级进程。CPU调度的时候,看lwp。

线程相关子问题:

1.关于调度的时间片问题:等分给不同的线程(防止线程无限分裂)

2.异常之后?任何一个线程崩溃,都会导致整个进程崩溃!

3.线程库pthread是用户层和操作系统层之间的一层中间层,在编译时需要指定动态链接,带上 -lpthread

Linux中,不存在真正意义上的线程,它所谓的概念,使用轻量级进程模拟的。在OS中,只有轻量级进程,所谓的线程,只是用户层的概念。

但用户层只认线程,因此pthread库诞生了,把创建轻量级进程封装起来,给用户提供一批创建线程的接口。

linux的线程实现,是在用户层实现的。我们称之为:用户级线程。pthread为原生线程库。

拓展:

linux创建轻量级进程的接口,vfork和clone创建子进程,和父进程共享地址空间。

C++11的多线程,在linux下,本质是封装了pthread库。

语言为了其跨平台可移植性,其对应的线程实现都是封装了各个操作系统的系统调用或对应的库,通过条件编译,形成库。

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,

void *(*start_routine)(void *), void *arg);

thread:线程id,输出型参数(注:这个线程id是线程库的概念,和lwp不同)

attr:线程属性,设置称nullptr

start_routine:新线程要执行的函数入口,一个函数指针

arg:新线程要执行的函数入口的参数

返回值:成功返回0,失败返回错误数字。

注:线程创建好之后,新线程要被主线程等待->类似僵尸进程的问题,内存泄漏。

int pthread_join(pthread_t thread, void **retval);

thread:线程id

retval:返回值指针,输出型参数

pthread_t pthread_self(void); //获取当前线程的id

为什么retval没有信号部分的内容(没有异常相关的内容)?

等待的目标线程,如果异常了,整个进程都退出了,包括main线程,所以,join异常,没有意义,看也看不到!join都是基于:线程健康跑完的情况,不需要处理异常信号,异常信号,是进程要处理的话题!!!
POSIX线程库
1.线程终止

1)线程的入口函数,进行return就是线程终止
注意:线程不能用exit终止,因为exit是终止进程的
2)pthread_exit()
3)如果线程被取消,退出结果是-1(PTHREAD_CANCLED)

pthread_exit函数
功能:线程终⽌
原型:
void pthread_exit(void *value_ptr);
参数:
value_ptr:value_ptr不要指向⼀个局部变量。
返回值:
无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的, 不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
pthread_cancel函数
功能:取消⼀个执⾏中的线程
原型:
int pthread_cancel(pthread_t thread);
参数:
thread:线程ID
返回值:成功返回0;失败返回错误码
特别注意:取消的时候一定要保证新线程已经启动。

2.线程分离

如果主线程不想再关心新线程,而是当新线程结束的时候,让他自己释放??
设置新线程为分离状态

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

想让新线程结束之后,自己退出,设置为分离状态(!joinable or detach) // TODO
理解层面:线程分离,主分离新,新把自己分离。

分离的线程,依旧在进程的地址空间中,进程的所有资源,被分离的线程,依旧可以访问,可以操作。主不等待新线程。
如果线程被设置为分离状态,不需要进行join,join会失败!!

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <string>
#include <cstring>
#include <unistd.h>

void *routine(void *args)
{
    std::string name = (char *)args;
    while (true)
    {
        sleep(1);
        std::cout << "I am a thread, name: " << name << std::endl;
    }

    return (void *)123;
}

int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, routine, (void *)"thread-1");

    // 分离新线程
    pthread_detach(tid);

    sleep(10);
    // 取消新线程
    pthread_cancel(tid);

    // 等待新线程,获取返回值
    void *retval = nullptr;
    int ret = pthread_join(tid, &retval);
    if (ret != 0)
        std::cout << "等待失败," << strerror(ret) << std::endl;
    else
        std::cout << "等待成功" << std::endl;

    while (true)
    {
        std::cout << "当前是主线程,新线程已被回收,
        退出信息为:" << (long long int)retval << std::endl;
        sleep(1);
    }

    return 0;
}

三、线程ID及地址空间布局

Linux中没有真正意义上的线程 -> OS不会直接提供线程的接口 -> 在用户层,封装轻量级进程,形成原生线程库 -> 动态库,ELF格式 -> 加载到内存和映射到进程地址空间。

线程的概念是在库中维护的,在库内部,一定会事先存在一个或多个被创建好的线程,如何管理这些线程?先描述,在组织。

struct tcb

{

// 线程应有的属性

// 线程状态

// 线程ID

// 线程独立的栈结构

// 线程栈大小

... ...

};

为什么不包括上下文,lwp,优先级?因为这是linux系统内核的具体实现,不是操作系统学科对于线程的描述,用户不需要关心具体实现。

进程自己的代码区可以访问到pthread库内部的函数和数据。

线程ID本质:管理线程块的起始虚拟地址。

线程传参和返回值:保存在线程管理块中

线程分离:线程管理块中存在标识位,标识线程是否分离。


以下是 glibc-2.4 中 pthread 源码相关内容:
路径: nptl/pthread_create.c

线程tcb结构属性:

更正tid为lwp,轻量级进程的id。

创建线程create_thread

相关推荐
小糖学代码3 小时前
LLM系列:1.python入门:3.布尔型对象
linux·开发语言·python
shizhan_cloud3 小时前
Shell 函数的知识与实践
linux·运维
Deng8723473484 小时前
代码语法检查工具
linux·服务器·windows
霍夫曼6 小时前
UTC时间与本地时间转换问题
java·linux·服务器·前端·javascript
月熊6 小时前
在root无法通过登录界面进去时,通过原本的普通用户qiujian如何把它修改为自己指定的用户名
linux·运维·服务器
大江东去浪淘尽千古风流人物7 小时前
【DSP】向量化操作的误差来源分析及其经典解决方案
linux·运维·人工智能·算法·vr·dsp开发·mr
赖small强8 小时前
【Linux驱动开发】NOR Flash 技术原理与 Linux 系统应用全解析
linux·驱动开发·nor flash·芯片内执行
IT运维爱好者9 小时前
【Linux】LVM理论介绍、实战操作
linux·磁盘扩容·lvm
LEEE@FPGA9 小时前
ZYNQ MPSOC linux hello world
linux·运维·服务器
郝学胜-神的一滴9 小时前
Linux定时器编程:深入理解setitimer函数
linux·服务器·开发语言·c++·程序人生