锁-Linux与C++接口介绍

这一篇,我们主要来总结一下Linux与C++下线程锁的接口!

互斥量接口:

初始化互斥量

初始化互斥量有两种方法

方法1,静态分配:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法2,动态分配

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict
attr);
参数:
mutex:要初始化的互斥量
attr:NULL

销毁:

注意:

使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁

不要销毁一个已经加锁的互斥量

已经销毁的互斥量,要确保后面不会有线程再尝试加锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功

发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁

条件变量接口:

初始化:

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);
参数:
cond:要初始化的条件变量
attr:NULL

销毁:

int pthread_cond_destroy(pthread_cond_t *cond)

等待条件满足

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量

唤醒等待:

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond)

成功返回0

上面是Linux的接口,现在,我们顺便来引出来

C++的接口:

实际上,Linux与C++的接口都是差不多的,只不过是C++封装成类罢了,所以我们了解完Linux的锁后,类似地类比到C++那也是能看懂的!

std::mutex

std::mutex最基础的互斥量,支持「加锁-解锁」的基本操作,不可递归加锁(同一线程重复加锁会导致死锁)。

接口函数    功能说明
lock()阻塞线程,直到获取互斥量所有权(若已被其他线程锁定,则当前线程等待)。
unlock()释放互斥量所有权(必须由当前持有锁的线程调用,否则行为未定义)。
try_lock()尝试非阻塞获取锁:成功返回,失败(锁已被占用)返回,不阻塞。

C++中出了一个智能锁:它避免手动解锁(通常我们写C++时推荐使用)

std::mutex 需手动调用 lock() 和 unlock() ,容易因异常或忘记解锁导致死锁。C++提供RAII风格的智能锁,自动管理锁的生命周期(构造时加锁,析构时解锁)。

std::lock_guard(基础智能锁)

#include <mutex>
#include <thread>

int shared_data = 0;

class LogMutex
{
public:
    void lock()
    {
        std::cout<<"lock被调用"<<std::endl;
        mtx.lock();
    }
    void unlock()
    {
        std::cout<<"unlock被调用"<<std::endl;
        mtx.unlock();
    }
private:
    std::mutex mtx;

};

int main()
{
    LogMutex log_mtx;
    std::lock_guard<LogMutex> lock(log_mtx);
    std::cout<<"lock_guard已经创建"<<std::endl;
    std::cout<<"执行临界区代码"<<std::endl;
    shared_data++;
    std::cout<<"作用域结束"<<std::endl;
    shared_data++;
    return 0;
}

当出现异常时:

int main()
{
    LogMutex log_mtx;
    try{
    {
        std::lock_guard<LogMutex> lock(log_mtx);
        std::cout << "lock_guard已经创建" << std::endl;
        std::cout << "执行临界区代码" << std::endl;
        throw std::runtime_error("故意抛出异常");
        shared_data++;
        std::cout << "作用域结束" << std::endl;
        shared_data++;
    }
    }catch(const std::exception&e)
    {
        std::cout<<"捕捉到异常:"<<e.what()<<std::endl;
    }
    return 0;
}

注意:

如果是这样写的话,这次 unlock 没被调用,是因为C++异常处理的“栈展开”行为,在程序 terminate 时可能被跳过了——但这是环境/编译器的“极端情况”,我们可以通过捕获异常来确保栈展开(让 lock_guard 析构)。

问题本质:

当你直接抛出未捕获的异常时,程序会调用 std::terminate 终止运行。而部分编译器/环境下, std::terminate 会直接终止进程,不会执行“栈展开”(即不调用局部对象的析构函数),所以 lock_guard 的析构(及 unlock )没机会执行。

所以我们才使用到了上面的捕捉异常,强制触发栈展开!

同时为什么最好要给lock_guard加局部作用域呢?

因为局部作用域结束,即使抛异常也会调到析构函数的!

std::unique_lock(灵活智能锁)

- 用途:比 lock_guard 更灵活,支持手动加锁/解锁、延迟加锁、条件变量配合等场景。

- 构造时可指定 std::defer_lock :延迟加锁(需后续手动调用 lock() )。
-  lock() / unlock() :手动控制锁的获取与释放。
-  try_lock() :非阻塞尝试加锁。
-  owns_lock() :判断当前智能锁是否持有互斥量所有权。

std::mutex mtx;
int shared_data=0;
int main()
{
    std::unique_lock<std::mutex> lock(mtx,std::defer_lock);
    lock.lock();
    shared_data++;
    lock.unlock();
    return 0;
}

std::recursive_mutex(递归互斥量)

- 特点:允许同一线程多次加锁(需对应次数的解锁),避免递归函数中的死锁。
- 接口:与 std::mutex 一致( lock() / unlock() / try_lock() ),可搭配 lock_guard 或 unique_lock 使用。
- 注意:仅在「递归调用必须访问同一临界区」时使用,避免滥用(可能隐藏设计问题)。

std::timed_mutex(超时互斥量)

