【Linux】线程

【Linux】线程

一、线程的核心概念

1.1 什么是线程

线程是"进程内部的控制序列",是操作系统调度的最小单位。一个进程至少包含一个主线程,也可以创建多个子线程,所有线程共享进程的地址空间和资源,同时拥有自己的私有上下文。

核心特征

  • 线程在进程地址空间内运行,共享代码段、数据段、文件描述符等资源。
  • 线程拥有独立的线程ID、寄存器上下文、栈空间、errno和信号屏蔽字。
  • 内核视角:Linux没有专门的线程结构体,线程本质是"轻量级进程",通过task_struct结构体管理,与进程共享mm_struct(虚拟地址空间)。

1.2 线程与进程的区别与联系

维度 进程 线程
资源分配 资源分配的基本单位 调度的基本单位
地址空间 独立地址空间,进程间隔离 共享进程地址空间
创建开销 大(需分配地址空间、页表等) 小(仅需创建线程上下文)
切换开销 大(需切换地址空间、刷新TLB) 小(仅切换寄存器和栈)
通信方式 需借助IPC(管道、共享内存等) 可直接访问进程全局变量
健壮性 进程崩溃不影响其他进程 单个线程崩溃导致整个进程终止

1.3 线程的优势与缺点

优势
  • 开销小:创建和切换线程的成本远低于进程。
  • 高并发:多线程可充分利用多CPU核心,提升程序执行效率。
  • 资源共享:线程间共享进程资源,通信无需额外IPC机制,效率更高。
  • IO密集型优化:线程等待IO时,其他线程可继续执行,提升程序吞吐量。
缺点
  • 健壮性低:线程缺乏资源隔离,一个线程的错误(如野指针)会导致整个进程崩溃。
  • 编程复杂:需处理线程同步、竞态条件等问题,调试难度高。
  • 缺乏访问控制:进程是资源访问控制的基本单位,线程无法单独设置资源权限。

二、线程的内存管理基础

要理解线程,必须先掌握Linux的分页式存储管理和进程地址空间布局,这是线程资源共享与私有隔离的底层基础。

2.1 分页式存储管理

2.1.1 虚拟地址与物理地址映射
  • 虚拟地址:操作系统为每个进程分配的连续逻辑地址空间(32位系统为0~4GB)。
  • 物理地址:内存硬件的实际地址,离散分配。
  • 页表:建立虚拟地址与物理地址的映射关系,将虚拟地址划分为"页"(4KB),物理地址划分为"页框",通过页表实现离散映射。
2.1.2 多级页表与TLB

单级页表会占用大量连续内存(32位系统需4MB),Linux采用二级页表解决该问题:

  1. 页目录表:存储二级页表的物理地址。
  2. 页表:存储虚拟页与物理页框的映射。
  3. TLB(转译后备缓冲器):缓存近期使用的页表项,加速地址转换(减少页表查询次数)。
2.1.3 缺页异常

当CPU访问的虚拟地址未映射到物理页时,会触发缺页异常,内核处理流程:

  • 硬缺页:物理内存中无对应页,需从磁盘加载到内存并建立映射。
  • 软缺页:物理内存已有对应页(如共享内存),仅需建立映射。
  • 无效缺页:地址越界或无权限,触发SIGSEGV信号终止进程。

2.2 线程的地址空间布局

进程地址空间中,线程的资源分布如下:

  • 共享资源:代码段(.text)、数据段(.data/.bss)、堆区、文件描述符表、信号处理方式、当前工作目录。
  • 私有资源
    • 主线程栈:位于进程栈区(从高地址向下生长)。
    • 子线程栈:位于共享区(mmap区域),默认大小为8MB,通过pthread_attr_t可修改。
    • 线程控制块(TCB):存储线程ID、栈地址、状态等信息,本质是进程地址空间中的一块内存(pthread_t类型本质是该地址)。

地址空间布局示意图(从高地址到低地址):

