【C++ 并发与多线程】std::thread类-保护数据加锁

多线程数据保护实战
本文探讨了多线程环境下共享资源的保护方法,重点介绍了std::mutex的使用,包括互斥锁的基本概念、加锁与解锁操作,以及通过std::lock_guard和std::unique_lock提高异常安全性。

在应届生面试的时候,很多面试官都会问——“多线程如何共享资源”。在操作系统层面上可以给出若干关键词答案,但是在语言层面,这个问题考虑的就没有那么简单了。同时,很多人会将多线程数据共享和线程同步混淆。有关线程同步,我们会在接下来的章节里着重阐述。本文主要聚焦于保护共享数据,首先从加锁入手,进而扩展到加锁无法解决的问题,最后会给出一些其他保护方案。


参数入栈

一个存放参数的栈数据结构,相同函数的参数必须要在栈中相连,我们来实现这个功能,看下面代码:

#include <stack>
#include <iostream>

class MutexTest
{
public:
	MutexTest() : m_charStack() { }
	~MutexTest() { }

	void Push(int n, char c)
	{
		for (int i = 0; i < n; ++i)
		{
			m_charStack.push(c);
			std::cout << c;
		}
		std::cout << std::endl;
	}
private:
	std::stack<char> m_charStack;
};

int main()
{
	MutexTest test;
	std::thread mutexTestThread1(&MutexTest::Push, &test, 10, 'a');
	std::thread mutexTestThread2(&MutexTest::Push, &test, 10, 'b');

	mutexTestThread1.join();
	mutexTestThread2.join();

	std::cout << std::this_thread::get_id() << std::endl;
	return 0;
}
------------------------------------------------
aaaababababababab
bbb
5420
请按任意键继续. . .
结果不确定!!!!!!!!!!!!!!!!

上面这段代码的执行结果是不确定的,这是因为我们无法预测线程的执行顺序,多个线程共享同一个数据栈存在竞态条件(Race Condition)。所以我们可能得到下面的执行结果,所有的参数都是交叉在一起的,这不是我们想要的结果。

aabbbbbbbaaaaaaaabbb

竞态条件是多线程编程的噩梦,为什么会出现竞态条件可以自行百度,我们主要是为了解决这个问题。让最终执行的结果为:

aaaaaaaaaa
bbbbbbbbbb

参数入栈保护

std::mutex是C++11提供的数据加锁类,C++中通过实例化 std::mutex 创建互斥量,通过调用成员函数lock()进行上锁,unlock()进行解锁。

#include <iostream>
#include <mutex>
#include <thread>
#include <stack>
using namespace std;

class MutexTest
{
public:
	MutexTest() : m_mutex(), m_charStack() { }
	~MutexTest() { }

	void Push(int n, char c)
	{
		m_mutex.lock();
		for (int i = 0; i < n; ++i)
		{
			m_charStack.push(c);
			std::cout << c;
		}
		std::cout << std::endl;
		m_mutex.unlock();
	}
private:
	std::mutex       m_mutex;
	std::stack<char> m_charStack;
};

int main()
{
	MutexTest test;
	std::thread mutexTestThread1(&MutexTest::Push, &test, 10, 'a');
	std::thread mutexTestThread2(&MutexTest::Push, &test, 10, 'b');

	mutexTestThread1.join();
	mutexTestThread2.join();

	std::cout << std::this_thread::get_id() << std::endl;
	return 0;
}
-------------------------------------------
aaaaaaaaaa
bbbbbbbbbb
6472
请按任意键继续. . .

这段代码和上面的不同点就是使用std::mutex,在访问m_charStack之前上锁,其他线程就必须要等待解锁后才能访问m_charStack。如果我们忘记解锁,那么m_charStack就再也无法被访问了,所以有必要用RAII类std::lock_guard进行封装——构造时上锁,析构时解锁。

void MutexTest::Push(int n, char c)
{
    std::lock_guard<std::mutex> lg(m_mutex);
    for (int i = 0; i < n; ++i)
    {
        m_charStack.push(c);
        std::cout << c;
    }
    std::cout << std::endl;
}

C++还提供了std::unique_lock锁,相对于std::lock_guard,该锁提供了更好地上锁和解锁灵活性控制。std::unique_lock以独占所有权的方式来管理mutex对象的上锁和解锁操作。我们来看看其用法:

#include <iostream>
#include <mutex>
#include <thread>
#include <stack>
using namespace std;
// unique_lock constructor example

std::mutex foo, bar;

void task_a() {
	std::lock(foo, bar);         // simultaneous lock (prevents deadlock)
	std::unique_lock<std::mutex> lck1(foo, std::adopt_lock);
	std::unique_lock<std::mutex> lck2(bar, std::adopt_lock);
	std::cout << "task a\n";
	// (unlocked automatically on destruction of lck1 and lck2)
}

void task_b() {
	// foo.lock(); bar.lock(); // replaced by:
	std::unique_lock<std::mutex> lck1, lck2;
	lck1 = std::unique_lock<std::mutex>(bar, std::defer_lock);
	lck2 = std::unique_lock<std::mutex>(foo, std::defer_lock);
	std::lock(lck1, lck2);       // simultaneous lock (prevents deadlock)
	std::cout << "task b\n";
	// (unlocked automatically on destruction of lck1 and lck2)
}


int main()
{
	std::thread th1(task_a);
	std::thread th2(task_b);

	th1.join();
	th2.join();

	return 0;
}
----------------------------------------------------
task a
task b
请按任意键继续. . .

现在我们终于得到了我们想要的结果,可惜在很多时候加锁并不是解决数据共享的万能药。下一节,我们将会涉及到一些加锁无法解决的数据共享问题。


std::mutex

定义于头文件 <mutex>

  

class mutex;

 (C++11 起)
   

mutex 类是能用于保护共享数据免受从多个线程同时访问的同步原语。

mutex 提供排他性非递归所有权语义:

  • 调用方线程从它成功调用 lock 或 try_lock 开始,到它调用 unlock 为止占有 mutex 。
  • 线程占有 mutex 时,所有其他线程若试图要求 mutex 的所有权,则将阻塞(对于 lock 的调用)或收到 false 返回值(对于 try_lock ).
  • 调用方线程在调用 lock 或 try_lock 前必须不占有 mutex 。

若 mutex 在仍为任何线程所占有时即被销毁,或在占有 mutex 时线程终止,则行为未定义。 mutex 类满足互斥 (Mutex) 和标准布局类型 (StandardLayoutType) 的全部要求。

std::mutex 既不可复制亦不可移动。

成员类型

 
成员类型定义
native_handle_type(可选)实现定义

成员函数

(构造函数)

构造互斥 
(公开成员函数)

(析构函数)

销毁互斥 
(公开成员函数)

operator=

[被删除]

不可复制赋值 
(公开成员函数)

锁定

lock

锁定互斥,若互斥不可用则阻塞 
(公开成员函数)

try_lock

尝试锁定互斥,若互斥不可用则返回 
(公开成员函数)

unlock

解锁互斥 
(公开成员函数)

原生句柄

native_handle

返回底层实现定义的原生句柄 
(公开成员函数)

注意

通常不直接使用 std::mutex : std::unique_lock 、 std::lock_guard 或 std::scoped_lock (C++17 起)以更加异常安全的方式管理锁定。

示例

此示例展示 mutex 能如何用于在保护共享于二个线程间的 std::map 。

#include <iostream>
#include <map>
#include <string>
#include <chrono>
#include <thread>
#include <mutex>
 
std::map<std::string, std::string> g_pages;
std::mutex g_pages_mutex;
 
void save_page(const std::string &url)
{
    // 模拟长页面读取
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::string result = "fake content";
 
    std::lock_guard<std::mutex> guard(g_pages_mutex);
    g_pages[url] = result;
}
 
int main() 
{
    std::thread t1(save_page, "http://foo");
    std::thread t2(save_page, "http://bar");
    t1.join();
    t2.join();
 
    // 现在访问g_pages是安全的,因为线程t1/t2生命周期已结束
    for (const auto &pair : g_pages) {
        std::cout << pair.first << " => " << pair.second << '\n';
    }
}
------------------------------------------------------
http://bar => fake content
http://foo => fake content
请按任意键继续. . .

引用来源:

C++并发编程2——为保护数据加锁(一)

std::mutex

