Linux线程:并发编程的双刃剑

目录

一线程的优点

1.1创建和删除一个新线程的代价要比创建一个新进程小得多

1.2与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

cpu缓存(缓存最近使用的数据和指令)

局部性原理:

cpu怎么判断切换的是进程还是线程?

MMU缓存(缓存最近使用的页表项)

1.3线程还有很好的并发性

二线程的缺点

1.性能损失

2.健壮性降低

3.编程难度提高

三线程异常

四线程VS进程

五结语

在计算机并发编程领域,进程与线程是支撑系统并发执行的两大核心基础单元,而线程凭借轻量化的特性,成为当下高并发业务场景的首选并发方案。相较于传统进程,线程在资源占用、调度效率上具备显著差异化优势,能够极大降低系统调度开销、提升硬件资源利用率。但任何技术都存在双面性,线程并非适配所有场景,其共享资源的运行特性也带来了数据竞争、程序稳定性下降等一系列问题。为了清晰厘清线程的技术特性与适用边界,本文将从性能原理、优缺点及实际落地场景出发,全面剖析线程的核心机制与使用要点。

一线程的优点

1.1创建和删除一个新线程的代价要比创建一个新进程小得多

进程是资源分配的最小单位,线程是CPU调度的最小单位。 创建一个新进程相当于"另起炉灶"(需要为进程分配独立的地址空间,PCB......),而创建一个新线程相当于"在现有炉灶上多加一个灶眼"(只需分配一个tcb)。

1.2与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

CPU上下文切换时,需要保存和恢复的内容有寄存器状态,内存管理单元(MMU)状态,还会影响cpu缓存的内容。

1)最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上

下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能

损耗是将寄存器中的内容切换出。将寄存器中的内容切换出是固定开销 ,但是MMU相关寄存器(如CR3)只有进程切换才需要切换。注意寄存器切换的损耗本身,不是造成进程切换比线程切换慢几十倍甚至上百倍的原因如果只算切出/切入寄存器的损耗,而不考虑缓存和TLB失效,那么进程和线程切换的代价相差很小,几乎可以忽略不计。

2)另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。 简单的说,一旦去切换上下

文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚

拟内存空间的时候,处理的页表缓冲TLB (快表)会被全部刷新,这将导致内存的访问在一

段时间内相当的低效。但是在线程的切换中,不会出现这个问题,当然还有硬件cache。

cpu缓存(缓存最近使用的数据和指令)

指令:lscpu 查看缓存信息

局部性原理:

CPU缓存很小(L1只有32KB,L2几百KB),而主存很大(16GB以上)。但现代CPU运行程序时,90%以上的内存访问都能在缓存中直接命中,而不是去慢得多的主存。

cpp 复制代码
// 好局部性:顺序访问数组
int a[10000];
for (int i = 0; i < 10000; i++) {
    a[i] = i;  // 每次访问a[i]时,缓存都"恰好"已经包含了它
}

缓存并不知道你会访问a[1],但它利用了空间局部性 ------当访问a[0]时,硬件一次性把a[0]及其附近的一整块数据(缓存行,通常64字节) 都加载到缓存中。

缓存本质上就是一种预加载机制 ,它基于一个核心假设:"你刚刚用过的数据,以及它旁边的数据,在不久的将来很可能也会被用到"。这就是局部性原理。

上下文切换时:

  • 如果是 线程切换(同一进程内):新线程可以直接复用旧线程留下的缓存数据,因为这些数据对应的物理页属于同一个进程。

  • 如果是 进程切换(不同进程间):旧进程的数据仍然留在缓存中,但新进程无法使用,因为对应的物理页属于另一个进程。新进程必须用自己的数据覆盖它们。

cpu怎么判断切换的是进程还是线程?

时间片决定切换的是进程还是线程。时间片是操作系统为每个调度单位(通常是线程,在某些简单操作系统中是进程)分配的CPU执行时间配额。

线程时间片在进程时间片里是平分的。时间片耗尽就除法调度。根据切换类型看会不会导致缓存失效。

