Linux多线程概念与控制

Linux多线程概念与控制

8.1 Linux线程概念

线程本质上就是进程内部的执行流,多个线程共享同一进程的地址空间,但它们拥有独立的执行状态(如寄存器、栈等)。在 Linux 系统中,线程的实现方式使得它们比传统进程更加轻量级,CPU 看到的是线程的 PCB(task_struct),但 Linux 通过轻量级进程(LWP, Lightweight Process)机制,使线程调度更加高效。

在 Linux 内核中,task_struct 是进程/线程的核心数据结构,CPU 在调度时只关心 task_struct,而不区分是单线程还是多线程。CPU 只关注 task_struct,而不关心进程/线程的概念。

每个进程或线程都有一个 task_struct,无论是单执行流还是多执行流。

多线程程序的每个线程本质上都是一个 task_struct 实例,在 CPU 眼中,这些线程与普通进程无异,只是共享某些资源(如地址空间)。

Linux 中,线程和进程的区别并不明显,因为 Linux 线程实际上是轻量级进程

只有一个 task_struct,独占虚拟内存,运行时,CPU 看到一个 task_struct 在调度。有多个 task_struct,但它们共享同一个虚拟内存,每个线程在 CPU 眼中就是一个独立的 task_struct,并由调度器管理。

单线程:

 task_struct(进程)

    ├──进程的地址空间

    ├── files_struct(文件描述符表)

    ├── signal_struct(信号处理)

    ├── 线程私有的 kernel_stack(内核栈)

    ├── 线程私有的 thread_info(寄存器等)

   

