C++ 并发锁管理工具

在 C++ 多线程编程中,我们绕不开一个核心问题:共享资源竞争。为了保护数据一致性,是最常用的工具:访问共享资源前 lock 加锁,操作完成后 unlock 解锁,使用起来虽然简单,但存在一些潜在的风险,例如:

  1. 分支遗漏:如代码中 if 条件触发提前返回,直接跳过 unlock
  2. 异常中断:操作共享资源时若抛出异常,程序会直接跳转至异常处理,unlock 无法被调用
  3. 人为疏忽:开发者在复杂逻辑中忘记编写 unlock 语句

// 1. 异常中断
std::mutex mtx;
void demo01()
{
    mtx.lock();
    throw std::runtime_error("发生异常!");
    mtx.unlock();
}

// 2. 分支遗漏
void demo02()
{
    mtx.lock();
    if (true) 
    {
        return;
    }
    mtx.unlock();
}

// 3. 人为疏忽
void demo03()
{
    mtx.lock();
    // 忘记写 unlock
}

显然,通过手动管理保证锁的释放,风险过高。我们需要一种让锁自动管理的机制,从根源上规避这类问题。

C++ 提供了一种基于 RAII 原则的资源管理机制,用于对各种资源(如内存、文件、锁等)进行自动管理。
其中,std::lock_guardstd::unique_lockstd::shared_lockscoped_lock 是专门用于管理锁资源的工具类。

1. std::lock_guard

std::lock_guard 是 C++11 引入的轻量级的 RAII 锁管理工具。它在构造时自动调用 lock() 加锁,在析构时自动调用 unlock() 解锁,可有效防止因异常或提前返回导致的锁泄漏。它不支持延迟加锁、超时加锁或锁所有权转移,适用于简单的锁应用场景。

#include <iostream>
#include <chrono>
#include <mutex>
#include <shared_mutex>

// 互斥锁
std::mutex mtx1;
// 递归锁
std::recursive_mutex mtx2;
// 读写锁
std::shared_mutex mtx3;
// 超时锁
std::timed_mutex mtx4;


void demo01()
{
	// 构造函数中自动加锁,析构函数中自动解锁
	{
		std::lock_guard<std::mutex> lock(mtx1);
	}

	{
		std::lock_guard<std::recursive_mutex> lock(mtx2);
	}

	// 注意:lock_guard 只能把读写锁当做互斥锁来使用,无法实现读写分离
	{
		std::lock_guard<std::shared_mutex> lock(mtx3);
	}

	// 注意:lock_guward 只能把超时锁当做普通锁来用,无法实现延迟加锁
	{
		std::lock_guard<std::timed_mutex> lock(mtx4);
	}

	// 简言之:std::lock_guard 不关心是什么锁,只会调用 lock 和 unlock 函数
}

void demo02()
{
	mtx1.lock();
	// 假设已经提前手动加锁
	std::lock_guard<std::mutex> lock(mtx1, std::adopt_lock);
}

int main()
{
	demo01();
	return 0;
}

2. std::unique_lock

std::unique_lock 是 C++11 引入的锁管理工具,在构造时调用 lock() 加锁,在析构时调用 unlock() 解锁,它支持自动加锁、延迟加锁、尝试加锁、接管已有锁,适合更为精细的锁管理。并且支持移动语义,被认为是多线程编程中最通用、最灵活的锁管理方案。

#include <iostream>
#include <chrono>
#include <mutex>
#include <shared_mutex>

std::mutex mtx1;
std::shared_mutex mtx2;
std::recursive_mutex mtx3;
std::timed_mutex mtx4;


// 1. unique_lock 基本使用
void demo01()
{
	// 1.1 管理互斥锁-标准用法
	{
		std::unique_lock<std::mutex> lock(mtx1);
		std::cout << "持有 mutex" << std::endl;
		// 自动解锁
	}

	// 1.2 手动管理-谨慎使用
	{
		std::unique_lock<std::mutex> lock(mtx1);
		// 某些情况下需要提前解锁
		lock.unlock();
		// 这里可以执行非临界区操作
		lock.lock(); // 重新加锁
		// 自动解锁
	}

	// 1.3 释放管理权-高级用法
	{
		std::unique_lock<std::mutex> lock(mtx1);
		std::mutex* p_mutex = lock.release();
		// 现在需要手动管理
		p_mutex->unlock();
	}

	// 1.4 管理读写锁(写)
	{
		std::unique_lock<std::shared_mutex> lock(mtx2);
		std::cout << "持有 shared_mutex 写模式" << std::endl;
	}

	// 1.5 管理递归锁
	{
		std::unique_lock<std::recursive_mutex> lock(mtx3);
		// 可以在同一线程递归加锁
		std::unique_lock<std::recursive_mutex> lock2(mtx3);
		std::cout << "持有递归锁" << std::endl;
	}
}

