《Linux线程——从概念到实践》

发布于:2025-09-15 ⋅ 阅读:(21) ⋅ 点赞:(0)

1. 线程的概念

在Linux中,线程 (Thread) 程序执行过程中最小的调度单位。线程是操作系统能够进行运算调度的基本单位,而进程是资源分配的基本单位。线程在同一个进程内共享资源(如内存、文件描述符等),但它们各自拥有独立的执行路径和程序计数器(PC)。线程的概念使得程序能够并发执行,提高效率。

1.2 为什么要使用线程?

在计算机世界里,程序的运行效率始终是开发者追求的核心目标之一。传统的单进程模型如同 “单线程工人”,一次只能处理一项任务,在面对复杂业务或高并发场景时显得力不从心。而 线程(Thread) 的出现,如同为程序配备了 “多线程工人团队”—— 它们共享资源却又能独立执行,让程序在效率与灵活性上实现了质的飞跃。

1.3 线程 VS 进程

维度 进程 线程
资源分配单位 是(拥有独立内存、文件句柄等) 否(共享进程资源)
调度单位 是(但开销大,现代系统更倾向调度线程) 是(操作系统直接调度的最小单位)
创建开销 高(需分配内存、初始化资源) 低(仅需创建线程栈和上下文)
通信复杂度 高(需通过 IPC 机制,如管道、消息队列) 低(可直接共享内存,无需额外机制)
影响范围 进程崩溃不影响其他进程 线程崩溃可能导致整个进程崩溃
应用 服务器守护进程、需要强隔离的场景 浏览器多标签页、即时通信软件、后端服务的线程池

1.4 Linux 如何实现线程:轻量级进程(LWP)的奥秘

LWP(LightweightProcess,轻量级进程)是一种在操作系统中实现线程的方式。它是介于传统进程(Process)和线程(Thread)之间的一种抽象概念, 用于表示操作系统中的一个调度单位。

LWP的特点:

  1. 共享资源:多个LWP通常属于同一个进程,它们共享进程的资源,如内存空间、文件描述符等,但每个LWP拥有独立的程序计数器、堆栈和局部变量。这意味着,LWP之间共享大部分资源,但仍可以独立执行
  2. 调度单位:LWP是操作系统调度的基本单位,虽然LWP之间共享进程的资源,但操作系统会根据各个LWP的状态进行独立调度。LWP能够在多核系统上并行执行,因此也称为轻量级线程。
  3. 对比线程和进程:进程是一个更重的实体,它有独立的内存空间和资源,且进程切换开销较大。线程(或LWP)是轻量级的调度单位,它们共享进程的资源,但每个线程都有自己的堆栈和执行上下文。

2.创建一个线程的完整流程

POSIX:pthread线程

创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void*(*start_routine)(void *), void *arg);
返回值:成功时返回 0,失败时返回错误码(如 EAGAIN 表示资源不足,EINVAL 表示参数无效)。
参数 作用
pthread_t *thread 输出参数,用于存储新创建线程的 ID(类型为 pthread_t)。
const pthread_attr_t *attr 设置线程的属性(如栈大小、调度策略、分离状态等)。若为 NULL,则使用默认属性。
void* (start_routine)(void) 线程的入口函数,新线程启动后将执行该函数。
void *arg 传递给线程入口函数的参数(类型为 void*),支持任意类型的指针。

其他函数:

函数 作用 参数
void pthread_exit(void* retval); 主动退出当前线程(线程自己调用,结束执行) retval : 线程的 “返回值”,类型是 void*,可传递任意数据(如字符串、结构体指针等)。若不需要返回值,填 NULL 即可。
int pthread_join(pthread_t thread, void **retval); 阻塞等待指定线程结束(类似 “等人完成工作”) thread:要等待的线程的 ID(由 pthread_create 输出)。retval: 传入一个 void* 指针的地址(即 void** 类型),用于存储线程的返回值。若不需要返回值,填 NULL 即可。
int pthread_detach(pthread_t thread); 分离线程。 pthread_tthread:要分离的线程ID,使其资源在终止时自动回收。
pthread_t pthread_self(void); 获取当前线程的 ID 无参数,返回调用该函数的线程的 ID(pthread_t 类型),用于标识当前线程。
#include <pthread.h>
#include <stdio.h>