- 特点:在 std::mutex 基础上,支持「超时等待锁」(避免无限阻塞)。
- 新增接口:
-  try_lock_for(std::chrono::duration) :等待指定时间,超时返回 false 。
-  try_lock_until(std::chrono::time_point) :等待到指定时间点,超时返回 false 。

注意:

1. 避免死锁:
- 同一线程对 std::mutex 不可重复 lock() (改用 recursive_mutex )。
- 多线程加锁时,严格遵守「相同的加锁顺序」(如线程1先锁A再锁B,线程2也必须先A后B)。
2. 智能锁优先:除非有特殊需求(如手动控制锁的生命周期),否则优先使用 lock_guard (简单场景)或 unique_lock (复杂场景),避免手动调用 unlock() 。
3. 互斥量不可拷贝/移动:所有互斥量类( mutex / recursive_mutex 等)均禁用拷贝和移动构造,确保锁的唯一性。

C++线程接口:

在C++11及以后标准中,线程操作的核心接口是 <thread>  头文件中的 std::thread  类,它提供了创建、管理线程的基础功能

接口函数    功能描述
std::thread t(func)构造函数:创建线程对象 t,并执行函数func ( 可带参数,需在构造时传入)。
t.join()阻塞当前线程,等待线程 t 执行完毕后再继续(避免主线程先退出导致子线程被终止)。
t.detach()将线程 t 与主线程“分离”,线程 t成为后台线程,其资源由系统自动回收(分离后不可再join )。
t.joinable()判断线程  t是否可被 join(未被join /detach、未移动构造的线程返回true )。
std::thread::hardware_concurrency静态成员函数:返回当前设备的逻辑核心数(如4核8线程CPU返回8),用于指导线程数量设计。
 
std::this_thread::get_id()获取当前线程的唯一标识(返回 std::thread::id  类型,可直接打印);
std::this_thread::sleep_for(duration)让当前线程休眠指定时间(如 chrono::seconds(1)  表示1秒);
std::this_thread::yield() :

当前线程主动“让出”CPU,允许其他线程优先执行(适用于减少忙等消耗)。

ps:Linux中是sched_yield()

现在,我们来用代码练习一下:

无参函数:

void mythread()
{
    std::cout<<"hello mythread"<<std::endl;
}

int main()
{
    std::thread t(mythread);
    if(t.joinable())
    {
        t.join();
    }
    std::thread::id th_id=std::this_thread::get_id();
    std::cout<<th_id<<std::endl;
    
    std::cout<<std::this_thread::get_id()<<std::endl;

    return 0;
}

有参函数:(值传递/引用传递)

- 值传递:默认情况下,线程会拷贝参数(即使函数形参是引用);
- 引用传递:需用  std::ref()  包裹实参,强制传递引用(注意确保实参生命周期长于线程)。

void mythread(int &a,int b)
{
    a+=b;
    std::cout<<"mthread: a="<<a<<std::endl;

}

int main()
{
    int num=10;
    std::thread t(mythread,num,5);
    if(t.joinable())
    {
        t.join();
    }
    std::cout<<"main thread:num"<<num<<std::endl;
    return 0;
}


void mythread(int &a,int b)
{
    a+=b;
    std::cout<<"mthread: a="<<a<<std::endl;

}

int main()
{
    int num=10;
    std::thread t(mythread,std::ref(num),5);
    if(t.joinable())
    {
        t.join();
    }
    std::cout<<"main thread:num"<<num<<std::endl;
    return 0;
}

上面我们可以看到,当我们传递引用的时候,需要用std::ref(num),否则num会会被拷贝,导致出现报错

分离线程(后台线程)

void Background()
{
    for (int i = 0; i < 5; i++)
    {
        std::this_thread::sleep_for(std::chrono::seconds(1)); // 休眠1s
        std::cout << "Background thread running" << std::endl;
    }
}

int main()
{
    std::thread t(Background);
    t.detach(); // 分离线程,主线程无需等待

    std::this_thread::sleep_for(std::chrono::seconds(2));

    std::cout << "main thread quit" << std::endl;

    return 0;
}

注意:

1. 线程对象销毁前未  join / detach 
若线程对象(如  std::thread t )在生命周期结束时仍处于“可 join”状态(未调用  join  或  detach ),会触发  std::terminate()  终止程序。解决:必须在对象销毁前调用  join  或  detach 。
2. 分离线程访问局部变量
分离线程( detach  后)的生命周期可能长于主线程的局部变量,若线程内访问该变量,会导致野指针/悬垂引用。解决:仅让分离线程访问全局变量、静态变量或动态分配的内存(需确保内存不提前释放)。
3. 重复  join  同一线程
一个线程只能被  join  一次,重复  join  会触发未定义行为。解决:每次  join  前用  t.joinable()  检查。

好了,关于接口介绍暂时到此结束了。

需要了解锁的具体原理,可移到接下来我要更新的文章中,这篇文章也是基于下面的文章写的!

希望大家一起进步!

最后,到了本次鸡汤环节:

不要等待机会,而是创造机会!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值