Linux/C++ 线程与互斥锁封装学习总结(新手入门,超详细)

最近跟着学习了 Linux 下 C++ 线程和互斥锁的封装,从 0 到 1 实现了线程类、互斥锁类,还解决了多线程抢票的线程安全问题,过程中踩了不少坑,也搞懂了很多之前模糊的知识点。现在整理成博客,方便后续复习回顾,也希望能帮到和我一样入门的小伙伴~
核心学习内容:线程类封装 → 互斥锁+RAII自动锁封装 → 多线程抢票案例(解决线程安全+线程饥饿问题),全程基于 Linux pthread 库,纯新手友好,无复杂冗余内容。
下面的文字有些是我自己总结的知识点,可能有些乱,可以直接看代码就,代码里面解释的挺详细的。

一、线程类(Thread)封装

目标:将 Linux 原生 pthread 线程库,封装成简洁、易用的 C++ 类,屏蔽底层复杂调用,同时支持函数绑定、线程命名、join/detach 模式。
核心知识点回顾

  • 线程函数类型定义:用 using threadfunc_t = std::function<void()> 定义无参、无返回值的函数类型,适配线程执行的任意函数(普通函数、lambda、bind 绑定的函数)。

  • 静态入口函数 run():pthread 要求线程入口函数必须是 void*()(void) 格式(C 风格函数),而 C++ 普通成员函数自带隐藏 this 指针,格式不匹配,因此必须用 static 静态函数作为“跳板”,接收 this 指针后,再调用真正的线程函数。

  • this 指针传递:在 Start() 调用 pthread_create 时,将 this 作为参数传入,在 run() 中通过 Thread self = static_cast<Thread>(obj) 强转,拿到当前线程对象,从而调用对象内的成员函数和变量(核心巧妙点)。

  • join/detach 控制:用 bool 变量 _joined 标记线程模式,默认 join 模式(主线程等待子线程结束),可通过 EnableDetach() 切换为 detach 模式(线程自动回收,无需主线程等待)。

#pragma once
#include <iostream>
#include <string>
// 函数封装
#include <functional>
#include <pthread.h>
#include <cstdint>

namespace ThreadModule
{
    // 1.原子技术器,自动生成线程名,Thread-0,;;;
    std::uint32_t cnt = 0;
    // 2.线程函数类型:无参数,返回值的函数,,,它是一个 “能装任何无参、无返回值函数” 的盒子。
    // using,就是给类型起别名,函数包装器,就是一个函数盒子
    using threadfunc_t = std::function<void()>;
    // 3.线程状态:新建,运行,停止,这里采用枚举的方式
    enum class TSTATUS
    {
        THREAD_NEW,
        THREAD_RUNNING,
        THREAD_STOP
    };

    // 4.线程类,线程中有什么,线程名,id,状态,是否jionable,要执行的任务
    class Thread
    {
    private:
        void SetName()
        {
            //这里后期需要枷锁保护
            _name = "Thread---" + std::to_string(cnt++);
        }
        // 重点,为什么要加static,因为在类中每个函数都有一个隐藏指针this
        static void *run(void *arg)
        {
            // 1. 拿到线程对象自己,把传进来的参数,强转为当前对象
            // 但是线程创建的时候他只认void* (void*)类型,所以用static就不会有对象指针了
            // 这里为什么要强转,void*是任何类型的指针,这里必须强转为具体类型、

            // c的风格强转,不安全Thread *self = (Thread *)arg;
            Thread *self = static_cast<Thread *>(arg);
            pthread_setname_np(pthread_self(), self->_name.c_str()); // 设置线程名称
            // 设置线程状态,
            self->_status = TSTATUS::THREAD_RUNNING;
            if (!self->_joined) // 如果线程不被等待,也就是为false,那就分离线程
            {
                pthread_detach(pthread_self());
            }
            // 2. 执行你传进来的函数:hello1()
            self->_func();
            return nullptr;
        }
        void EnableDetach()
        {
            if (_status == TSTATUS::THREAD_NEW)
                // 标记后,不需要被等待了,所以为false
                _joined = false;
        }
        void EnableJoined()
        {
            if (_status == TSTATUS::THREAD_NEW)
                // 已经被等待成功
                _joined = true;
        }