void* thread_func(void* arg) {
    printf("Hello from thread! Argument: %s\n", (char*)arg);
    return NULL;
}

int main() {
    pthread_t tid;
    char* msg = "World";
    // 创建线程,入口函数为thread_func,参数为msg
    int ret = pthread_create(&tid, NULL, thread_func, msg);
    if (ret != 0) {
        perror("pthread_create failed");
        return 1;
    }
    // 等待线程结束
    pthread_join(tid, NULL);
    return 0;
}

C++11 thread线程

使用方式很简单,这里不过多介绍

#include <iostream>
#include <thread>

// 普通函数作为线程执行体
void threadFunction() {
    std::cout << "线程ID: " << std::this_thread::get_id() << std::endl;
}

int main() {
    std::thread t(threadFunction);  // 创建并启动线程
    t.join();                       // 等待线程结束
    return 0;
}

参照:cplusplus网站

组件 作用
std::thread 线程对象,管理线程创建与生命周期
std::mutex 互斥锁,保护共享资源
std::lock_guard 自动管理互斥锁(构造加锁,析构解锁)
std::unique_lock 灵活的锁管理(支持手动解锁、延迟加锁)
std::condition_variable 线程间 “等待 - 通知” 通信
std::atomic 原子类型,提供无锁的原子操作
join() / detach() 线程同步与分离

两者区别

维度 Linux 线程(pthread) C++ 线程(std::thread)
本质 操作系统提供的底层线程接口(C 语言风格) C++ 标准库的跨平台线程抽象(面向对象风格)
跨平台性 仅支持 POSIX 兼容系统(如 Linux、macOS) 支持所有 C++11 及以上标准的平台(Linux、Windows、macOS 等)
使用方式 基于函数和结构体(如 pthread_t、pthread_mutex_t),需手动管理资源(如检查返回值、释放锁) 基于类和对象(如 std::thread、std::mutex),RAII 机制自动管理资源(如析构函数释放锁)
错误处理 通过函数返回值(如 0 表示成功,非 0 表示错误码) 通常通过抛出异常(如 std::system_error)
语言依赖 依赖 C 语言接口,可在 C/C++ 中使用 仅在 C++ 中可用,依赖 C++ 特性(如 lambda、模板)

3 多线程编程 同步与并发

如果两个或多个线程并发进行,同时访问和修改共享资源时,由于执行顺序的不确定性导致结果不可预测。

两个线程同时对全局变量 counter 执行 ++ 操作:
int count = 0;

void* increment(void* arg) {
    for (int i = 0; i < 10000; i++) {
        count++; // 非原子操作(读取→修改→写入),可能导致结果错误
    }
    return NULL;
}

CPU 对count++的执行分为 “读 - 改 - 写” 三步,若两个线程的这三步交叉执行,会导致结果小于预期(如两个线程各执行 1 万次,最终counter可能小于 2 万)。

3.1 互斥锁

Mutex(互锁,MutualExclusion)是一种关键的同步机制,用于阻止许多个线程相同访问共享资源时,而避免了状态条(Race Condition)和数据不一致的问题。

函数 作用
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutex_attr); 初始化互斥锁
pthread_mutex_destroy(pthread_mutex_t *mutex) 销毁互斥锁
pthread_mutex _lock(pthread_mutex_t *mutex) 加锁(如果已被锁,则阻塞)
pthread_mutex_trylock(pthread_mutex_t *mutex) 尝试加锁(不会阻塞)
pthread_mutex_unlock(pthread_mutex_t *mutex) 解锁

