安全编码课程 实验7 并发
实验项目:C++ 多线程中的数据竞争与同步机制
实验要求:
1. 编写基础代码:模拟账户余额取款
创建一个全局共享变量 int balance = 100,表示初始余额;
创建两个线程 Thread A 和 Thread B,尝试各自取出 100 元;
不使用任何同步机制,观察两个线程是否可能都成功取款(产生负余额);
输出每个线程的操作过程和最终余额。
2. 引入 std::atomic<int> 实现原子性控制
使用 std::atomic<int> 声明余额变量balance;
使用 compare_exchange_weak 实现原子性“读取余额并扣款”的逻辑;
再次运行程序,观察是否仍然出现竞争问题。
3. 使用 std::mutex 锁住临界区
改用普通变量 int balance,但在关键操作处加锁(std::lock_guard<std::mutex> lock(mtx);),确保线程间互斥
再次运行程序,观察是否仍然出现竞争问题。
4. 总结以上三种方式的优缺点
实验步骤、实验结果及结果分析:
1. 编写基础代码:模拟账户余额取款
创建一个全局共享变量 int balance = 100,表示初始余额;
创建两个线程 Thread A 和 Thread B,尝试各自取出 100 元;
不使用任何同步机制,观察两个线程是否可能都成功取款(产生负余额);
输出每个线程的操作过程和最终余额。
创建一个全局共享变量 int balance = 100,表示初始余额;
int balance = 100; // 全局共享变量
创建两个线程 Thread A 和 Thread B,尝试各自取出 100 元;
std::thread thread_a(withdraw, "Thread A", 100);
std::thread thread_b(withdraw, "Thread B", 100);
不使用任何同步机制,观察两个线程是否可能都成功取款(产生负余额);
输出每个线程的操作过程和最终余额。
#include <iostream>
#include <thread>
#include <chrono>
int balance = 100; // 全局共享变量
void withdraw(const std::string& thread_name, int amount) {
if (balance >= amount) {
std::cout << thread_name << ": 当前余额 " << balance << " 元,尝试取款 " << amount << " 元\n";
balance -= amount;
std::cout << thread_name << ": 取款成功,新余额 " << balance << " 元\n";
} else {
std::cout << thread_name << ": 余额不足,取款失败\n";
}
}
int main() {
std::cout << "初始余额: " << balance << " 元\n";
std::thread thread_a(withdraw, "Thread A", 100);
std::thread thread_b(withdraw, "Thread B", 100);
thread_a.join();
thread_b.join();
std::cout << "最终余额: " << balance << " 元\n";
return 0;
}
编译多线程程序时链接 pthread 库 :
g++ 1.cpp -o 1 -pthread
竞争条件导致负余额:
2. 引入 std::atomic<int> 实现原子性控制
使用 std::atomic<int> 声明余额变量balance;
使用 compare_exchange_weak 实现原子性“读取余额并扣款”的逻辑;
再次运行程序,观察是否仍然出现竞争问题。
std::atomic<int> balance(100); // 原子变量
void withdraw(const std::string& thread_name, int amount) {
int expected = balance.load();
while (expected >= amount &&
!balance.compare_exchange_weak(expected, expected - amount)) {
}
if (expected >= amount) {
std::cout << thread_name << ": 取款成功,新余额 " << balance.load() << " 元\n";
} else {
std::cout << thread_name << ": 余额不足,取款失败\n";
}
}
3. 使用 std::mutex 锁住临界区
改用普通变量 int balance,但在关键操作处加锁(std::lock_guard<std::mutex> lock(mtx);),确保线程间互斥
再次运行程序,观察是否仍然出现竞争问题。
int balance = 100; // 普通变量
std::mutex mtx; // 互斥锁
void withdraw(const std::string& thread_name, int amount) {
std::lock_guard<std::mutex> lock(mtx); // 自动加锁解锁
if (balance >= amount) {
balance -= amount;
std::cout << thread_name << ": 取款成功,新余额 " << balance << " 元\n";
} else {
std::cout << thread_name << ": 余额不足,取款失败\n";
}
}
4. 总结以上三种方式的优缺点
-
无同步机制的问题:
- 两个线程可能同时读取到余额为100
- 都认为可以取款,导致最终余额为-100
- 这是典型的竞争条件。
-
atomic解决方案:
-
compare_exchange_weak会在原子操作中比较并交换值
- 如果其他线程修改了值,操作会失败并重试
-
-
mutex解决方案:
- 使用锁确保同一时间只有一个线程能访问临界区
- lock_guard在构造时加锁,析构时自动解锁