    public:
        Thread(threadfunc_t func)
            : _status(TSTATUS::THREAD_NEW),
              _joined(true), // 一般被新创建的线程都是需要join的
              _func(func)
        {
            // 自动起名
            SetName();
        }
        // 启动线程
        bool Start()
        {
            if (_status == TSTATUS::THREAD_RUNNING)
                return true; // 已经启动
                             // 首先创建线程
            int n = pthread_create(&_id, nullptr, run, this);
            // 成功就返回0,否则返回错误码
            return n == 0;
        }
        // 结束线程
        bool Join()
        {
            if (_joined)
            {
                pthread_join(_id, nullptr);
                return true;
            }
            return false;
        }
        ~Thread() {}

    private:
        std::string _name;
        pthread_t _id;
        TSTATUS _status;
        bool _joined; // 是否可等待
        threadfunc_t _func;
    };
}

  • 为什么 run 必须是 static?→ pthread 只认识 C 风格函数,普通成员函数自带 this 指针,格式不匹配;static 函数无 this 指针,符合 void*()(void) 要求。

  • static_cast<Thread*>(obj) 和 (Thread*)obj 的区别?→ 两者都是强转,前者是 C++ 安全写法,会做类型检查;后者是 C 语言暴力强转,不安全,C++ 优先用 static_cast。

  • std::bind 的作用?→ 将有参函数绑定参数后,转换成无参函数,适配 threadfunc_t 类型(比如 bind(printNum, 100),把有参的 printNum 变成无参可调用对象)。

二、互斥锁(Mutex)+ RAII 自动锁(LockGuard)封装

目标:解决多线程操作共享变量的线程安全问题(比如抢票时的超卖、负数票),封装 pthread 互斥锁,同时用 RAII 风格实现自动加锁/解锁,避免忘记解锁导致死锁。

2.1 核心知识点回顾

  • 线程安全问题原因:多线程并发操作共享变量时,临界区代码(操作共享变量的代码)可能被多个线程同时执行,比如 ticket-- 不是原子操作,会导致数据混乱。

  • 互斥锁作用:保证临界区代码“原子执行”,同一时间只有一个线程能进入临界区,其他线程阻塞等待,直到锁被释放。

  • RAII 自动锁:LockGuard 类封装 Mutex,构造函数自动调用 Lock() 加锁,析构函数自动调用 Unlock() 解锁(离开作用域自动析构),彻底避免忘记解锁导致的死锁。

  • 禁止锁拷贝/赋值:互斥锁是唯一的,系统不允许拷贝,拷贝会导致逻辑混乱、程序崩溃,因此用 Mutex(const Mutex &) = delete; 禁止拷贝和赋值。

  • LockGuard 中 mutex 必须用引用:如果不用引用,会尝试拷贝传入的锁,而锁已禁止拷贝,会编译报错;用引用可以指向同一把锁,保证所有线程操作的是同一把锁。

#pragma once
#include <unistd.h>
#include <iostream>
#include <string>
#include <pthread.h>
namespace LockModule
{
    class Mutex
    {
    public:
        ////这里一定要禁止考拷贝锁,复制锁
        /*
        为什么?锁是唯一的,不能复制
系统不允许复制锁
复制锁会导致程序崩溃、逻辑混乱*/
        Mutex(const Mutex &) = delete;
        Mutex &operator=(const Mutex &) = delete;
        Mutex()
        {
            // 对锁进行初始化
            pthread_mutex_init(&_mutex, nullptr);
        }
        // 加锁
        void Lock()
        {
            pthread_mutex_lock(&_mutex);
        }

        // 解锁
        void Unlock()
        {
            pthread_mutex_unlock(&_mutex);
        }

        // 销毁锁
        ~Mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }

    private:
        // 先定义一个锁
        pthread_mutex_t _mutex;
    };
    class LockGuard
    {
    public:
        LockGuard(Mutex &mutex) : _mutex(mutex)
        {
            _mutex.Lock();
        }
        // 析构锁
        ~LockGuard()
        {
            _mutex.Unlock();
        }

    private:
        /*
        LockGuard 里如果不用引用,成员变量会拷贝外部传入的锁,但锁禁止拷贝,
        而且锁必须全局唯一,所以必须用引用,指向同一把锁,不拷贝、不新建。

        */
        Mutex &_mutex;
    };
}

