RAII(Resource Acquisition Is Initialization)是一种 C++ 中的编程技术,它利用了对象的生命周期和析构函数的特性来管理资源的获取和释放。在 RAII 中,资源的获取和释放都与对象的生命周期相关联,资源在对象构造时被获取,而在对象析构时被释放,从而确保资源的正确管理,避免资源泄漏和内存泄漏等问题。
RAII 的基本原则是:资源的获取应该在对象的构造函数中进行,而资源的释放应该在对象的析构函数中进行。这样,只要对象存在,资源就会得到正确的管理,无需手动管理资源的获取和释放。RAII 还可以结合异常安全性和自动化资源管理的优势,使得代码更加健壮和可靠。本文章中对下面的线程同步工具进行了封装,并通过RAII方便管理资源。
下面是对线程同步工具的说明和封装:
-
互斥锁(Mutex):
- 互斥锁用于保护临界区,确保同一时刻只有一个线程可以访问临界区的资源,其他线程必须等待锁被释放才能继续执行。
- 互斥锁提供了两个主要的操作:锁定(lock)和解锁(unlock)。只有持有锁的线程才能访问被保护的资源,其他线程必须等待当前线程释放锁。
- 互斥锁通常使用操作系统提供的原语来实现,因此在资源竞争较为频繁的情况下,可能会引入较高的开销。
class Mutex : public Uncopyable {
public:
Mutex() {
if(pthread_mutex_init(&mutex_, nullptr)) {
throw std::runtime_error("pthread_mutex_init error");
}
}
~Mutex() {
pthread_mutex_destroy(&mutex_);
}
void lock() {
if(pthread_mutex_lock(&mutex_)) {
throw std::runtime_error("pthread_mutex_lock error");
}
}
void unlock() {
if(pthread_mutex_unlock(&mutex_)) {
throw std::runtime_error("pthread_mutex_unlock error");
}
}
pthread_mutex_t& getMutex() { return mutex_; }
private:
pthread_mutex_t mutex_;
};
-
信号量(Semaphore):
- 信号量是一个计数器,用于控制对共享资源的访问。它允许多个线程同时访问共享资源,但是需要控制同时访问的线程数量。
- 信号量可以分为二进制信号量和计数信号量。二进制信号量的计数器只有0和1两个值,用于实现互斥锁的功能;而计数信号量的计数器可以是任意正整数,用于控制同时访问资源的线程数量。
- 信号量提供了等待(wait)和释放(post)两个主要的操作。等待操作会使计数器减一,如果计数器为负,则线程被阻塞;释放操作会使计数器加一,唤醒等待的线程。
- 信号量通常用于进程间通信和线程同步的场景,是一种比互斥锁更加灵活的同步机制。
class Semaphore : public Uncopyable {
public:
Semaphore(u_int32_t count = 0) {
if(sem_init(&semaphore_, 0, count)) {
throw std::runtime_error("sem_init error");
}
}
~Semaphore() {
sem_destroy(&semaphore_);
}
void wait() {
if(sem_wait(&semaphore_)) {
throw std::runtime_error("sem_wait error");
}
}
void post() {
if(sem_post(&semaphore_)) {
throw std::runtime_error("sem_post error");
}
}
bool trywait() {
return (sem_trywait(&semaphore_)== 0);
}
bool timewait(const std::chrono::milliseconds& timeout) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += timeout.count() / 1000;
ts.tv_nsec += (timeout.count() % 1000) * 1000000;
int result;
do {
result = sem_timedwait(&semaphore_, &ts);
} while(result == -1 && errno == EINTR);
if(result == -1) {
if(errno == ETIMEDOUT) {
return false;
} else {
throw std::runtime_error("sem_timeout error");
}
}
return true;
}
private:
sem_t semaphore_;
};
-
条件变量(Condition Variable):
- 条件变量用于线程间的通信和同步,它允许线程在满足特定条件时进行等待,直到另一个线程唤醒它。
- 条件变量通常与互斥锁一起使用,用于防止竞态条件(Race Condition)的发生。等待线程会先获取互斥锁,然后检查条件是否满足,如果条件不满足,则进入等待状态并释放锁;唤醒线程会在改变条件后通知等待的线程,并释放互斥锁。
- 条件变量提供了等待(wait)、通知(notify)和广播(broadcast)等操作。等待操作会使线程进入等待状态,直到收到通知或广播;通知操作用于唤醒一个等待的线程;广播操作用于唤醒所有等待的线程。
class Cond : public Uncopyable {
public:
Cond(Mutex& mutex):
mutex_(mutex) {
if(pthread_cond_init(&cond_, nullptr)) {
throw std::runtime_error("pthread_cond_init error");
}
}
~Cond() {
pthread_cond_destroy(&cond_);
}
void wait() {
mutex_.lock();
if(pthread_cond_wait(&cond_, &mutex_.getMutex())) {
mutex_.unlock();
throw std::runtime_error("pthread_cond_wait error");
}
mutex_.unlock();
}
bool timedwait(const std::chrono::milliseconds& timeout) {
mutex_.lock();
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += timeout.count() / 1000;
ts.tv_nsec += (timeout.count() % 1000) * 1000000;
int result = pthread_cond_timedwait(&cond_, &mutex_.getMutex(),&ts);
mutex_.unlock();
if(result == ETIMEDOUT) {
return false;
} else if(result != 0) {
throw std::runtime_error("pthread_cond_timewait error");
} else {
return true;
}
}
void signal() {
if(pthread_cond_signal(&cond_)) {
throw std::runtime_error("pthread_cond_signal error");
}
}
void broadcast() {
if(pthread_cond_broadcast(&cond_)) {
throw std::runtime_error("pthread_cond_broadcast error");
}
}
private:
pthread_cond_t cond_;
Mutex& mutex_;
};
-
自旋锁(Spin Lock):
- 自旋锁是一种基于忙等待的锁,它不会使线程进入阻塞状态,而是在锁被释放之前一直尝试获取锁,这样会消耗 CPU 时间。
- 自旋锁适用于临界区很小、锁的持有时间很短的情况,因为长时间的自旋可能会降低系统的性能。
- 自旋锁通常使用原子操作来实现,因此在多核处理器上可以保证线程安全。
class SpinLock : public Uncopyable {
public:
SpinLock() {
if(pthread_spin_init(&spinlock_, 0)) {
throw std::runtime_error("pthread_spinlock_init error");
}
}
~SpinLock() {
pthread_spin_destroy(&spinlock_);
}
void lock() {
if(pthread_spin_lock(&spinlock_)) {
throw std::runtime_error("pthread_spin_lock error");
}
}
void unlock() {
if(pthread_spin_unlock(&spinlock_)) {
throw std::runtime_error("pthread_spin_unlock error");
}
}
private:
pthread_spinlock_t spinlock_;
};
-
原子锁(Atomic Lock):
- 原子锁是一种特殊的自旋锁,它使用原子操作来实现临界区的访问,从而保证了线程安全性。
- 原子锁通常只有两种状态:锁定和解锁,因此它比一般的自旋锁更加简单,但也更加高效。
- 原子锁通常用于对共享资源进行简单的读写操作,例如增加或减少计数器的值等。
class AtomicLock : public Uncopyable {
public:
AtomicLock() {
atomic_.clear();
}
~AtomicLock() {
// 不做处理
}
void lock() {
atomic_.test_and_set(std::memory_order_acquire);
}
void unlock() {
atomic_.clear(std::memory_order_release);
}
private:
std::atomic_flag atomic_;
};
-
读写锁(Read-Write Lock):
- 读写锁是一种特殊的锁机制,用于控制对共享资源的读取和写入操作。
- 读取操作可以并发执行,多个线程可以同时获取读取锁并进行读取操作,但写入操作必须独占执行,一个线程获取写入锁后其他线程无法获取读取锁或写入锁。
- 读写锁适用于读取操作频繁而写入操作较少的场景,可以提高并发读取性能。
class RWLock : public Uncopyable {
public:
RWLock() {
if(pthread_rwlock_init(&rwmutex_, nullptr)) {
throw std::runtime_error("pthread_rwlock_init error");
}
}
~RWLock() {
pthread_rwlock_destroy(&rwmutex_);
}
void readLock() {
if(pthread_rwlock_rdlock(&rwmutex_)) {
throw std::runtime_error("pthread_rwlock_rdlock error");
}
}
void writeLock() {
if(pthread_rwlock_wrlock(&rwmutex_)) {
throw std::runtime_error("pthread_rwlock_wrlock error");
}
}
void unlock() {
if(pthread_rwlock_unlock(&rwmutex_)) {
throw std::runtime_error("pthread_rwlock_unlock error");
}
}
private:
pthread_rwlock_t rwmutex_;
};
现在已经完成对锁的封装,下面实现RAII。考虑到除了读写锁外都只包含lock上锁函数和unlock解锁函数外,使用模板来实现RAII机制。
template<class LockType>
class LockGuard {
public:
LockGuard(LockType& locktype):
locktype_(locktype) {
locktype_.lock();
is_locked_ = true;
}
~LockGuard() {
unlock();
}
void lock() {
if(!is_locked_) {
locktype_.lock();
is_locked_ = true;
}
}
void unlock() {
if(is_locked_) {
locktype_.unlock();
is_locked_ = false;
}
}
private:
LockType& locktype_;
bool is_locked_ = false;
};
如上代码所示,is_locked变量是为了避免直接去尝试加锁或解锁。在构造函数中进行加锁,在析构函数中解锁。在使用时,需要在类中声明。假设要在Mutex中使用,如下所示:
class Mutex{
...
typedef LockGuard<Mutex> Lock;
...
};
// 调用
Mutex::Lock lock(mutex);
Mutex::Lock时typedef LockGuard<Mutex>,根据Mutex实例化模板,也就是用Mutex初始化出来一个新的类。lock是这个新类创建的对象,该对象在构造函数中完成加锁,在析构时解锁。
下面是对读写锁的封装,和上面类似,只是调用的加锁函数有所不同。
template <class LockType>
class ReadLockGuard {
public:
ReadLockGuard(LockType& locktype):
locktype_(locktype) {
locktype_.lock();
is_locked_ = true;
}
~ReadLockGuard() {
unlock();
}
void lock() {
if(!is_locked_) {
locktype_.readLock();
is_locked_ = true;
}
}
void unlock() {
if(is_locked_) {
locktype_.unlock();
is_locked_ = false;
}
}
private:
LockType& locktype_;
bool is_locked_ = false;
};
template <class LockType>
class WriteLockGuard {
public:
WriteLockGuard(LockType& locktype):
locktype_(locktype) {
locktype_.lock();
is_locked_ = true;
}
~WriteLockGuard() {
unlock();
}
void lock() {
if(!is_locked_) {
locktype_.writeLock();
is_locked_ = true;
}
}
void unlock() {
if(is_locked_) {
locktype_.unlock();
is_locked_ = false;
}
}
private:
LockType& locktype_;
bool is_locked_ = false;
};
最后给出测试函数:
Semaphore sem(1);
void printSemaphore(size_t i) {
std::cout << "Thread " << i << " access sem" << std::endl;
// sem.wait();
int ret = sem.timewait(std::chrono::milliseconds(1000));
std::cout << "ret = " << ret << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
sem.post();
std::cout << "Thread " << i << " get sem" << std::endl;
}
void testSemaphore() {
std::vector<std::thread> threads;
threads.reserve(4);
for(size_t i = 0; i < 4; ++i) {
threads.emplace_back(std::bind(printSemaphore, i));
}
for(auto& thread : threads) {
thread.join();
}
}
Mutex mutex;
void printMutex(size_t i) {
std::cout << "Thread " << i << " access mutex" << std::endl;
Mutex::Lock lock(mutex);
std::cout << "Thread " << i << " get mutex" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "Thread " << i << " release mutex" << std::endl;
}
void testMutex() {
std::vector<std::thread> threads;
threads.reserve(4);
for(size_t i = 0; i < 4; ++i) {
threads.emplace_back(std::bind(printMutex, i));
}
for(auto& thread : threads) {
thread.join();
}
}
void printSpinLock(size_t i) {
std::cout << "Thread " << i << " access spinlock" << std::endl;
// SpinLock::Lock lock();
AtomicLock::Lock lock();
std::cout << "Thread " << i << " get spinlock" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "Thread " << i << " release spinlock" << std::endl;
}
void testSpinLock() {
std::vector<std::thread> threads;
threads.reserve(4);
for(size_t i = 0; i < 4; ++i) {
threads.emplace_back(std::bind(printSpinLock, i));
}
for(auto& thread : threads) {
thread.join();
}
}
Mutex mutex_cond;
Cond cond(mutex_cond);
void printCond(size_t i) {
std::cout << "Thread " << i << " begin" << std::endl;
cond.wait();
std::cout << "Thread " << i << " end" << std::endl;
}
void testCond() {
std::vector<std::thread> threads;
threads.reserve(4);
for(size_t i = 0; i < 4; ++i) {
threads.emplace_back(std::bind(printCond, i));
}
std::this_thread::sleep_for(std::chrono::seconds(2));
cond.broadcast();
// for(int i = 0; i < 4; ++i) {
// std::this_thread::sleep_for(std::chrono::seconds(1));
// cond.signal();
// }
for(auto& thread : threads) {
thread.join();
}
}
void printAtomic(size_t i) {
std::cout << "Thread " << i << " begin" << std::endl;
AtomicLock::Lock lock();
std::cout << "Thread " << i << " end" << std::endl;
}
void testAtomic() {
std::vector<std::thread> threads;
threads.reserve(4);
for(size_t i = 0; i < 4; ++i) {
threads.emplace_back(std::bind(printAtomic, i));
}
std::this_thread::sleep_for(std::chrono::seconds(2));
for(auto& thread : threads) {
thread.join();
}
}
void printRWLock(size_t i) {
std::cout << "Thread " << i << " begin" << std::endl;
// RWLock::ReadLock lock();
RWLock::WriteLock lock();
std::cout << "Thread " << i << " end" << std::endl;
}
void testRWLock() {
std::vector<std::thread> threads;
threads.reserve(4);
for(size_t i = 0; i < 4; ++i) {
threads.emplace_back(std::bind(printRWLock, i));
}
std::this_thread::sleep_for(std::chrono::seconds(2));
for(auto& thread : threads) {
thread.join();
}
}
int main() {
// testSemaphore();
// testMutex();
// testSpinLock();
// testCond();
// testAtomic();
testRWLock();
return 0;
}
在测试时发现了条件变量的问题,在唤醒线程时需要保证线程已经跑起来了,因此测试函数中睡眠了2秒。此外互斥量和条件变量搭配使用,wait或者timewait需要已经上锁的互斥量,在wait或timedwait后释放互斥量。