防重入或并发调用(C++)


通常我们要防止同一方法在执行过程被「重入」(Re-entrance)或被并发调用,可以在方法入口做一个简单的“忙碌”检测,常见做法有两种:基于互斥锁(mutex)/尝试锁(try-lock),或基于原子标志(atomic flag)+ 小型 RAII 守卫。

1. std::mutex + std::unique_lock(try_to_lock)

#include <mutex>

class MyClass {
public:
    void foo() {
        // 尝试获取锁,如果获取失败说明已有一次 foo() 在执行,直接返回
        std::unique_lock<std::mutex> lk(foo_mtx_, std::try_to_lock);
        if (!lk) {
            // 可选:记录日志或发信号告知“已在执行中”
            return;
        }
        // —— 这里是真正的业务逻辑 ——  
        doHeavyWork();
    }

    void bar() {
        std::unique_lock<std::mutex> lk(bar_mtx_, std::try_to_lock);
        if (!lk) return;
        doOtherWork();
    }

private:
    std::mutex foo_mtx_;
    std::mutex bar_mtx_;
    void doHeavyWork();
    void doOtherWork();
};
  • 优点:跨线程安全;不用显式解锁,unique_lock 会在作用域尾自动释放。

  • 缺点:每个方法要维护一个 std::mutex 成员,稍微繁琐。

2. QMutex + QMutexLocker(AdoptLock)

如果你想全部用 Qt 类型,也可以:

#include <QMutex>
#include <QMutexLocker>

class MyClass {
public:
    void foo() {
        // tryLock() 返回 false 时表示已有执行
        if (!fooMtx_.tryLock()) return;
        // 采用 AdoptLock 模式,让 QMutexLocker 在析构时释放
        QMutexLocker locker(&fooMtx_, QMutexLocker::AdoptLock);

        doHeavyWork();
    }

private:
    QMutex fooMtx_;
    void doHeavyWork();
};

tryLock() + QMutexLocker(…, AdoptLock) 等同于上面 std::try_to_lock 的模式。

3. std::atomic_flag + 简易 RAII 守卫

如果你仅仅需要同线程下防止重入,或者执行体非常短暂,也可以考虑更轻量的原子标志法:

#include <atomic>

class MyClass {
public:
    void foo() {
        // test_and_set 返回前一个值:如果已经被 set 过(true),就直接 return
        if (busyFoo_.test_and_set(std::memory_order_acquire)) return;

        // RAII 守卫:离开作用域时自动 clear
        struct FlagGuard {
            std::atomic_flag &f;
            FlagGuard(std::atomic_flag &flag): f(flag) {}
            ~FlagGuard() { f.clear(std::memory_order_release); }
        } guard(busyFoo_);

        doHeavyWork();
    }

private:
    std::atomic_flag busyFoo_ = ATOMIC_FLAG_INIT;
    void doHeavyWork();
};
  • 优点:无锁开销(其实底层也是原子操作),代码简洁。

  • 缺点:只能防同线程或跨线程对同方法的并发调用,不适合复杂锁依赖。

4. 减少重复样板:宏或模版封装

如果方法很多,不想给每个都写一堆相似代码,可以把“尝试加锁 + RAII 解锁”封装成一个小宏或模版,例如:

// nonreentrant.h
#pragma once
#include <atomic>
#define NON_REENTRANT(method)                          \
    static std::atomic_flag _busy_##method = ATOMIC_FLAG_INIT; \
    if (_busy_##method.test_and_set(std::memory_order_acquire)) \
        return;                                         \
    auto _guard_##method = [&]() {                     \
        struct G { ~G() { _busy_##method.clear(std::memory_order_release); } } g; \
    }();                                                

// MyClass.cpp
#include "nonreentrant.h"
void MyClass::foo() {
    NON_REENTRANT(foo);
    // … 实际逻辑 …
}
void MyClass::bar() {
    NON_REENTRANT(bar);
    // … 实际逻辑 …
}

这样每个方法顶部只要一行 NON_REENTRANT(方法名),就能自动完成标志检测和清理。

模板

// NonReentrantGuard.h
#pragma once
#include <atomic>

/**
 * @brief 非重入守卫:同一 Tag 只允许一个实例“激活”其所在作用域
 *
 * Tag 用来区分各个不同的方法/调用点。
 */
template<typename Tag>
class NonReentrantGuard {
public:
    NonReentrantGuard()
      : engaged_(!flag_.test_and_set(std::memory_order_acquire))
    {}

    ~NonReentrantGuard() {
        if (engaged_) {
            flag_.clear(std::memory_order_release);
        }
    }

    /// 转换为 bool:为 true 时表示“首次进入”,可以继续执行;为 false 则应直接返回
    explicit operator bool() const noexcept {
        return engaged_;
    }

private:
    static std::atomic_flag flag_;
    bool engaged_;
};

// 必须在某个翻译单元里定义静态成员
template<typename Tag>
std::atomic_flag NonReentrantGuard<Tag>::flag_ = ATOMIC_FLAG_INIT;
// MyClass.h
#include "NonReentrantGuard.h"

class MyClass {
public:
    void foo();
    void bar();

private:
    void doHeavyWork();
    void doOtherWork();
};

小结

  • 跨线程/性能可控:std::mutex + std::try_to_lock 或者 Qt 的 QMutex::tryLock()

  • 轻量级同线程:std::atomic_flag + 简易 RAII 守卫

  • 大批方法:可用宏或模板封装上面逻辑,减少重复代码

选用时只要注意解锁一定要在所有退出路径(包括异常)都会执行 —— RAII 守卫或 unique_lock/QMutexLocker 都能帮你做到这一点。这样就能保证「正在运行」的那次调用结束前,任何重复触发都会被忽略。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值