文章目录
-
- 3.2 使用互斥量
- 3.2.3 接口间的条件竞争
- 3.2.4 死锁:问题描述及解决方案
- 3.2.5 避免死锁的进阶指导
- 3.2.6 `std::unique_lock` —— 灵活的锁
- 3.2.8 锁的粒度
- 3.3.3 嵌套锁
第3章 共享数据
本章主要内容
- 共享数据的问题
- 使用互斥保护数据
- 保护数据的替代方案
在上一章中,我们已经对线程管理有了一定的了解。现在,让我们来探讨一下“共享数据的那些事儿”。
共享数据的问题
想象一下,你和朋友合租一个公寓,公寓中只有一个厨房和一个卫生间。当你的朋友在使用卫生间时,你就无法使用它了。同样的问题也会出现在厨房。例如,如果厨房里有一个烤箱,你在烤香肠的同时,也在做蛋糕,那么你可能会得到不想要的食物(比如香肠味的蛋糕)。此外,在公共空间进行某项任务时,如果发现某些需要的东西被别人拿走,或者在你离开的一段时间内有些东西被移动了位置,这都会让你感到不爽。
类似的问题也困扰着线程。当多个线程访问共享数据时,必须制定一些规则来限定哪些数据可以被哪些线程访问。如果一个线程更新了共享数据,它需要通知其他线程。从易用性的角度来看,同一进程中的多个线程共享数据有利有弊,但错误的共享数据使用是导致bug的主要原因。
使用互斥保护数据
为了避免上述问题,我们可以使用互斥锁(mutex)来保护共享数据。互斥锁确保在同一时间只有一个线程可以访问共享数据,从而防止数据竞争和不一致的状态。
保护数据的替代方案
除了互斥锁,还有其他一些方法可以保护共享数据,例如:
- 读写锁:允许多个线程同时读取数据,但只允许一个线程写入数据。
- 原子操作:通过硬件支持的原子操作来确保数据的一致性。
- 无锁编程:通过复杂的算法和数据结构来实现线程安全,而不使用锁。
本章将以数据共享为主题,探讨如何避免上述及潜在问题的发生,同时最大化共享数据的优势。
通过以上内容,我们希望能够帮助你更好地理解共享数据的问题及其解决方案。在接下来的章节中,我们将深入探讨这些技术的具体实现和应用。
3.1 共享数据的问题
在多线程编程中,共享数据是一个强大但危险的工具。当多个线程同时访问和修改共享数据时,如果没有妥善的管理,就会引发一系列复杂的问题。本节将深入探讨共享数据修改带来的挑战,以及如何通过理解“不变量”来避免潜在的错误。
共享数据的核心问题
共享数据的问题主要源于数据的修改。如果共享数据是只读的,那么所有线程都能安全地访问它,因为数据不会被改变。然而,当一个或多个线程试图修改共享数据时,情况就会变得复杂。修改操作可能会破坏数据的一致性,导致其他线程读取到错误或无效的数据。
这种问题的根源在于条件竞争(Race Condition):当多个线程同时访问共享数据,且至少有一个线程试图修改数据时,程序的执行结果可能依赖于线程调度的顺序,从而导致不可预测的行为。
不变量的重要性
为了理解共享数据修改带来的问题,我们需要引入**不变量(Invariants)**的概念。不变量是描述数据结构在特定条件下必须保持的稳定状态。例如,对于一个双链表,不变量可能是“每个节点的前向指针和后向指针都正确指向相邻节点”。
在修改共享数据时,尤其是复杂的数据结构,更新操作通常会暂时破坏不变量。例如,在删除双链表中的一个节点时,需要更新其相邻节点的指针。在这个过程中,不变量会被暂时破坏,直到所有指针更新完成。
示例:删除双链表中的节点
![[Pasted image 20250209160942.png]]
让我们以双链表的节点删除为例,具体说明共享数据修改可能引发的问题。假设我们有一个双链表,每个节点包含两个指针:一个指向下一个节点,另一个指向前一个节点。删除一个节点的步骤如下:
- 找到要删除的节点N。
- 更新前一个节点的指针,使其指向节点N的下一个节点。
- 更新后一个节点的指针,使其指向节点N的前一个节点。
- 删除节点N。
在这个过程中,步骤2和步骤3会暂时破坏不变量。例如,在步骤2完成后,前一个节点的指针已经指向了节点N的下一个节点,但后一个节点的指针还未更新。此时,如果有其他线程访问链表,可能会读取到不一致的数据。
多线程环境中的问题
在多线程环境中,这种临时的不变量破坏会引发严重的问题。例如:
- 如果一个线程在删除节点的过程中被中断,其他线程可能会访问到一个部分更新的链表,导致读取到无效的数据。
- 如果多个线程同时尝试删除相邻的节点,可能会导致链表结构的永久性损坏,甚至引发程序崩溃。
这种问题被称为条件竞争(Race Condition),是多线程编程中最常见的错误之一。它的根本原因在于多个线程对共享数据的访问和修改缺乏协调。
条件竞争的后果
条件竞争的后果可能是灾难性的。例如:
- 数据损坏:链表、树等复杂数据结构可能会被破坏,导致程序无法正常运行。
- 不可预测的行为:程序的执行结果可能依赖于线程调度的顺序,导致难以复现和调试的bug。
- 程序崩溃:在极端情况下,条件竞争可能导致程序崩溃或数据丢失。
总结
共享数据的修改是多线程编程中的一个核心挑战。为了确保程序的正确性,我们必须理解不变量在数据结构中的作用,并采取措施避免条件竞争的发生。在接下来的章节中,我们将探讨如何使用互斥锁、原子操作等技术来保护共享数据,确保多线程程序的稳定性和可靠性。
通过以上分析,我们希望你能更清晰地认识到共享数据问题的本质,并为解决这些问题打下坚实的基础。
3.1.1 条件竞争
想象一下你在一个大型电影院买票,售票窗口很多,大家都在同时购票。当你和其他人都在竞争购买同一场电影的票时,你的座位选择就取决于之前的座位预定情况。如果剩余的座位不多了,那么就会出现一场“抢票大战”,看谁能抢到最后一张票。这就是一个典型的条件竞争的例子:你的座位(或者电影票)是否能成功购买,取决于购票的先后顺序。
在并发编程中,竞争条件指的是多个线程的执行顺序会影响到程序的结果。每个线程都试图尽快完成自己的任务。多数情况下,即使执行顺序发生变化,也是良性竞争,结果仍然可以接受。例如,两个线程同时向一个处理队列中添加任务,由于队列的特性,谁先添加任务并不会影响最终的结果。
只有当不变量(invariant)遭到破坏时,才会出现恶性竞争,比如双向链表的例子。并发访问共享数据时,如果多个线程的执行顺序不当,导致数据状态与预期的不变量不符,就会产生恶性竞争。C++ 标准中定义了数据竞争这个术语,它是一种特殊的条件竞争:当多个线程并发地修改同一个独立对象时,就会发生数据竞争。数据竞争会导致未定义行为,这是并发编程中非常严重的问题。
恶性条件竞争通常发生在对多个数据块进行修改的场景,例如修改两个相连的指针(如图 3.1 所示)。当操作需要访问两个独立的数据块时,不同的指令可能会交错执行,一个线程可能正在修改数据块,而另一个线程同时访问了该数据块。由于这种交错执行的概率较低,因此这类问题通常难以发现和复现。即使 CPU 指令连续执行完成,并且数据结构可以被其他并发线程访问,问题再次复现的几率仍然很低。但是,随着系统负载的增加,执行次数也随之增加,问题复现的概率也会增大。因此,条件竞争问题可能会在系统高负载的情况下才会显现出来。此外,条件竞争通常对时间非常敏感,因此在调试模式下运行程序时,错误可能会完全消失,因为调试模式会影响程序的执行时间(即使影响很小)。
对于并发编程人员来说,条件竞争是一个噩梦。在编写多线程程序时,我们需要使用各种复杂的技术来避免恶性条件竞争。
3.1.2 避免恶性条件竞争
解决恶性条件竞争最直接的方法是对共享数据结构采用某种保护机制,以确保只有修改线程才能看到不变量的中间状态。从其他访问线程的角度来看,修改要么已经完成,要么尚未开始。C++ 标准库提供了许多类似的机制,我们将在后面逐一介绍。
另一种选择是修改数据结构和不变量的设计,使其能够完成一系列不可分割的变化,从而保证每个不变量的状态都是一致的。这种方法称为无锁编程(lock-free programming)。然而,无锁编程非常复杂,很难保证其正确性。在这种层面上,无论是内存模型的细微差别,还是线程访问数据的能力,都会增加编程的难度。
还有一种处理条件竞争的方法是使用事务(transaction)的方式来更新数据结构(就像更新数据库一样)。所需的读取和写入数据都存储在事务日志中,然后将之前的操作合并并提交。当数据结构被另一个线程修改或者处理重启时,提交操作将无法进行。这种方法称为软件事务内存(software transactional memory,STM),是一个热门的研究领域。本书不会对 STM 进行详细介绍,因为 C++ 目前没有直接支持 STM(尽管 C++ 有事务性内存扩展的技术规范 [1])。
最基本的保护共享数据结构的方法是使用 C++ 标准库提供的互斥量(mutex)。
好的,我来帮你优化这段关于互斥量的叙述:
3.2 使用互斥量
在并发编程中,我们不希望共享数据出现竞争条件,导致数据不变量遭到破坏。一种简单的想法是将所有访问共享数据的代码都标记为互斥的,即同一时刻只允许一个线程访问共享数据。这样,任何线程在执行时,其他线程都必须等待,除非该线程正在修改共享数据,否则任何线程都不可能看到不变量的中间状态。
实现这一想法的关键在于锁机制。线程在访问共享数据之前,先将数据“锁住”,访问结束后再将数据“解锁”。线程库需要保证,当一个线程使用互斥量锁住共享数据时,其他线程必须等到该线程解锁后才能访问数据。
互斥量(mutex)是 C++ 中保护数据的最通用机制。然而,正确使用互斥量需要仔细的代码编排,以确保数据的正确性(见 3.2.2 节),并避免接口间的竞争条件(见 3.2.3 节)。此外,互斥量也可能导致死锁(见 3.2.4 节),或者对数据的保护过多或过少(见 3.2.8 节)。
3.2.1 互斥量
我们可以通过实例化 std::mutex
来创建互斥量实例。lock()
成员函数用于对互斥量上锁,unlock()
用于解锁。但是,不建议直接调用这些成员函数,因为这意味着必须在每个函数出口(包括异常情况)都调用 unlock()
。C++ 标准库为互斥量提供了 RAII(Resource Acquisition Is Initialization)模板类 std::lock_guard
,它在构造时提供一个已锁定的互斥量,并在析构时自动解锁,从而保证互斥量始终能被正确解锁。
以下代码展示了如何在多线程应用中使用 std::mutex
和 std::lock_guard
来保护列表的访问:
#include <list>
#include <mutex>
#include <algorithm>
std::list<int> some_list; // 1
std::mutex some_mutex; // 2
void add_to_list(int new_value) {
std::lock_guard<std::mutex> guard(some_mutex); // 3
some_list.push_back(new_value);
}
bool list_contains(int value_to_find) {
std::lock_guard<std::mutex> guard(some_mutex); // 4
return std::find(some_list.begin(), some_list.end(), value_to_find) != some_list.end();
}
在上述代码中,some_list
是一个全局变量 ①,它被一个全局互斥量 some_mutex
保护 ②。add_to_list()
③ 和 list_contains()
④ 函数使用 std::lock_guard<std::mutex>
来确保对数据的访问是互斥的:list_contains()
不可能看到正在被 add_to_list()
修改的列表。
C++17 添加了一个新特性:模板类参数推导。对于像 std::lock_guard
这样简单的模板类型,我们可以省略模板参数列表。因此,③ 和 ④ 的代码可以简化为:
C++
std::lock_guard guard(some_mutex);
具体的模板参数类型推导则交给 C++17 的编译器完成。在 3.2.4 节中,我们将介绍 C++17 中的一种增强版数据保护机制——std::scoped_lock
。因此,在 C++17 环境下,上面的代码也可以写成:
C++
std::scoped_lock guard(some_mutex);
为了保持代码清晰,并兼容只支持 C++11 标准的编译器,我们继续使用 std::lock_guard
,并在代码中明确写出模板参数的类型。
在某些情况下,使用全局变量没有问题。但大多数情况下,互斥量通常会与需要保护的数据放在同一个类中,而不是定义为全局变量。这是面向对象设计的原则:将它们放在一个类中,可以使它们联系在一起,也可以对类的功能进行封装和数据保护。在这种情况下,add_to_list
和 list_contains
函数可以作为这个类的成员函数。互斥量和需要保护的数据都在类中定义为私有成员,这使得代码更清晰,也方便了解何时对互斥量上锁。所有成员函数都会在调用时对数据上锁,结束时对数据解锁,这就保证了访问时数据不变量的状态稳定。
例子 (多线程插入容器会导致容器失效)
#include"baseinclude.h"
// 共享资源(列表)
std::list<int> shared_list;
std::mutex list_mutex;
// 不受保护的函数:多个线程同时访问和修改共享列表
void unprotected_add_to_list(int new_value) {
shared_list.push_back(new_value);
}
// 受保护的函数:使用互斥锁保护共享列表
void protected_add_to_list(int new_value) {
std::lock_guard<std::mutex> guard(list_mutex);
shared_list.push_back(new_value);
}
// 模拟线程执行的函数
void thread_function(int start_value, int count, bool use_protection) {
for (int i = 0; i < count; ++i) {
int value = start_value + i;
if (use_protection) {
protected_add_to_list(value);
}
else {
unprotected_add_to_list(value);
}
}
}
int main() {
const int num_threads = 20;
const int values_per_thread = 100*100;
// 受保护的情况
shared_list.clear(); // 清空列表
std::vector<std::thread> protected_threads;
for (int i = 0; i < num_threads; ++i) {
protected_threads.push_back(std::thread(thread_function, i * values_per_thread, values_per_thread, true));
}
for (auto& thread : protected_threads) {
thread.join();
}
std::cout << "Protected list size: " << shared_list.size() << std::endl; // 预期结果为 num_threads * values_per_thread
// 不受保护的情况
shared_list.clear(); // 第一次清空列表不会崩溃,因为容器是在受保护的情况下插入数据的,容器结构不会被破坏
std::vector<std::thread> unprotected_threads;
for (int i = 0; i < num_threads; ++i) {
unprotected_threads.push_back(std::thread(thread_function, i * values_per_thread, values_per_thread, false));
}
for (auto& thread : unprotected_threads) {
thread.join();
}
std::cout << "Unprotected list size: " << shared_list.size() << std::endl; // 预期结果可能小于 num_threads * values_per_thread
# shared_list.clear(); // 第二次清空列表会导致容器崩溃
return 0;
}
unprotected_add_to_list 代码不仅会导致 shared_list的size() 不一致,更会 对 shared_list 的结构 进行破坏
当然,情况并非总是如此理想:当其中一个成员函数返回的是受保护数据的指针或引用时,也会破坏数据。具有访问能力的指针或引用可以访问(并可能修改)受保护的数据,而不会受到互斥锁的限制。这就需要谨慎设计接口,确保互斥量能够锁住数据访问,并且不留后门。
3.2.2 保护共享数据
使用互斥量保护共享数据并非简单地在每个成员函数中添加 std::lock_guard
就能万事大吉。通过指针或引用“泄露”受保护数据,同样会使保护形同虚设。虽然检查指针和引用相对容易——只需确保成员函数不通过返回值或输出参数返回指向受保护数据的指针或引用——但更重要的是要全面考虑:
- 防止成员函数“泄露”: 仔细检查所有成员函数,确保它们不会将指向受保护数据的指针或引用传递给调用者。
- 防范外部访问: 除了自己编写的成员函数,还要注意是否有其他代码(尤其是你无法控制的代码)可能通过指针或引用的方式访问你的数据。即使函数本身没有在互斥量保护区域内存储指针或引用,也可能存在风险。
- 避免将保护数据作为运行时参数传递: 像代码3.2那样,将受保护数据作为参数传递给用户提供的函数,会留下可乘之机。
代码 3.2 无意中传递了保护数据的引用
C++
#include <iostream>
#include <string>
#include <thread>
#include <chrono>
#include <mutex>
#include <vector>
class SomeData {
int a;
std::string b;
public:
SomeData() : a(0), b("") {
}
void do_something(const std::string& threadName) {
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟耗时操作
a++;
b += "1";
std::cout << threadName << " - Thread ID: " << std::this_thread::get_id() << ", a: " << a << ", b: " << b << std::endl;
}
void print_data() const {
std::cout << "Current Data - a: " << a << ", b: " << b << std::endl;
}
};
SomeData* unprotected = nullptr;
class DataWrapper {
private:
SomeData data;
std::mutex m;
public:
template<typename Function>
void process_data(Function func, const std::string& threadName) {
std::lock_guard<std::mutex> lock(m); // 确保在作用域内持有锁
func(data, threadName);
}
void access_unprotected_data(const std::string& threadName) {
std::lock_guard<std::mutex> lock(m); // 在持有锁的情况下访问
if (unprotected) {
unprotected->do_something(threadName + " - Unsafe Access");
}
}
};
void malicious_function(SomeData& protected_data, const std::string& threadName) {
unprotected = &protected_data; // 将受保护的数据暴露给全局指针
std::cout << threadName << " - Exposing protected data to global pointer." << std::endl;
}
void foo_protected(DataWrapper& x, const std::string& threadName) {
x.process_data(malicious_function, threadName + " - Protected"); // 调用恶意函数,但保持锁的持有
x.access_unprotected_data(threadName + " - Protected"); // 安全地访问数据
}
void foo_unprotected(DataWrapper& x, const std::string& threadName) {
x.process_data(malicious_function, threadName + " - Unprotected"); // 调用恶意函数
if (unprotected) {
unprotected->do_something(threadName + " - Unprotected"); // 不安全地访问数据
}
}
int main() {
DataWrapper x;
std::cout << "--- Protected Access ---" << std::endl;
std::vector<std::thread> protected_threads;
for (int i = 0; i < 2; ++i) {
protected_threads.emplace_back(foo_protected, std::ref(x), std::to_string(i));
}
for (auto& t : protected_threads) {
t.join();
}
x.process_data([](SomeData& data, const std::string&) {
data.print_data(); }, "Final");
std::cout << "\n--- Unprotected Access (Data Race) ---" << std::endl;
std::vector<std::thread> unprotected_threads;
for (int i = 0; i < 2; ++i) {
unprotected_threads.emplace_back(foo_unprotected, std::ref(x), std::to_string(i));
}
for (auto& t : unprotected_threads) {
t.join();
}
x.process_data([](SomeData& data, const std::string&) {
data.print_data(); }, "Final");
return 0;
}
这段代码看似使用了 std::lock_guard
进行了保护,但问题在于 process_data
函数将受保护的 data
传递给了用户提供的函数 func
①。这就导致 foo
函数可以绕过保护机制,将恶意函数 malicious_function
传递进去 ②,从而在没有锁定互斥量的情况下访问 do_something()
③。
核心问题: 这段代码的问题在于,它只是“表面上”保护了数据结构,而没有真正限制对数据的访问。foo()
函数中调用 unprotected->do_something()
的代码本质上是在无保护的状态下访问共享数据。
解决之道: 为了真正保护共享数据,务必遵守以下原则:永远不要将受保护数据的指针或引用传递到互斥锁作用域之外!
1. SomeData
类
这个类代表共享的数据资源。它包含两个成员变量:一个整数 a
和一个字符串 b
。
- 构造函数:初始化
a
为0,b
为空字符串。 do_something
方法:模拟耗时操作(通过线程休眠),然后对数据进行修改,并输出当前线程ID和修改后的数据状态。print_data
方法:打印当前的数据状态。
class SomeData {
int a;
std::string b;
public:
SomeData() : a(0), b("") {
}
void do_something(const std::string& threadName) {
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟耗时操作
a++;
b += "1";
std::cout << threadName << " - Thread ID: " << std::this_thread::get_id() << ", a: " << a << ", b: " << b << std::endl;
}
void print_data() const {
std::cout << "Current Data - a: " << a << ", b: " << b << std::endl;
}
};
2. 全局指针 unprotected
这是一个全局指针,指向 SomeData
类型的对象。用于演示不安全的数据访问。
SomeData* unprotected = nullptr;
3. DataWrapper
类
这个类封装了 SomeData
对象,并使用互斥量来保护共享数据,防止并发访问导致的数据竞争。
process_data
方法:接受一个函数对象作为参数,并在持有锁的情况下执行该函数,确保对共享数据的操作是线程安全的。access_unprotected_data
方法:在持有锁的情况下访问全局指针指向的数据,以展示如何安全地访问共享数据。
class DataWrapper {
private:
SomeData data;
std::mutex m;
public:
template<typename Function>
void process_data(Function func, const std::string& threadName) {
std::lock_guard<std::mutex> lock(m); // 确保在作用域内持有锁
func(data, threadName);
}
void access_unprotected_data(const std::string& threadName) {
std::lock_guard<std::mutex> lock(m); // 在持有锁的情况下访问
if (unprotected) {
unprotected->do_something(threadName + " - Unsafe Access");
}
}
};
4. malicious_function
函数
这个函数将受保护的数据暴露给全局指针 unprotected
,模拟恶意行为。
void malicious_function(SomeData& protected_data, const std::string& threadName) {
unprotected = &protected_data; // 将受保护的数据暴露给全局指针
std::cout << threadName << " - Exposing protected data to global pointer." << std::endl;
}
5. foo_protected
和 foo_unprotected
函数
这两个函数分别展示了安全和不安全的访问方式。
foo_protected
:调用malicious_function
并在持有锁的情况下访问数据,确保操作是线程安全的。foo_unprotected
:同样调用malicious_function
,但在不持有锁的情况下访问数据,展示数据竞争的风险。
void foo_protected(DataWrapper& x, const std::string& threadName) {
x.process_data(malicious_function, threadName + " - Protected"); // 调用恶意函数,但保持锁的持有
x.access_unprotected_data(threadName + " - Protected"); // 安全地访问数据
}
void foo_unprotected(DataWrapper& x, const std::string& threadName) {
x.process_data(malicious_function, threadName + " - Unprotected"); // 调用恶意函数
if (unprotected) {
unprotected->do_something(threadName + " - Unprotected"); // 不安全地访问数据
}
}
6. main
函数
主函数中创建多个线程来测试安全和不安全的访问方式,并输出最终的数据状态。
int main() {
DataWrapper x;
std::cout << "--- Protected Access ---" << std::endl;
std::vector<std::thread> protected_threads;
for (int i = 0; i < 2; ++i) {
protected_threads.emplace_back(foo_protected, std::ref(x), std::to_string(i));
}
for (auto& t : protected_threads) {
t.join();
}
x.process_data([](SomeData& data, const std::string&) {
data.print_data(); }, "Final");
std::cout << "\n--- Unprotected Access (Data Race) ---" << std::endl;
std::vector<std::thread> unprotected_threads;
for (int i = 0; i < 2; ++i) {
unprotected_threads.emplace_back(foo_unprotected, std::ref(x), std::to_string(i));
}
for (auto& t : unprotected_threads) {
t.join();
}
return 0;
}
总结
这段代码通过创建多个线程并使用不同的方法访问共享数据,展示了多线程编程中的同步问题。具体来说,它演示了如何使用互斥量保护共享资源,以及不使用互斥量可能导致的数据竞争问题。通过这种方式,可以帮助理解线程安全的重要性及其实际应用场景。
根据你提供的输出,我们可以详细解释每个部分的执行过程和结果。以下是结合输出对代码执行流程的详细解释:
输出解析
1. Protected Access (安全访问)
--- Protected Access ---
1 - Protected - Exposing protected data to global pointer.
1 - Protected - Unsafe Access - Thread ID: 2364, a: 1, b: 1
0 - Protected - Exposing protected data to global pointer.
0 - Protected - Unsafe Access - Thread ID: 12884, a: 2, b: 11
Current Data - a: 2, b: 11
-
线程 1:
- 调用
foo_protected
函数。 - 在
process_data
中调用了malicious_function
,将SomeData
对象暴露给全局指针unprotected
,并打印出"1 - Protected - Exposing protected data to global pointer."
。 - 然后在持有锁的情况下通过
access_unprotected_data
访问数据,并调用do_something
方法,打印出"1 - Protected - Unsafe Access - Thread ID: 2364, a: 1, b: 1"
。
- 调用
-
线程 0:
- 类似地,调用
foo_protected
函数。 - 在
process_data
中调用了malicious_function
,将SomeData
对象暴露给全局指针unprotected
,并打印出"0 - Protected - Exposing protected data to global pointer."
。 - 然后在持有锁的情况下通过
access_unprotected_data
访问数据,并调用do_something
方法,打印出"0 - Protected - Unsafe Access - Thread ID: 12884, a: 2, b: 11"
。
- 类似地,调用
-
最终状态:
- 最后,通过
x.process_data([](SomeData& data, const std::string&) { data.print_data(); }, "Final");
打印出当前的数据状态:"Current Data - a: 2, b: 11"
。
- 最后,通过
2. Unprotected Access (不安全访问)
--- Unprotected Access (Data Race) ---
0 - Unprotected - Exposing protected data to global pointer.
1 - Unprotected - Exposing protected data to global pointer.
1 - Unprotected - Thread ID: 3944, a: 0 - Unprotected - Thread ID: 4, b: 111112844, a: 4, b: 1111
-
线程 0 和线程 1:
- 调用
foo_unprotected
函数。 - 在
process_data
中调用了malicious_function
,将SomeData
对象暴露给全局指针unprotected
,并分别打印出"0 - Unprotected - Exposing protected data to global pointer."
和"1 - Unprotected - Exposing protected data to global pointer."
。
- 调用
-
并发问题:
- 这里没有使用互斥量来保