其中,在加锁过程中,pthread_mutex_lock()函数和 pthread_mutex_trylock()函数的过程略有不同:
1.当使用pthread_mutex_lock()函数进行加锁时,若此时已经被锁,则尝试加锁的线程会被阻塞,直到互斥锁被其他线程释放,当pthread_mutex_lock0函数有返回值时,说明加锁成功;
2. 而使用pthread_mutex_trylock()函数进行加锁时,若此时已经被锁,则会返回EBUSY的错误码。

1. 创建锁
pthread_mutex_t mutex;  // 定义互斥锁

// 方法一:静态初始化(使用默认属性)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 方法二:动态初始化(可设置属性)
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);  // NULL 表示使用默认属性

2. 上锁
// 阻塞式加锁:若锁被占用,则线程阻塞等待
pthread_mutex_lock(&mutex);

// 非阻塞式加锁:立即返回,成功返回0,失败返回EBUSY
int ret = pthread_mutex_trylock(&mutex);
if (ret == 0) {
    // 加锁成功
} else if (ret == EBUSY) {
    // 锁被占用,处理冲突
}

3. 解锁
pthread_mutex_unlock(&mutex);  // 释放锁,唤醒等待线程

4. 销毁锁
pthread_mutex_destroy(&mutex);  // 释放互斥锁资源
给刚才的代码加入互斥锁
pthread_mutex_t mutex;
int counter = 0;

void* increment(void* arg) {
    for (int i = 0; i < 10000; i++) {
        pthread_mutex_lock(&mutex);   // 加锁
        counter++;                    // 临界区
        pthread_mutex_unlock(&mutex); // 解锁
    }
    return NULL;
}

在c++11中引入了更方便的mutex,想要更多了解可看:
《C++11 多线程必学:std::mutex 详解与实战案例》

3.2 条件变量

在多线程场景中,线程常需等待特定条件(如数据就绪、资源释放)才能继续执行。若用轮询检查实现,会导致 CPU 空转,浪费系统资源。条件变量则提供了更高效的方案
条件变量需搭配互斥锁来使用:
条件变量用于等待 / 通知 “某个条件成立”,而条件的判断依赖共享变量(如生产者 - 消费者模型里的 “队列是否非空”)。这些共享变量属于临界资源,若不通过互斥锁保护,可能出现:

  1. 线程 A 判断 “条件不满足” 准备等待,此时线程 B 修改条件并通知,但线程 A 已错过通知,导致永久阻塞。
  2. 多个线程同时修改条件,引发数据竞争(如队列长度被并发修改)。
函数 作用
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr); 初始化条件变量,为后续线程同步操作做准备。
int pthread_cond_destroy(pthread_cond_t *cond); 销毁条件变量,释放相关资源,通常在不再需要条件变量时调用。
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); 让线程阻塞等待条件满足,需配合互斥锁(pthread_mutex_t )使用,会原子性解锁互斥锁并等待,条件满足时重新加锁并返回。
int pthread_cond_signal(pthread_cond_t *cond); 唤醒一个因该条件变量等待的线程,若有多个等待线程,按调度策略选一个唤醒。
pthread_cond_broadcast(pthread_cond_t *cond) 唤醒所有因该条件变量等待的线程,常用于需所有等待线程响应的场景 。

示例代码(封装cond类)

#pragma once

#include <iostream>
#include <pthread.h>
#include "Mutex.hpp"
//通过 C++ 类封装 POSIX 条件变量 API,简化条件变量的使用,实现线程间的等待 / 通知机制
//条件变量在构造函数中初始化(pthread_cond_init),在析构函数中销毁(pthread_cond_destroy),符合 RAII 原则,避免资源泄漏。
namespace CondModule
{
    using namespace LockModule;