复制代码
内核空间
├── 主线程栈
├── 共享区(mmap):子线程栈、动态库、线程局部存储(TLS)
├── 堆区(向上生长)
├── .bss段(未初始化全局变量)
├── .data段(已初始化全局变量)
└── .text段(代码段)

二、线程控制

Linux通过POSIX线程库(libpthread)提供线程控制接口,所有函数以pthread_开头,使用时需链接线程库(编译选项-lpthread)。

2.1 线程创建(pthread_create)

c 复制代码
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, 
                   void *(*start_routine)(void*), void *arg);
  • 参数说明
    • thread:输出参数,返回创建的线程ID。
    • attr:线程属性(如栈大小、分离状态),NULL表示使用默认属性。
    • start_routine:线程启动后执行的函数(返回值和参数均为void*)。
    • arg:传递给线程函数的参数。
  • 返回值 :成功返回0,失败返回错误码(不设置errno)。

实战案例:创建子线程

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

// 线程执行函数
void* thread_func(void* arg) {
    char* msg = (char*)arg;
    while (true) {
        std::cout << "子线程[" << pthread_self() << "]:" << msg << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main() {
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, thread_func, (void*)"hello thread");
    if (ret != 0) {
        std::cerr << "创建线程失败:" << strerror(ret) << std::endl;
        return 1;
    }

    // 主线程执行
    while (true) {
        std::cout << "主线程[" << pthread_self() << "]:running" << std::endl;
        sleep(1);
    }

    return 0;
}

编译运行

bash 复制代码
g++ thread_create.cpp -o thread_create -lpthread
./thread_create

2.2 线程ID与线程属性

2.2.1 线程ID
  • pthread_self():获取当前线程的ID(进程内唯一,本质是线程控制块的地址)。
  • LWP(轻量级进程ID) :内核分配的系统级线程ID,可通过ps -aL查看。
bash 复制代码
# 查看线程信息
ps -aL | grep thread_create
# 输出示例:
# PID    LWP     TTY      TIME CMD
# 1234   1234    pts/0    00:00:00 thread_create  # 主线程(LWP=PID)
# 1234   1235    pts/0    00:00:00 thread_create  # 子线程
2.2.2 线程属性(pthread_attr_t)

常用属性设置:

  • 栈大小:pthread_attr_setstacksize
  • 分离状态:pthread_attr_setdetachstate(设置线程是否可分离)。

示例:设置线程栈大小

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

void* thread_func(void* arg) {
    std::cout << "子线程栈大小:" << pthread_get_stacksize_np(pthread_self()) << std::endl;
    return nullptr;
}

int main() {
    pthread_attr_t attr;
    pthread_attr_init(&attr);  // 初始化属性
    pthread_attr_setstacksize(&attr, 1024*1024);  // 设置栈大小为1MB

    pthread_t tid;
    pthread_create(&tid, &attr, thread_func, nullptr);
    pthread_join(tid, nullptr);

    pthread_attr_destroy(&attr);  // 销毁属性
    return 0;
}

2.3 线程终止

线程终止有三种合法方式,避免直接调用exit(会终止整个进程):

  1. 线程函数return返回(主线程return等价于exit)。
  2. 调用pthread_exit主动终止当前线程。
  3. 其他线程调用pthread_cancel取消当前线程。
2.3.1 pthread_exit函数
c 复制代码
void pthread_exit(void *value_ptr);
  • value_ptr:线程退出状态,需指向全局变量或堆内存(不能是局部变量)。
2.3.2 pthread_cancel函数
c 复制代码
int pthread_cancel(pthread_t thread);
  • 取消指定线程,被取消线程的退出状态为PTHREAD_CANCELED
  • 注意:线程可通过设置取消点控制是否响应取消。

2.4 线程等待(pthread_join)

线程退出后,需通过pthread_join回收其资源,否则会产生"僵尸线程",占用系统资源。

c 复制代码
int pthread_join(pthread_t thread, void **value_ptr);
  • thread:待等待的线程ID。
  • value_ptr:输出参数,接收线程的退出状态。
  • 功能:调用线程会阻塞,直到目标线程终止。

实战案例:线程等待与退出状态

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

// 线程1:return返回
void* thread_ret(void* arg) {
    std::cout << "线程1:通过return退出" << std::endl;
    int* ret = new int(100);
    return (void*)ret;
}

// 线程2:pthread_exit退出
void* thread_exit(void* arg) {
    std::cout << "线程2:通过pthread_exit退出" << std::endl;
    int* ret = new int(200);
    pthread_exit((void*)ret);
}

// 线程3:被取消
void* thread_cancel(void* arg) {
    while (true) {
        std::cout << "线程3:运行中(可被取消)" << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main() {
    pthread_t tid1, tid2, tid3;
    void* ret;

    // 等待线程1
    pthread_create(&tid1, NULL, thread_ret, NULL);
    pthread_join(tid1, &ret);
    std::cout << "线程1退出状态:" << *(int*)ret << std::endl;
    delete (int*)ret;

    // 等待线程2
    pthread_create(&tid2, NULL, thread_exit, NULL);
    pthread_join(tid2, &ret);
    std::cout << "线程2退出状态:" << *(int*)ret << std::endl;
    delete (int*)ret;

    // 取消并等待线程3
    pthread_create(&tid3, NULL, thread_cancel, NULL);
    sleep(3);
    pthread_cancel(tid3);
    pthread_join(tid3, &ret);
    if (ret == PTHREAD_CANCELED) {
        std::cout << "线程3被取消" << std::endl;
    }

    return 0;
}

2.5 线程分离(pthread_detach)

默认情况下,线程是"可连接的(joinable)",必须通过pthread_join回收资源。若不关心线程退出状态,可将线程设置为"分离态(detached)",线程退出后自动释放资源。

c 复制代码
int pthread_detach(pthread_t thread);
  • 可由其他线程调用,也可线程自分离:pthread_detach(pthread_self())
  • 注意:分离态线程不能被pthread_join等待,否则返回错误。

实战案例:线程分离

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

void* thread_func(void* arg) {
    pthread_detach(pthread_self());  // 线程自分离
    std::cout << "分离态线程:运行中" << std::endl;
    sleep(2);
    std::cout << "分离态线程:退出" << std::endl;
    return nullptr;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);
    sleep(1);  // 等待线程完成分离

    // 尝试等待分离态线程(会失败)
    int ret = pthread_join(tid, NULL);
    if (ret != 0) {
        std::cerr << "pthread_join失败:" << strerror(ret) << std::endl;
    }

    sleep(2);  // 等待线程退出
    return 0;
}

三、线程的内存布局与内核实现

3.1 线程的地址空间分布

进程地址空间中,线程的私有资源和共享资源分布如下:

  • 共享资源
    • 代码段(.text):进程的可执行代码。
    • 数据段(.data/.bss):全局变量和静态变量。
    • 堆区:动态分配的内存(malloc/new分配)。
    • 文件描述符表、信号处理方式、当前工作目录。
  • 私有资源
    • 线程栈:主线程栈位于进程栈区(高地址向下生长),子线程栈位于共享区(mmap分配,默认8MB)。
    • 线程控制块(TCB):pthread库维护的struct pthread结构体,存储线程ID、栈地址、退出状态等信息。
    • 线程局部存储(TLS):通过__thread关键字定义的变量,每个线程有独立副本。

3.2 内核对线程的实现

Linux内核没有专门的线程结构体,线程本质是"轻量级进程",通过clone系统调用创建,与进程共享mm_struct(虚拟地址空间):

  • clone系统调用:创建轻量级进程,通过标志位控制资源共享(CLONE_VM共享地址空间、CLONE_FILES共享文件描述符等)。
  • 内核视角:每个线程对应一个task_struct,共享mm_structfiles_struct等结构体,实现资源共享。

核心流程

  1. 调用pthread_create时,libpthread库先通过mmap为线程分配栈空间和TCB。
  2. 调用clone系统调用,内核创建task_struct,共享进程的mm_struct
  3. 线程执行指定的start_routine函数,运行结束后释放资源。

四、线程封装实战:面向对象的线程类

基于POSIX线程库,封装一个通用的线程类,支持线程创建、启动、等待和分离,简化多线程编程。

4.1 线程类实现(Thread.hpp)

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <unistd.h>

namespace ThreadModule {
    // 线程状态
    enum class ThreadStatus {
        NEW,        // 未启动
        RUNNING,    // 运行中
        STOPPED     // 已停止
    };

    // 线程函数类型(无参数,无返回值)
    using ThreadFunc = std::function<void()>;

    class Thread {
    public:
        // 构造函数:传入线程执行函数
        Thread(ThreadFunc func) 
            : _func(func), _status(ThreadStatus::NEW), _joined(true) {
            _name = "Thread-" + std::to_string(_cnt++);
        }

        // 禁用拷贝构造和赋值
        Thread(const Thread&) = delete;
        Thread& operator=(const Thread&) = delete;

        // 析构函数
        ~Thread() {
            if (_status == ThreadStatus::RUNNING && !_joined) {
                pthread_detach(_tid);  // 分离未等待的线程,避免资源泄漏
            }
        }

        // 设置线程为分离态
        void setDetached() {
            if (_status == ThreadStatus::NEW) {
                _joined = false;
            }
        }

        // 启动线程
        bool start() {
            if (_status != ThreadStatus::NEW) {
                std::cerr << "线程已启动" << std::endl;
                return false;
            }

            // 创建线程:传入静态成员函数作为入口,this作为参数
            int ret = pthread_create(&_tid, nullptr, threadRoutine, this);
            if (ret != 0) {
                std::cerr << "创建线程失败:" << strerror(ret) << std::endl;
                return false;
            }

            _status = ThreadStatus::RUNNING;
            return true;
        }

        // 等待线程
        bool join() {
            if (!_joined || _status != ThreadStatus::RUNNING) {
                std::cerr << "线程不可等待" << std::endl;
                return false;
            }

            int ret = pthread_join(_tid, nullptr);
            if (ret != 0) {
                std::cerr << "等待线程失败:" << strerror(ret) << std::endl;
                return false;
            }

            _status = ThreadStatus::STOPPED;
            return true;
        }

        // 获取线程名称
        std::string name() const { return _name; }

    private:
        // 静态成员函数:线程入口(必须是静态函数,无this指针)
        static void* threadRoutine(void* arg) {
            Thread* self = static_cast<Thread*>(arg);
            // 设置线程名称(调试用)
            pthread_setname_np(pthread_self(), self->_name.c_str());
            // 执行线程函数
            self->_func();
            self->_status = ThreadStatus::STOPPED;
            return nullptr;
        }

    private:
        static std::uint32_t _cnt;  // 线程计数器(用于生成名称)
        std::string _name;          // 线程名称
        pthread_t _tid;             // 线程ID
        ThreadStatus _status;       // 线程状态
        bool _joined;               // 是否可连接(默认true)
        ThreadFunc _func;           // 线程执行函数
    };

    // 初始化静态计数器
    std::uint32_t ThreadModule::Thread::_cnt = 0;
}

4.2 线程类使用示例(main.cc

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include "Thread.hpp"

using namespace ThreadModule;

// 线程1:打印信息
void printInfo() {
    while (true) {
        std::cout << "线程[" << pthread_self() << "]:" << Thread::current()->name() << " 运行中" << std::endl;
        sleep(1);
    }
}

// 线程2:计数
void countNum() {
    int cnt = 0;
    while (cnt < 5) {
        std::cout << "线程[" << pthread_self() << "]:计数 " << ++cnt << std::endl;
        sleep(1);
    }
    std::cout << "线程[" << pthread_self() << "]:计数完成" << std::endl;
}

int main() {
    // 创建线程1(默认可连接)
    Thread t1(printInfo);
    t1.start();
    std::cout << "启动线程:" << t1.name() << std::endl;

    // 创建线程2(分离态)
    Thread t2(countNum);
    t2.setDetached();
    t2.start();
    std::cout << "启动线程:" << t2.name() << std::endl;

    // 等待线程1(线程2为分离态,无需等待)
    t1.join();
    return 0;
}

4.3 编译运行

makefile 复制代码
CC = g++
FLAGS = -std=c++11 -Wall -g
LDFLAGS = -lpthread
TARGET = thread_demo
SRC = main.cc

$(TARGET): $(SRC)
	$(CC) $(FLAGS) -o $@ $^ $(LDFLAGS)

.PHONY: clean
clean:
	rm -f $(TARGET)
bash 复制代码
make
./thread_demo

五、线程的关键注意事项

5.1 线程安全与竞态条件

线程共享进程资源,当多个线程同时访问共享变量时,可能导致数据不一致,这就是竞态条件 。确保线程安全的核心是"同步与互斥",常用机制包括互斥锁(pthread_mutex_t)、条件变量(pthread_cond_t)等。

示例:竞态条件问题

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

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

void* increment(void* arg) {
    for (int i = 0; i < 10000; ++i) {
        g_cnt++;  // 非原子操作,可能导致竞态条件
    }
    return nullptr;
}

int main() {
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, increment, NULL);
    pthread_create(&tid2, NULL, increment, NULL);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    std::cout << "最终计数:" << g_cnt << std::endl;  // 可能小于20000
    return 0;
}

5.2 可重入函数

可重入函数是指多个线程同时调用时,不会因共享资源导致数据错乱的函数。线程安全的函数不一定是可重入的,但可重入函数一定是线程安全的。

不可重入函数的特征

  • 访问全局变量、静态变量或共享资源。
  • 调用malloc/free(堆内存由全局链表管理)。
  • 使用标准I/O库函数(如printf,共享缓冲区)。

5.3 线程局部存储(TLS)

线程局部存储用于存储线程私有数据,通过__thread关键字定义,每个线程有独立副本,避免线程安全问题。

示例:线程局部存储

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

__thread int t_local = 0;  // 线程局部变量

void* thread_func(void* arg) {
    t_local = *(int*)arg;
    std::cout << "线程[" << pthread_self() << "]:t_local = " << t_local << std::endl;
    return nullptr;
}

int main() {
    int a = 10, b = 20;
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, thread_func, &a);
    pthread_create(&tid2, NULL, thread_func, &b);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    return 0;
}

六、总结

Linux线程是实现并发编程的核心技术,核心要点如下:

  1. 线程共享进程地址空间,拥有独立的上下文,创建和切换开销小。
  2. 线程控制依赖POSIX线程库,核心API包括pthread_createpthread_joinpthread_exit等。
  3. 线程的内核实现本质是轻量级进程,通过clone系统调用创建,共享mm_struct
  4. 线程安全是多线程编程的核心问题,需通过同步机制(互斥锁、条件变量)避免竞态条件。
  5. 实战中可封装线程类,简化线程的创建、启动和管理。
相关推荐
智者知已应修善业1 小时前
【51单片机LED贪吃蛇】2023-3-27
c语言·c++·经验分享·笔记·嵌入式硬件·51单片机
Demon--hx2 小时前
[C++]迭代器
开发语言·c++
AndrewHZ2 小时前
【图像处理基石】如何入门图像配准算法?
图像处理·opencv·算法·计算机视觉·cv·图像配准·特征描述子
BanyeBirth2 小时前
C++窗口问题
开发语言·c++·算法
q***13343 小时前
Linux(CentOS)安装 Nginx
linux·nginx·centos
摘星编程3 小时前
openGauss 快速上手:CentOS 环境下单机部署完整指南
linux·运维·centos
前端小L4 小时前
图论专题(十五):BFS的“状态升维”——带着“破壁锤”闯迷宫
数据结构·算法·深度优先·图论·宽度优先
郝学胜-神的一滴6 小时前
Qt的QSlider控件详解:从API到样式美化
开发语言·c++·qt·程序人生
橘颂TA6 小时前
【剑斩OFFER】算法的暴力美学——连续数组
c++·算法·leetcode·结构与算法