Linux/C++ 线程与互斥锁封装学习总结
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 线程封装核心
-
用 std::function 封装线程函数,支持多种函数类型。
-
static 入口函数作为跳板,传递 this 指针,解决 pthread 函数格式要求。
-
join/detach 模式控制线程生命周期,避免内存泄漏。
4.2 互斥锁封装核心
-
Mutex 类封装 pthread_mutex_t,负责锁的初始化、加锁、解锁、销毁。
-
禁止锁的拷贝/赋值,保证锁的唯一性。
-
LockGuard 用 RAII 风格,自动加锁/解锁,避免死锁。
-
LockGuard 中 mutex 必须用引用,避免拷贝锁。
4.3 多线程实战核心
-
共享变量 + 多线程修改 → 必须加锁,保护临界区。
-
锁释放后主动让出CPU,避免线程饥饿。
-
编译时必须链接 pthread 库(-lpthread),否则报错。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)