mutex.h:信号量,互斥锁,读写锁,范围锁模板,自旋锁,原子锁
锁
**锁不能进行拷贝操作:**锁是用于管理多线程并发访问共享资源的同步原语。这些锁包括互斥锁(mutex)、读写锁(rwlock)等。它们通常不支持拷贝构造和拷贝赋值,这是为了防止在一个线程持有锁的情况下,另一个线程通过拷贝得到相同的锁,从而可能导致死锁或数据不一致的问题。
《Effective C++》 条款 06
想要禁止⼀个类对象的拷⻉操作,就要禁⽌拷⻉构造函数和拷⻉赋值运算符。
解决⽅案2:
定义⼀个基类专⻔阻⽌拷⻉动作,继承该基类的派生类起实例化对象也就无法进行拷⻉操作。
/**
* @file noncopyable.h
* @brief 不可拷贝对象封装
*/
#ifndef __Fzk_NONCOPYABLE_H__
#define __Fzk_NONCOPYABLE_H__
namespace FzkCoroutine {
/**
* @brief 对象无法拷贝,赋值
*/
class Noncopyable {
public:
/**
* @brief 默认构造函数
*/
Noncopyable() = default;
/**
* @brief 默认析构函数
*/
~Noncopyable() = default;
/**
* @brief 拷贝构造函数(禁用)
*/
Noncopyable(const Noncopyable&) = delete;
/**
* @brief 赋值函数(禁用)
*/
Noncopyable& operator=(const Noncopyable&) = delete;
};
}
#endif
基于pthread进一步封装了信号量,互斥锁,读写锁,范围锁模板,自旋锁,原子锁。
局部锁模板:
采用RAII编程风格,RAII的核心思想是利用对象的生命周期来管理资源,确保资源在对象的构造函数中被获取,并在析构函数中被释放。
ScopedLockImpl 及其派生类通过在构造时获取资源(加锁)并在析构时释放资源(解锁)。
/**
* @brief 局部锁的模板实现
*/
template<class T>
struct ScopedLockImpl {
public:
/**
* @brief 构造函数
* @param[in] mutex Mutex
*/
ScopedLockImpl(T& mutex)
:m_mutex(mutex) {
m_mutex.lock();
m_locked = true;
}
/**
* @brief 析构函数,自动释放锁
*/
~ScopedLockImpl() {
unlock();
}
/**
* @brief 加锁
*/
void lock() {
if(!m_locked) {
m_mutex.lock();
m_locked = true;
}
}
/**
* @brief 解锁
*/
void unlock() {
if(m_locked) {
m_mutex.unlock();
m_locked = false;
}
}
private:
/// mutex
T& m_mutex;
/// 是否已上锁
bool m_locked;
};
类似于C++的lock_guard:
lock_guard通过与互斥锁(mutex)结合使用来实现线程同步。当创建一个lock_guard对象时,获取提供给它的互斥锁的所有权,并自动调用互斥锁的lock()方法来加锁。如果互斥锁已经被其他线程锁定,当前线程将会阻塞,直到互斥锁被解锁。
当lock_guard对象离开作用域时,它的析构函数会被自动调用。在析构函数中,lock_guard会调用互斥锁的unlock()方法来解锁互斥锁。这样可以确保即使在异常情况下,互斥锁也能被正确解锁,避免死锁的发生。
lock_guard特点:
1、创建即加锁,作⽤域结束⾃动析构并解锁,⽆需⼿⼯解锁
2、不能中途解锁,必须等作⽤域结束才解锁
3、不能复制
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 定义一个互斥锁
int counter = 0; // 共享资源
void increment() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 加锁
++counter; // 访问共享资源
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Counter: " << counter << std::endl;
return 0;
}
互斥量Mutex:
/**
* @brief 互斥量
*/
class Mutex : Noncopyable { //继承不可拷贝数据类
public:
/// 局部锁
typedef ScopedLockImpl<Mutex> Lock;
/**
* @brief 构造函数
*/
Mutex() {
pthread_mutex_init(&m_mutex, nullptr);
}
/**
* @brief 析构函数
*/
~Mutex() {
pthread_mutex_destroy(&m_mutex);
}
/**
* @brief 加锁
*/
void lock() {
pthread_mutex_lock(&m_mutex);
}
/**
* @brief 解锁
*/
void unlock() {
pthread_mutex_unlock(&m_mutex);
}
private:
/// mutex
pthread_mutex_t m_mutex;
};
读写互斥量RWMutex:
/**
* @brief 读写互斥量
*/
class RWMutex : Noncopyable{
public:
/// 局部读锁
typedef ReadScopedLockImpl<RWMutex> ReadLock;
/// 局部写锁
typedef WriteScopedLockImpl<RWMutex> WriteLock;
/**
* @brief 构造函数
*/
RWMutex() {
pthread_rwlock_init(&m_lock, nullptr);
}
/**
* @brief 析构函数
*/
~RWMutex() {
pthread_rwlock_destroy(&m_lock);
}
/**
* @brief 上读锁
*/
void rdlock() {
pthread_rwlock_rdlock(&m_lock);
}
/**
* @brief 上写锁
*/
void wrlock() {
pthread_rwlock_wrlock(&m_lock);
}
/**
* @brief 解锁
*/
void unlock() {
pthread_rwlock_unlock(&m_lock);
}
private:
/// 读写锁
pthread_rwlock_t m_lock;
};
自旋锁
自旋锁是一种同步机制,它不会导致线程进入睡眠状态,而是通过循环不断尝试获取锁资源,可以避免线程切换的开销。但只适用于锁被持有时间短的场景,自旋等待的时间不会太长,而且可以避免线程切换的开销。如果锁被持有时间较长或竞争激烈导致很多线程在自旋等待,那么自旋锁可能会导致CPU资源的浪费,因为线程在等待时会持续占用CPU周期。
此外,自旋锁不适合在中断处理中使用,因为中断处理程序应该尽快完成,避免长时间占用CPU。
/**
* @brief 自旋锁
*/
class Spinlock : Noncopyable {
public:
/// 局部锁
typedef ScopedLockImpl<Spinlock> Lock;
/**
* @brief 构造函数
*/
Spinlock() {
pthread_spin_init(&m_mutex, 0);
}
/**
* @brief 析构函数
*/
~Spinlock() {
pthread_spin_destroy(&m_mutex);
}
/**
* @brief 上锁
*/
void lock() {
pthread_spin_lock(&m_mutex);
}
/**
* @brief 解锁
*/
void unlock() {
pthread_spin_unlock(&m_mutex);
}
private:
/// 自旋锁
pthread_spinlock_t m_mutex;
};
原子锁??感觉就是用原子布尔类型实现的自旋锁
CASLock使用C++11中的std::atomic_flag来实现无锁同步,lock()函数使用std::atomic_flag_test_and_set_explicit()函数尝试获取锁,如果获取失败则一直循环等待。unlock()函数使用std::atomic_flag_clear_explicit()函数释放锁。
定义了一个volatile std::atomic_flag类型的成员变量m_mutex,用于表示锁的状态。由于它是volatile类型的,因此编译器不会对其进行优化,保证了其可见性,每次操作都会从内存读取m_mutex的值。
/**
* @brief 原子锁
*/
class CASLock : Noncopyable {
public:
/// 局部锁
typedef ScopedLockImpl<CASLock> Lock;
/**
* @brief 构造函数
*/
CASLock() {
m_mutex.clear();
}
/**
* @brief 析构函数
*/
~CASLock() {
}
/**
* @brief 上锁
*/
void lock() {
//获取失败则一直循环等待 感觉和自旋锁没区别了
while (std::atomic_flag_test_and_set_explicit(&m_mutex, std::memory_order_acquire));
}
/**
* @brief 解锁
*/
void unlock() {
std::atomic_flag_clear_explicit(&m_mutex, std::memory_order_release);
}
private:
/// 原子状态
volatile std::atomic_flag m_mutex;
};
CASLock lock;
int shared_data = 0;
void thread_func() {
for (int i = 0; i < 100000; ++i) {
CASLock::Lock l(lock); // 加锁
++shared_data; // 访问共享资源
}
}
int test2() {
std::thread t1(thread_func);
std::thread t2(thread_func);
t1.join();
t2.join();
std::cout << "shared_data: " << shared_data << std::endl;
return 0;
}
测试效果:
1、加原子锁
2、未加原子锁
#include 只能将基本数据类型声明为原子变量
#include<iostream>
#include<thread>
#include<windows.h>
#include<atomic> //新增
#include<vector>
#include<mutex>
using namespace std;
void Mythread(atomic<int>& num) //修改
{
for (int i = 0; i < 100000; i++)
{
num++;
}
}
void test03()
{
//int num = 0;
std::atomic<int> num = 0;
int threadNum = 2; //线程个数
vector<std::thread> m_thread;
m_thread = vector<std::thread>(threadNum);
for (auto i = 0; i < threadNum; i++)
{
m_thread[i] = std::thread(Mythread, &num);
}
for (auto i = 0; i < threadNum; i++)
{
m_thread[i].join();
}
cout <<"结果:"<<num << endl;
}
int main()
{
test03();
system("pause");
return 0;
}
小结一下:
互斥量(Mutex):
互斥量是最基本的同步机制之一。它阻止多个线程同时访问共享资源。
当一个线程锁定互斥量时,如果另一个线程尝试锁定同一个互斥量,它将被阻塞(挂起),直到拥有锁的线程释放该锁。
互斥量适用于锁定时间较长的场景,比如复杂操作或涉及I/O的操作。
在 C++ 中,可以使用 头文件中的 std::mutex 类。
自旋锁(Spinlock):
自旋锁在等待锁释放时,线程会在循环检查锁的状态直到获取锁,它不会使线程挂起,而是占用CPU周期等待。
自旋锁适用于锁定时间非常短的场景,因为它避免了线程挂起和恢复的开销(避免线程切换)。
C++11 标准没有直接提供自旋锁,但可以通过原子操作实现,或者使用平台特定的实现(如 POSIX 的 pthread_spinlock,POSIX“可移植操作系统接口”,能够在多种系统之间使用)。
原子锁(Atomic Lock):
原子操作是指不可分割、不会被线程调度机制打断的操作。在执行完毕之前,不会有其他线程对这个操作进行干扰。
C++11 引入了原子操作库 ,提供了一组原子类型,如 std::atomic,可以用来实现低开销的线程安全操作。
原子操作通常用于简单的赋值、递增、递减等操作,而且是无锁的,所以开销比互斥量和自旋锁都要小。
总结:
使用互斥量时,长时间锁定资源会使其他线程挂起,适合复杂操作。
使用自旋锁时,线程会忙等待直到获取锁,适合短时间锁定资源。
使用原子操作时,可以保证单一操作的线程安全,无需锁定,开销最小,但仅限于简单操作(因为只有两种状态)。