    class Cond
    {
    public:
        Cond()//初始化条件变量_cond。
        {
            int n = ::pthread_cond_init(&_cond, nullptr);
            (void)n;
        }
        //Wait() 函数接收 Mutex 对象引用,通过 mutex.LockPtr() 获取底层 pthread_mutex_t*,确保条件变量与互斥锁正确配合使用。
        void Wait(Mutex &mutex)
        {
            int n = ::pthread_cond_wait(&_cond, mutex.LockPtr());
        }
        void Notify()//唤醒一个等待该条件变量的线程。
        {
            int n = ::pthread_cond_signal(&_cond);
            (void)n;
        }
        void NotifyAll()//唤醒所有等待该条件变量的线程。
        {
            int n = ::pthread_cond_broadcast(&_cond);
            (void)n;
        }
        ~Cond()
        {
            int n = ::pthread_cond_destroy(&_cond);
        }
    private:
        pthread_cond_t _cond;
    };
}

在c++11中引入了更方便的条件变量condition_variable,想要更多了解可看:
《Linux 环境下 C++ 条件变量与 POSIX 条件变量详解》

3.3 读写锁

读写锁(Read - Write Lock)是一种用于解决并发访问共享资源时同步问题的同步机制,尤其适用于读操作远多于写操作的场景,能有效提升并发性能。

3.3.1 核心概念与状态

读写锁把对共享资源的访问者分为读者(Reader)和写者(Writer):

  1. 读锁(共享锁):允许多个线程同时持有,用于纯读操作(不修改资源),因为读操作本身不影响数据一致性,并发读可提升效率。
  2. 写锁(排他锁 / 独占锁):同一时刻仅允许一个线程持有,用于写操作(修改资源),保证写操作的原子性,避免多线程写导致数据混乱。
  3. 读写锁有三种状态:读模式加锁、写模式加锁、不加锁 。

3.3.2 加锁规则(核心逻辑)

写锁优先时(常见默认行为,也可配置公平策略):

  1. 若读写锁处于写加锁状态:所有后续尝试加读锁或写锁的线程都会被阻塞,直到写锁释放。保证写操作的原子性,避免写过程中被其他线程干扰。
  2. 若读写锁处于读加锁状态:
    新的读锁请求可直接成功(多线程共享读锁);
    新的写锁请求会被阻塞,直到所有读锁都释放(避免读 - 写冲突,保证写操作能修改 “干净” 的数据 )。
  3. 特殊情况:若读锁已持有,新写锁请求到来后,后续读锁请求可能被 “限流”(部分实现会让新读锁等待,防止写请求长期阻塞,即 “写优先” 或 “读写公平” 策略 )。

公平策略下:严格按照线程请求顺序分配锁,写锁请求和读锁请求排队,避免某类操作(如读)长期占优导致另一类操作(如写)饥饿。

3.3.3 实例

在这里插入图片描述

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

// 读写锁、互斥锁、共享数据
pthread_rwlock_t rwlock;
int shared_data = 0;