多线程:

 task_struct(线程1

 task_struct(线程2

 task_struct(线程3

    ├── 共享 地址空间

    ├── 共享 files_struct

    ├── 共享 signal_struct

    ├── 独立的 kernel_stack(内核栈)

    ├── 独立的 thread_info(寄存器等)

线程的优点:

(1) 线程创建开销小

进程创建时,操作系统需要分配独立的虚拟内存、页表、文件描述符表、信号处理表等,开销较大。

线程创建时,由于共享地址空间、文件描述符,只需要分配独立的栈和寄存器,开销远小于进程。

对比:

进程创建:fork(),需要拷贝页表(Copy-on-Write)。

线程创建:pthread_create(),本质是 clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND),直接共享资源。

(2) 线程切换开销小

进程切换涉及地址空间切换,需要重新加载页表,成本高。

线程切换不涉及地址空间切换,只需保存/恢复寄存器和栈指针,操作更少,效率更高。调度器调度线程的代价小于调度进程。

(3) 线程占用资源少

进程独立拥有数据段、代码段、堆、栈,占用较多资源。

线程共享代码段、数据段、堆,只需独立栈和寄存器,开销小。

(4) 多核 CPU 上充分利用并行性

进程默认是独立的,只有通过 IPC(进程间通信)才能协同。

线程可以直接共享数据,在多核 CPU 上可以并行执行多个任务,提高 CPU 利用率。

示例:

Web 服务器:每个请求分配一个线程,提高吞吐量。

数据库系统:查询、事务、缓存等操作分开多个线程执行。

(5) I/O 操作时不阻塞整个进程

进程如果执行 I/O 操作,整个进程可能会阻塞,导致 CPU 资源浪费。

线程可以让一个线程等待 I/O,而其他线程继续执行计算任务,提高吞吐量。

示例:

浏览器:一个线程负责页面渲染,另一个线程下载资源,避免界面卡死。

视频播放器:解码线程和音视频同步线程并行工作,提高播放流畅度。

(6) 计算密集型任务的加速

计算密集型程序(如矩阵计算、图像处理、深度学习)可以拆分为多个线程,让不同 CPU 核心并行处理。

示例:

OpenMP 多线程加速科学计算。

机器学习训练使用多个线程并行计算梯度下降。

(7) I/O 密集型任务的吞吐量提升

在 数据库服务器、Web 服务器 这类 I/O 密集型应用中,线程可以同时等待多个 I/O 操作,避免 CPU 空闲等待,提高并发能力。

示例:

Nginx 服务器:使用异步 I/O 和多线程处理大量网络请求。

数据库引擎:多个线程同时处理查询、索引、缓存更新等。

线程的缺点:

(1) 计算密集型任务:线程数过多可能会降低性能

现象:

线程调度器会频繁切换线程,导致额外的上下文切换。线程同步(如锁、互斥量)会导致 线程间竞争,降低性能。

示例:

假设 CPU 只有 4 核,但创建了 100 个计算密集型线程,操作系统会花费大量时间在线程调度 上,而不是执行实际的计算任务。

(2) 线程之间共享资源,容易引发难以察觉的 Bug

线程共享全局变量、堆内存,如果没有合适的同步机制,可能会导致数据竞争(Race Condition)。

非确定性:线程执行顺序不确定,导致难以复现 Bug。

死锁(Deadlock):多个线程等待对方释放资源,导致程序卡死。

(3) 线程对整个进程的影响

线程共享进程的地址空间、文件描述符,一个线程如果出错,可能会影响整个进程。线程不能像进程那样独立崩溃,如果一个线程崩溃,整个进程可能都会崩溃。

(4) 多线程程序更难写、更难调试

由于线程执行顺序不确定,程序调试难度增加。

数据竞争、死锁、线程同步 Bug 都比单线程程序更难发现。

线程异常

在 Linux 中,线程异常会影响整个进程,因为所有线程共享同一个进程地址空间。如果某个线程发生段错误、除零异常或非法指令,整个进程都会终止。

线程异常导致进程崩溃的原因

(1) 线程共享进程地址空间

所有线程共享进程的代码段、数据段、堆、全局变量、文件描述符。

如果一个线程在共享资源上出错,整个进程都会受到影响。

(2) 线程异常触发信号(signal)

Linux 内核会用 信号机制处理异常。如果一个线程触发了这些信号,默认行为是终止整个进程。

线程用途

合理的使用多线程,能提高CPU密集型程序的执行效率

合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

进程是资源分配的基本单位

线程是调度的基本单位

每个线程独立的数据包括:

数据项                                           作用

线程 ID (TID)  用于标识线程,每个线程有唯一的 ID

一组寄存器       记录当前 CPU 的寄存器状态,包括 PC、SP 等

栈                每个线程都有自己的独立栈,存储局部变量、函数调用信息

errno 变量         每个线程的错误码独立,不会相互影响

信号屏蔽字       每个线程可以单独屏蔽某些信号

调度优先级       每个线程可设置不同的调度策略和优先级

重点:独立栈是最重要的,避免了多个线程修改同一栈上的局部变量,导致数据错误

所有线程共享以下进程资源:

代码段(Text Segment      线程共享相同的可执行代码

数据段(Data Segment     共享全局变量、静态变量

堆(Heap       共享动态分配的内存(如 mallocnew 分配的内存)

文件描述符表   共享打开的文件,如 open() 打开的文件

信号处理方式   共享信号的处理方式,如 SIG_IGNSIG_DFL 或者自定义处理函数

当前工作目录   线程改变进程的工作目录(chdir)会影响所有线程

用户 ID / ID     线程共享同一个用户和组身份

重点:

多线程修改全局变量时,必须加锁(如 pthread_mutex_t),否则可能发生数据竞争。堆(Heap)共享意味着不同线程可以 malloc/free 彼此分配的内存,但需要小心同步问题。

8.2 Linux线程控制

POSIX线程库:在 Linux 系统中,POSIX 线程库(POSIX Threads,简称 pthread)提供了一整套以 pthread_ 开头的 API 来管理线程。

包含头文件:#include <pthread.h>

编译时添加 -lpthread

gcc my_thread_program.c -o my_program -lpthread

POSIX 线程库包含多个重要的 API,主要分为线程创建、同步、控制、资源管理等类别。

创建线程

#include <pthread.h>

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

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

参数:

pthread_t *thread:线程 ID,用于存储新创建的线程的标识符。

const pthread_attr_t *attr :线程属性,NULL 表示默认属性。

void *(*start_routine) (void *):新线程执行的函数。

void *arg:传递给函数start_routine 的参数,可以是 NULL。

返回值

成功:返回 0。

失败:返回错误码(非 0 的值),表示线程创建失败。

线程退出

线程库(pthread)提供的线程终止函数,用于让当前线程安全地退出并返回一个值给 pthread_join()

终止当前线程,但不会影响整个进程(与 exit() 不同)。

允许返回一个指针(retval),这个值可以被 pthread_join() 获取。

线程退出后,资源不会自动回收,需要 pthread_join() 或 pthread_detach() 释放资源

#include <pthread.h>

       void pthread_exit(void *retval);

retval 是线程终止时的返回值,它可以是:

用来指示线程执行结果的数据,例如计算结果、错误码等。

pthread_join() 可以获取这个值,进行后续处理。

线程等待

pthread_join() 是一个用于等待线程终止的函数,它确保主线程或其他线程能够等待某个线程结束,并且可以获取该线程的退出状态(即 pthread_exit() 中传递的 retval 值)

#include <pthread.h>

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

thread:要等待的目标线程的 ID,类型为 pthread_t,由 pthread_create() 创建线程时返回。

retval:指向 void* 类型的指针,用于接收线程的返回值,即该线程通过 pthread_exit() 传递的 retval。

返回值:

0:表示成功,线程成功被 join。

非 0 值:表示失败,返回错误码。

示例:

#include <pthread.h>

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

void *funtest(void *arg)

{

    int id = *(int *)arg;

    printf("线程TID:%lu,传入的值:%d\n", pthread_self(), id);

    int *retval = (int *)malloc(sizeof(int));

    *retval = id + 10;

    pthread_exit((void *)retval);

    return NULL;

}

int main()

{

    // 创建五个线程ID数组

    pthread_t Tid[5];

    int nums[] = {1, 2, 3, 4, 5};

    for (int i = 0; i < 5; i++)

    {

        if (pthread_create(Tid, NULL, funtest, (void *)&nums[i]) != 0)

        {

            perror("创建线程失败!");

            return 1;

        }

        void *retval;

        pthread_join(Tid[0], &retval);

        printf("传回的值:%d\n", *(int *)retval);

        free(retval);

        sleep(1);

    }

    return 0;

}

线程分离

默认情况下,新创建的线程是可连接(joinable)的,即它的资源不会自动释放,必须调用 pthread_join() 才能回收资源。如果不调用 pthread_join(),线程的资源会一直占用,导致内存泄漏,类似于僵尸进程的问题。如果不关心线程的返回值,pthread_join() 可能会成为一种额外的开销。在这种情况下,可以让线程在退出时自动释放资源,避免手动 pthread_join(),这称为线程分离。

设置线程分离有两种方法:

在线程外部(由其他线程设置):使用 pthread_detach(thread_id)

在线程内部(自己设置自己):使用 pthread_detach(pthread_self())

    #include <pthread.h>

       int pthread_detach(pthread_t thread);

pthread_t thread:要分离的线程的 线程 ID(由 pthread_create() 生成)

成功:返回 0。

失败:返回错误码(非零),并不会设置 errno

线程分离 vs 线程连接

pthread_join()    等待线程结束,并获取线程的返回值      需要线程返回结果时

pthread_detach()      让线程资源自动释放,线程退出后系统自动回收,无需 join()     不关心返回值,只关心执行完成

注意事项:

已经分离的线程不能再 pthread_join(),避免对同一线程多次 pthread_detach();由于分离的线程无法通过 pthread_join() 确保它执行完毕,主线程可能会提前退出,导致分离线程来不及执行完:

#include <pthread.h>

#include <stdio.h>

#include <unistd.h>

void *funtest(void *arg)

{

    int num = *(int *)arg;

    printf("线程TID:%lu,传入的值:%d\n", pthread_self(), num);

    sleep(1);

    sleep(2); // 模拟工作

    printf("线程 %lu 结束\n", pthread_self());

    pthread_exit(NULL);

}

int main()

{

    int num = 10;

    pthread_t Tid;

    if (pthread_create(&Tid, NULL, funtest, (void *)&num) != 0)

    {

        printf("创建线程失败!");

        return 1;

    }

    if (pthread_detach(Tid) != 0)

    {

        printf("线程分离失败!");

        return 1;

    }

    printf("主线程执行,不等待子线程\n");

    sleep(3); // 确保子线程有足够时间运行

    printf("主线程退出\n");

}

8.3线程ID及进程地址空间布局

线程ID: 在 Linux 中,每个线程都有一个唯一的 ID,通常由 内核线程 ID(TID) 和 POSIX 线程 ID(pthread_t) 组成。

线程 ID 的类型:

pthread_t      POSIX 线程库 pthread_create() 返回的 ID,用户级标识

TID 线程在内核中的唯一标识 (gettid() 获取)

PID 进程 ID,在 Linux 内核中,每个进程的主线程的 TID = PID

先创建的线程可能为PID+1

#include <pthread.h>

#include <stdio.h>

#include <sys/syscall.h>

#include <unistd.h>

#include <sys/types.h>

void* thread_func(void *arg) {

    sleep(2);

    printf("线程 TID(pthread_self):%lu\n", pthread_self());

    printf("线程 TID(syscall gettid):%ld\n", syscall(SYS_gettid));

    return NULL;

}

int main() {

    pthread_t tid;

    pthread_create(&tid, NULL, thread_func, NULL);

    printf("主线程 TID(syscall gettid):%ld\n", syscall(SYS_gettid));

    printf("进程PID:%ld\n",getpid());

    pthread_join(tid, NULL);

    return 0;

}

多线程进程的地址空间

在 Linux 下,多线程的进程共享大部分地址空间,但每个线程也有自己的私有部分。理解这一点,可以从进程的内存布局和线程间的共享 vs 私有两个方面展开。

线程共享的部分

共享部分                                       说明

代码段                      可执行指令,所有线程都可以执行相同代码。

数据段                      全局变量、静态变量,所有线程都可访问和修改。

堆 (Heap)                 通过 malloc() 分配的内存,所有线程都可以访问。

文件描述符表          进程打开的文件(open()),所有线程可共享。

信号处理方式          SIG_IGN, SIG_DFL 以及自定义信号处理函数。

当前工作目录          chdir() 改变进程的当前目录,所有线程都会受影响。

用户 ID / 组 ID            setuid() 影响整个进程,所有线程都会变更。

线程私有的部分

私有部分                         说明

寄存器               线程调度时,每个线程都有自己的寄存器状态。

栈 (Stack)         局部变量、函数调用的返回地址,不同线程互不影响。

线程 ID             每个线程都有唯一 pthread_t 线程 ID。

线程局部存储 (TLS)使用 __thread 或 thread_local 定义的变量,每个线程独立。

错误码 (errno)  errno 是线程局部存储的,每个线程都有自己的 errno 变量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值