<think> 我们被要求回答关于std::scoped_lock的问题,包括使用方法、原理和应用场景。同时,我们需要参考提供的引用内容(引用[1]和引用[2])来构建答案。 从引用[1]和引用[2]中,我们可以提取以下关键信息: 引用[1]提到:std::scoped_lock用于同时锁个mutex(C++17引入)。 引用[2]详细说明了std::unique_lock和std::scoped_lock: - std::unique_lock:更灵活,支持延迟加锁、尝试加锁、手动解锁/重新加锁、可移动,必须用于std::condition_variable::wait。 - std::scoped_lock:同时锁个mutex(C++17)。 因此,我们的回答将围绕以下结构: 1. 使用方法:展示如何使用std::scoped_lock,包括基本用法和锁个互斥量的用法。 2. 原理:解释std::scoped_lock如何工作,特别是它如何避免死锁(使用std::lock的锁个互斥量的机制)以及RAII机制。 3. 应用场景:在哪些情况下应该使用std::scoped_lock,特别是需要同时锁定个互斥量时。 另外,注意引用[2]中提到的std::scoped_lockstd::lock_guard的关系:std::scoped_lock是std::lock_guard的扩展,用于个互斥量。 我们还需要注意,用户的问题中提到了引用[3],但引用[3]的内容是关于shared_ptr和删除器的,std::scoped_lock无关,因此我们忽略它。 现在,我们开始构建答案。 </think>### `std::scoped_lock`详解 `std::scoped_lock`是C++17引入的**RAII互斥量包装器**,专为同时锁定个互斥量而设计,能有效预防死锁。以下是核心要点: --- #### **一、使用方法** ```cpp #include <mutex> std::mutex mtx1, mtx2, mtx3; void safe_operation() { // 同时锁定个互斥量(自动防死锁) std::scoped_lock lock(mtx1, mtx2, mtx3); // 构造时加锁 // 受保护的临界区操作 // ... } // 析构时自动解锁(RAII) ``` **关键特性**: 1. **变参模板**:支持任意数量的互斥量 2. **自动死锁避免**:内部使用`std::lock`实现死锁回避算法[^1] 3. **零额外开销**:相比手动组合锁更简洁安全 --- #### **二、工作原理** 1. **构造阶段**: - 调用`std::lock(mutexes...)`锁定所有互斥量 - 使用**死锁回避算法**(如贪心算法)保证加锁顺序安全[^2] 2. **生命周期**: ```mermaid graph LR A[创建scoped_lock对象] --> B[锁定所有mutex] B --> C[执行临界区代码] C --> D[离开作用域] D --> E[自动解锁所有mutex] ``` 3. **析构阶段**: - 按**逆序释放**所有互斥量 - 异常安全:即使临界区抛出异常也能正确解锁 --- #### **三、应用场景** | 场景 | 说明 | 代码示例 | |------|------|----------| | **资源保护** | 需同时访问个关联资源 | `scoped_lock(res_mtx, log_mtx)` | | **嵌套锁** | 避免层级锁导致的死锁 | `scoped_lock(outer_mtx, inner_mtx)` | | **事务操作** | 保证步骤操作的原子性 | 转账:`scoped_lock(accA_mtx, accB_mtx)` | **对比其他锁**: | 特性 | `scoped_lock` | `lock_guard` | `unique_lock` | |------|---------------|--------------|---------------| | 锁数量 | 锁 | 锁 | 锁 | | 灵活性 | 中 | 低 | 高 | | 性能 | 最优 | 最优 | 稍低 | | 适用场景 | 互斥量 | 简锁 | 需延迟锁/条件变量 | > ✅ **最佳实践**:优先用`scoped_lock`替代`lock(mtx1); lock(mtx2);`手动组合[^2] --- #### **四、原理深入** 1. **死锁避免实现**: ```cpp template<typename... MutexTypes> scoped_lock(MutexTypes&... mtxs) { std::lock(mtxs...); // 关键:标准库死锁安全算法 } ``` 2. **RAII扩展**: - 继承`std::lock_guard`理念 - 通过**变参模板**泛化锁场景 3. **移动语义**: ```cpp scoped_lock(scoped_lock&&) = delete; // 禁止移动 ``` 设计为不可移动,确保锁生命周期严格绑定作用域 --- #### **五、典型错误示例** ```cpp // 危险:可能死锁 void unsafe_op() { std::lock_guard g1(mtx1); std::lock_guard g2(mtx2); // 若其他线程以相反顺序加锁则死锁 } // 正确:用scoped_lock防死锁 void safe_op() { std::scoped_lock g(mtx1, mtx2); // 自动处理加锁顺序 } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值