2.3 关键疑问解答(新手必看)

  • 为什么禁止锁的拷贝和赋值?→ 锁是唯一的,拷贝会产生多把锁,无法实现线程同步;系统底层也不允许拷贝 pthread_mutex_t 对象,强行拷贝会导致程序崩溃。

  • LockGuard 为什么用引用?→ 不用引用会拷贝锁(编译报错),用引用可以直接操作外部传入的那把唯一锁,保证所有线程共用一把锁,实现互斥。

  • RAII 有什么好处?→ 不用手动调用 Lock() 和 Unlock(),即使代码中出现异常,析构函数也会自动解锁,避免死锁,代码更安全、简洁。

三、多线程抢票案例(实战检验)

用封装好的 Thread 和 Mutex/LockGuard,实现多线程抢票,解决线程安全问题,同时解决“部分线程抢不到锁”的饥饿问题。

3.1 核心需求

4 个线程同时抢 1000 张票,保证票不超卖、不出现负数,所有线程都能公平抢到票,最后正常退出。

#include <unistd.h>
#include <iostream>
#include "Mutex.hpp"
#include <pthread.h>
int ticket = 100;
LockModule::Mutex mutex; // 定义一把锁
void *route(void *arg)
{
    char *id = static_cast<char *>(arg);
    while (1)
    {
        // 加锁
        LockModule::LockGuard lock(mutex);
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s你抢到票了!!:还有几张票:%d\n", id, ticket);
            ticket--;
        }
        else
        {
            break;
        }
    }
    return nullptr;
}

int main()
{
    // 创建线程准备抢票
    pthread_t p1, p2, p3, p4;
    pthread_create(&p1, nullptr, route, (void *)"p1");
    pthread_create(&p2, nullptr, route, (void *)"p2");
    pthread_create(&p3, nullptr, route, (void *)"p3");
    pthread_create(&p4, nullptr, route, (void *)"p4");

    pthread_join(p1, nullptr);
    pthread_join(p2, nullptr);
    pthread_join(p3, nullptr);
    pthread_join(p4, nullptr);
    return 0;
}

3.3 关键问题与解决方案

  • 问题1:票出现负数、超卖 → 未加锁或锁未正确使用,解决方案:将操作 ticket 的临界区用 LockGuard 包裹,保证原子执行。

  • 问题2:只有部分线程能抢到票(比如2个线程霸占锁)→ 锁释放后,当前线程立即再次抢锁,其他线程抢不过,解决方案:在锁释放后(while循环内、锁作用域外)调用 sched_yield() 或 usleep(1000),主动让出CPU,给其他线程抢锁机会。

  • 问题3:编译报错“缺少显式类型”→ 类名写错、禁止拷贝的代码写在类外面,解决方案:检查类名拼写,将 =delete 代码放在 Mutex 类内部。
    3.4 编译与运行
    编译(必须加 -lpthread 链接pthread库)

g++ main.cc -o ticket -lpthread

#运行

./ticket

运行结果:4个线程交替抢票,票从1000递减到1,无负数、无超卖,所有线程都能参与抢票,最后主线程正常退出。

四、整体总结(核心必背)

4.1 线程封装核心

  1. 用 std::function 封装线程函数,支持多种函数类型。

  2. static 入口函数作为跳板,传递 this 指针,解决 pthread 函数格式要求。

  3. join/detach 模式控制线程生命周期,避免内存泄漏。

4.2 互斥锁封装核心

  1. Mutex 类封装 pthread_mutex_t,负责锁的初始化、加锁、解锁、销毁。

  2. 禁止锁的拷贝/赋值,保证锁的唯一性。

  3. LockGuard 用 RAII 风格,自动加锁/解锁,避免死锁。

  4. LockGuard 中 mutex 必须用引用,避免拷贝锁。

4.3 多线程实战核心

  1. 共享变量 + 多线程修改 → 必须加锁,保护临界区。

  2. 锁释放后主动让出CPU,避免线程饥饿。

  3. 编译时必须链接 pthread 库(-lpthread),否则报错。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