MMU缓存(缓存最近使用的页表项

TLB(Translation Lookaside Buffer,快表)是MMU内部的一个小容量、高速度的缓存 ,用于存储虚拟地址到物理地址的转换结果(即页表项)。

在上下文切换中,寄存器切换是"必须的固定开销",而缓存失效是"造成巨大性能落差的主因"

1.3线程还有很好的并发性

1.能充分利用多处理器的可并行数量

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

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

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

二线程的缺点

1.性能损失

如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指

的是增加了额外的同步和调度开销,而可用的资源不变。

2.健壮性降低

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// 全局共享变量
int shared_counter = 0;

// 每个线程执行的函数
void* increment_unsafe(void* arg) {
    int times = *(int*)arg;  // 从参数获取循环次数
    
    for (int i = 0; i < times; ++i) {
        // 非原子操作:读-改-写
        shared_counter = shared_counter + 1;
    }
    
    return NULL;
}

int main() {
    const int NUM_THREADS = 10;
    const int INCREMENTS_PER_THREAD = 100000; // 每个线程10万次
    pthread_t threads[NUM_THREADS];
    int args[NUM_THREADS];
    
    // 创建线程
    for (int i = 0; i < NUM_THREADS; ++i) {
        args[i] = INCREMENTS_PER_THREAD;
        int ret = pthread_create(&threads[i], NULL, increment_unsafe, &args[i]);
        if (ret != 0) {
            fprintf(stderr, "创建线程失败: %d\n", ret);
            exit(1);
        }
    }
    
    // 等待所有线程结束
    for (int i = 0; i < NUM_THREADS; ++i) {
        pthread_join(threads[i], NULL);
    }
    
    // 输出结果
    int expected = NUM_THREADS * INCREMENTS_PER_THREAD;
    printf("期望值: %d\n", expected);
    printf("实际值: %d\n", shared_counter);
    printf("差值: %d\n", expected - shared_counter);
    
    if (shared_counter != expected) {
        printf("⚠️ 线程不安全:数据遭到破坏!\n");
    }
    
    return 0;
}

程序每次运行结果不同,且总是小于期望值

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

int main();

void *threadrun(void *args)
{
    main();
    return nullptr;
}

int main()
{
    pthread_t t;
    pthread_create(&t, nullptr, threadrun, nullptr);
    return 0;
}

它会引发无限递归栈溢出,最终导致程序崩溃。

在多线程中,重入函数是很常见的。

3.编程难度提高

三线程异常

单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃

线程是进程的执行分支,线程出异常,就类似进程出异常 ,进而触发信号机制,终止进程,进程

终止**,该进程内的所有线程也就随即退出**

四线程VS进程

1.进程间具有独立性,线程不独立

2.线程共享地址空间,也就共享进程资源

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

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

进程和线程的关系如下图:

之前学习的单进程就是具有一个线程执行流的进程

五结语

综上所述,线程的核心价值集中于轻量化的调度能力与高效的并发处理性能,依托共享进程地址空间的运行机制,线程规避了进程创建与上下文切换的高额开销 ,能够充分释放多核处理器的算力优势,实现I/O操作重叠执行,大幅提升程序并发处理效率,这也是其广泛应用于高并发系统的核心原因。但与此同时,**多线程带来的资源竞争、数据不一致、单线程异常牵连整体进程崩溃等问题,也提升了程序开发与调试的复杂度。**因此,在实际项目开发中,我们不能盲目滥用多线程技术,需要结合业务场景权衡利弊,通过合理的同步机制规避并发风险,最大化发挥线程的轻量化并发优势,在保障程序健壮性与稳定性的前提下,实现系统性能的最优提升。

相关推荐
程序员晨曦3 小时前
深入浅出JVM内存结构
jvm·面试·职场和发展
cfm_29146 小时前
JVM对象创建与内存分配机制深度解析
jvm
wuminyu6 小时前
Java锁膨胀机制之偏向锁到轻量级锁源码剖析
java·linux·c语言·jvm·c++
cfm_29146 小时前
JVM内存模型深度剖析与性能优化
jvm·性能优化
cfm_29147 小时前
JVM对象逃逸分析深度详解
java·开发语言·jvm
Full Stack Developme16 小时前
JVM 与 Linux 交互的核心原理
linux·运维·jvm
cfm_291416 小时前
JVM GC垃圾回收初步了解
java·开发语言·jvm
cfm_291421 小时前
JVM类加载深入理解
jvm
程序员二叉21 小时前
【JVM】OOM详解+JVM参数+FullGC排查+CPU飙高+死锁+内存泄漏+命令大全
java·开发语言·jvm·面试