安全编码课程 实验7 并发

发布于:2025-04-17 ⋅ 阅读:(82) ⋅ 点赞:(0)

实验项目: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. 总结以上三种方式的优缺点

  1. 无同步机制的问题

    • 两个线程可能同时读取到余额为100
    • 都认为可以取款,导致最终余额为-100
    • 这是典型的竞争条件。
  2. atomic解决方案

    • compare_exchange_weak会在原子操作中比较并交换值

    • 如果其他线程修改了值,操作会失败并重试
  3. mutex解决方案

    • 使用锁确保同一时间只有一个线程能访问临界区
    • lock_guard在构造时加锁,析构时自动解锁

网站公告

今日签到

点亮在社区的每一天
去签到