// 读操作线程函数
void* read_task(void* arg) {
    while (1) {
        // 获取读锁
        pthread_rwlock_rdlock(&rwlock);
        printf("Read: shared_data = %d\n", shared_data);
        // 释放读锁
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
    return NULL;
}

// 写操作线程函数
void* write_task(void* arg) {
    int count = 0;
    while (1) {
        // 获取写锁
        pthread_rwlock_wrlock(&rwlock);
        shared_data = ++count;
        printf("Write: shared_data updated to %d\n", shared_data);
        // 释放写锁
        pthread_rwlock_unlock(&rwlock);
        sleep(3);
    }
    return NULL;
}

int main() {
    // 初始化读写锁
    pthread_rwlock_init(&rwlock, NULL);

    pthread_t read_thread1, read_thread2, write_thread;
    // 创建读线程和写线程
    pthread_create(&read_thread1, NULL, read_task, NULL);
    pthread_create(&read_thread2, NULL, read_task, NULL);
    pthread_create(&write_thread, NULL, write_task, NULL);

    // 等待线程结束(实际需更优雅退出)
    pthread_join(read_thread1, NULL);
    pthread_join(read_thread2, NULL);
    pthread_join(write_thread, NULL);

    // 销毁读写锁
    pthread_rwlock_destroy(&rwlock);
    return 0;
}

4. 实战

4.1 封装原Thread接口(区别于C++ 标准库的线程)

简化原生pthread接口的使用,提供更面向对象、更安全的线程操作方式。

#ifndef _THREAD_HPP__
#define _THREAD_HPP__

#include <iostream>
#include <string>
#include <pthread.h>
#include <functional>
#include <sys/types.h>
#include <unistd.h>

// 简化原生pthread接口的使用,提供更面向对象、更安全的线程操作方式。
// 通过std::function支持任意可调用对象作为线程函数,比原生pthread只能使用静态函数更灵活。

// v1
namespace ThreadModule
{
    // 标准化线程执行的函数签名(接受字符串参数,无返回值)
    using func_t = std::function<void(std::string name)>;
    // 为每个线程生成唯一ID的计数器
    static int number = 1;

    // 定义线程生命周期状态
    enum class TSTATUS
    {
        NEW,     // 线程已创建但未运行
        RUNNING, // 线程正在运行
        STOP     // 线程已停止
    };

    class Thread
    {
    private:
        // 成员方法!
        // 线程执行的静态函数,作为pthread_create的入口
        static void *Routine(void *args)
        {
            // 将void*参数转换回Thread对象
            Thread *t = static_cast<Thread *>(args);
            // 设置线程状态为运行中
            t->_status = TSTATUS::RUNNING;
            // 执行用户传入的函数对象,将线程名称作为参数传入
            t->_func(t->Name());
            return nullptr;
        }

        // 启用线程分离,将_joinable标记为false
        void EnableDetach() { _joinable = false; }

    public:
        // 构造函数,接受一个可调用对象作为线程执行的任务
        Thread(func_t func) : _func(func), _status(TSTATUS::NEW), _joinable(true)
        {
            // 为线程生成唯一的名称
            _name = "Thread-" + std::to_string(number++);
            // 获取当前进程的ID
            _pid = getpid();
        }

        // 启动线程
        bool Start()
        {
            // 只有当线程状态不是RUNNING时才能启动
            if (_status != TSTATUS::RUNNING)
            {
                // 创建线程,将当前Thread对象作为参数传递给Routine函数
                int n = ::pthread_create(&_tid, nullptr, Routine, this);
                // 如果创建失败,返回false
                if (n != 0)
                    return false;
                return true;
            }
            return false;
        }

        // 停止线程
        bool Stop()
        {
            // 只有当线程状态为RUNNING时才能停止
            if (_status == TSTATUS::RUNNING)
            {
                // 取消线程
                int n = ::pthread_cancel(_tid);
                // 如果取消失败,返回false
                if (n != 0)
                    return false;
                // 设置线程状态为停止
                _status = TSTATUS::STOP;
                return true;
            }
            return false;
        }

        // 等待线程结束
        bool Join()
        {
            // 只有当线程是可连接状态时才能调用join
            if (_joinable)
            {
                // 等待线程结束
                int n = ::pthread_join(_tid, nullptr);
                // 如果等待失败,返回false
                if (n != 0)
                    return false;
                // 设置线程状态为停止
                _status = TSTATUS::STOP;
                return true;
            }
            return false;
        }

        // 分离线程
        void Detach()
        {
            // 启用线程分离
            EnableDetach();
            // 分离线程
            pthread_detach(_tid);
        }

        // 判断线程是否是可连接状态
        bool IsJoinable() { return _joinable; }

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

        // 析构函数
        ~Thread()
        {
        }

    private:
        std::string _name; // 线程的名称
        pthread_t _tid;    // 线程的ID
        pid_t _pid;        // 进程的ID
        bool _joinable;    // 是否是分离的,默认不是,标记线程是否为“可连接”(joinable)状态。
        func_t _func;      // 存储线程实际执行的函数(任务)。
        TSTATUS _status;   // 记录线程的当前状态。
    };
}

#endif

4.2 线程池

《深入剖析 C++ 线程池实现》

注意:使用的都是C++ 标准库的线程,互斥锁,条件变量,建议先了解!

4.3 力扣题目

测试题目 按序打印, H2O 生成

如有错误,欢迎指正!