// 2. unique_lock 对象构造
void demo02()
{
	// 2.1 延迟加锁
	{
		// 构造时不加锁,需要手动 lock 加锁,加锁成功后,lock 负责锁的释放
		std::unique_lock<std::mutex> lock(mtx1, std::defer_lock);
		// 手动加锁
		lock.lock();

		// 手动解锁之后(也支持自动解锁),不会出现重复释放锁(安全)
		lock.unlock();
	}

	// 2.2 接管锁
	{
		mtx1.lock();
		std::unique_lock<std::mutex> lock(mtx1, std::adopt_lock);
	}

	// 2.3 尝试加锁
	{
		std::unique_lock<std::mutex> lock(mtx1, std::try_to_lock);
		if (lock.owns_lock())
		{
			// 获取锁成功
		}
		else
		{
			// 获取锁失败
		}
	}

	// 2.4 超时解锁
	{
		std::unique_lock<std::timed_mutex> lock(mtx4, std::chrono::seconds(3));
		// std::unique_lock<std::timed_mutex> lock(mtx4, std::chrono::steady_clock::now() + std::chrono::seconds(3));
		if (lock.owns_lock())
		{
			// 获取锁成功
		}
		else
		{
			// 获取锁失败
		}
	}

}

int main()
{
	demo01();
	demo02();
	return 0;
}

3. std::shared_lock

std::shared_lock 是 C++17 引入的一种 RAII 锁管理工具,通常与 shared_mutexshared_timed_mutex 配合使用。它用于自动管理共享锁(读锁),在构造时调用 lock_shared() 加锁,在析构时调用 unlock_shared() 解锁,它支持延迟加锁、尝试加锁、接管已有锁以及移动语义。
注意:std::shared_lock 仅能用于管理支持共享加锁接口的锁对象。

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>

std::shared_mutex mtx;
std::shared_timed_mutex tmtx;
int shared_data = 0;

void reader() 
{
    // 读操作需要使用 shared_lock
    std::shared_lock<std::shared_mutex> lock(mtx);
    std::cout << "读取数据: " << shared_data << std::endl;
}

void writer(int value) 
{
    // 写操作需要使用 unique_lock
    std::unique_lock<std::shared_mutex> lock(mtx);
    shared_data = value;
    std::cout << " 写入数据: " << value << std::endl;
}


// 1. std::shared_lock 基本用法
void demo01()
{
    std::vector<std::thread> readers;
    std::vector<std::thread> writers;

    // 先创建写线程
    writers.emplace_back(writer, 100);
    writers.emplace_back(writer, 200);

    // 等待写线程完成
    for (auto& t : writers) t.join();

    // 然后创建读线程
    for (int i = 0; i < 3; ++i)
        readers.emplace_back(reader);

    // 等待读线程完成
    for (auto& t : readers) t.join();
}

// 2. std::shared_lock 对象构建
void demo02()
{
    // 2.1 延迟解锁
    {
        std::shared_lock<std::shared_mutex> lock(mtx, std::defer_lock);
        lock.lock();
    }

    // 2.2 接管锁
    {
        mtx.lock_shared();
        std::shared_lock<std::shared_mutex> lock(mtx, std::adopt_lock);
    }

    // 2.3 尝试加锁
    {
        // 尝试进行读锁加锁,如果加锁成功,lock 负责锁的释放
        std::shared_lock<std::shared_mutex> lock(mtx, std::try_to_lock);
        if (lock.owns_lock())
        {
            std::cout << "成功获取读锁" << std::endl;
        }
        else
        {
            std::cout << "获取读锁失败" << std::endl;
        }
    }

    // 2.4 超时加锁
    {
        // std::shared_lock<std::shared_timed_mutex> lock(tmtx, std::chrono::seconds(3));
        std::shared_lock<std::shared_timed_mutex> lock(tmtx, std::chrono::steady_clock::now() + std::chrono::seconds(3));
        if (lock.owns_lock())
        {
            // 获取锁成功
        }
        else
        {
            // 获取锁失败
        }
    }
}

int main()
{
    demo01();
    demo02();
    return 0;
}

4. std::scoped_lock

在多线程程序中,我们常常需要同时操作多个锁。为了避免不同线程之间出现加锁顺序不一致导致的死锁问题,C++ 提供了 std::lock() 来一次性安全地锁定多个互斥量。

std::mutex mtx1, mtx2;

void demo()
{
    std::lock(mtx1, mtx2);

    // 进行相应操作

    mtx1.unlock();
    mtx2.unlock();
}

不过,这样的写法仍然存在风险:如果在加锁后的代码中提前返回抛出异常,则可能导致锁未被正常释放,造成死锁或资源泄露。为了解决这一问题,C++17 引入了 std::scoped_lock,一种基于 RAII 的多锁管理工具,它能在作用域结束时自动解锁所有持有的锁,从而让多锁管理变得更安全、更简洁。

#include <iostream>
#include <mutex>

std::mutex mtx1, mtx2;

void demo()
{
    // 用法1:自动锁定多个互斥量(推荐)
    {
        std::scoped_lock lock(mtx1, mtx2);
        // 临界区 - 两个互斥量都已锁定
        std::cout << "两个锁自动加锁" << std::endl;
    } // 自动解锁


    // 用法2:接管已锁定的互斥量(不推荐,容易出错)
    {
        std::lock(mtx1, mtx2);
        // 注意:这里只传递互斥量,不重复锁定
        std::scoped_lock<std::mutex, std::mutex> lock(std::adopt_lock, mtx1, mtx2);
        std::cout << "接管已经加锁的锁" << std::endl;
    } // 自动解锁
}

int main()
{
    demo();
    return 0;
}

未经允许不得转载:一亩三分地 » C++ 并发锁管理工具
评论 (0)

7 + 5 =