本文使用 C++ RAII 机制来封装互斥量、条件变量,使其自动管理互斥量、条件变量的生命周期,避免手动维护带来的资源泄露等各种问题。本文使用的是 Linux 下 Pthread 库。

互斥量

MutexLock

首先封装 mutex,下面为实现:

class MutexLock : noncopyable {
public:
    MutexLock() {
        assert(pthread_mutex_init(&mutex_, nullptr) == 0);
    }

    ~MutexLock() {
        assert(pthread_mutex_destroy(&mutex_) == 0);
    }

    /**
     * 加锁(仅供 MutexLockGuard 调用,严禁用户调用)
     */
    void lock() {
        pthread_mutex_lock(&mutex_);
    }

    /**
     * 解锁(仅供 MutexLockGuard 调用,严禁用户调用)
     */
    void unlock() {
        pthread_mutex_unlock(&mutex_);
    }

    /**
     * 获取互斥量原始指针(仅供 Condition 调用,严禁用户调用)
     * 仅供 Condition 调用
     * @return 互斥量原始指针
     */
    pthread_mutex_t* getPthreadMutexPtr() {
        return &mutex_;
    }
private:
    pthread_mutex_t mutex_{}; // 互斥量
};

现在MutexLock对象已经可以自动管理 mutex 了,它在构造函数中初始化 mutex,在析构函数中销毁 mutex。

MutexLockGuard

MutexLock 对象有一点不足的是,一般加锁和解锁是成对出现的,但是使用 MutexLock 对象的时候,需要自己手动进行加锁和解锁。这个问题比较容易解决,引入一个 MutexLockGuard 对象来管理加锁和解锁即可。实现如下:

class MutexLockGuard : noncopyable {
public:
    explicit MutexLockGuard(MutexLock &mutex) : mutex_(mutex) {
            mutex_.lock();
    }

    ~MutexLockGuard() {
        mutex_.unlock();
    }

private:
    MutexLock &mutex_;
};

MutexLockGuard 对象使用也比较简单。

{
    // mutex 为 MutexLock 对象
    MutexLockGuard lock(mutex);
    ...
}

MutexLockGuard 对象创建的时候,加锁。当 MutexLockGuard 对象离开作用域时,MutexLockGuard 对象执行析构函数,解锁。有了 MutexLockGuard 对象,就不需要手动进行加锁和解锁了。

MutexLockGuard 对象还有一个小问题:MutexLockGuard 的临时对象能不能达到我们想要的自动加锁和解锁的效果呢?

{
    // mutex 为 MutexLock 对象
    MutexLockGuard(mutex);
    // MutexLockGuard 对象已被销毁
    ...
}

答案是能自动加锁和解锁,但不能保护临界区。临时对象在创建后会立即被销毁,并不是在离开作用域的时候被销毁,所以临时对象必不能达到锁住资源的效果。我们可以定义一个宏来阻止临时对象的使用。

#define MutexLockGuard(x) error "Missing guard object name"

这样就完成了对互斥量的封装了。

条件变量

有了前面的互斥量的封装,分装条件变量就简单多了。直接看代码:

class Condition : noncopyable {
public:
    /**
     * 构造函数
     * @param mutex 互斥量
     */
    explicit Condition(MutexLock &mutex) : mutex_(mutex) {
        assert(pthread_cond_init(&cond_, nullptr) == 0);
    }

    ~Condition() {
        assert(pthread_cond_destroy(&cond_) == 0);
    }

    void wait() {
        pthread_cond_wait(&cond_, mutex_.getPthreadMutexPtr());
    }

    /**
     * 等待规定时间
     * @param second 等待的时间
     * @return 如果超时,则返回true;否则,返回false
     */
    bool waitForSecond(int second) {
        struct timespec timeout{};
        clock_getres(CLOCK_REALTIME, &timeout);
        timeout.tv_sec += second;
        return pthread_cond_timedwait(&cond_, mutex_.getPthreadMutexPtr(), &timeout) == ETIMEDOUT;
    }

    void notify() {
        pthread_cond_signal(&cond_);
    }

    void notifyAll() {
        pthread_cond_broadcast(&cond_);
    }

private:
    MutexLock &mutex_;
    pthread_cond_t cond_{};
};

因为条件变量要配合互斥量使用,需要获取互斥量的指针,所以 MutexLock 对象提供了获取互斥量指针的 getPthreadMutexPtr 成员函数。 C++ RAII机制不提倡传递裸指针,因为这样做会有很大的风险。正如《Effective C++》第三版条款 15 中说到,这个世界并不完美,很多 APIs 直接涉及资源,例如 Unix 系统 API。所以说,这是无奈之举。

其他

如果你留意的话,应该注意到前面的三个对象都继承了 noncopyable 对象,因为他们都是对象语义的,是不可拷贝的。可参考C++ noncopyable类
该实现只是简单的实现,还有很多问题没有考虑(例如,未考虑多线程的情况),完全没有达到工业强度。


chenBright
817 声望57 粉丝

学习算法、C++、linux…