You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程转账中totalMoney计算值异常的原因及修正方案

C++多线程转账总金额异常问题分析与修复

问题背景

在以下C++多线程转账代码中,理论上银行总金额totalMoney应始终保持为200,但实际运行时该值有时会小于200。已尝试为Account类的getMoney()方法添加mutex,以及为Bank类的totalMoney()方法添加mutex,但问题仍未解决。

原代码

#include <condition_variable>
#include <iostream>
#include <mutex>
#include <set>
#include <thread>

using namespace std;

class Account {
public:
  Account(string name, double money): mName(name), mMoney(money) {};

public:
  void changeMoney(double amount) {
    unique_lock lock(mMoneyLock);
    mConditionVar.wait(lock, [this, amount] {
      return mMoney + amount > 0;
    });
    mMoney += amount;
    mConditionVar.notify_all();
  }

  string getName() {
    return mName;
  }

  double getMoney() {
    return mMoney;
  }

private:
  string mName;
  double mMoney;
  mutex mMoneyLock;
  condition_variable mConditionVar;
};

class Bank {
public:
  void addAccount(Account* account) {
    mAccounts.insert(account);
  }

  void transferMoney(Account* accountA, Account* accountB, double amount) {
    accountA->changeMoney(-amount);
    accountB->changeMoney(amount);
  }

  double totalMoney() const {
    double sum = 0;
    for (auto a : mAccounts) {
      sum += a->getMoney();
    }
    return sum;
  }

private:
  set<Account*> mAccounts;
};

mutex sCoutLock;
void randomTransfer(Bank* bank, Account* accountA, Account* accountB) {
  while(true) {
    double randomMoney = ((double)rand() / RAND_MAX) * 100;
    {
      lock_guard guard(sCoutLock);
      cout << "Try to Transfer " << randomMoney
           << " from " << accountA->getName() << "(" << accountA->getMoney()
           << ") to " << accountB->getName() << "(" << accountB->getMoney()
           << "), Bank totalMoney: " << bank->totalMoney() << endl;
    }
    bank->transferMoney(accountA, accountB, randomMoney);
  }
}

int main() {
  Account a("Paul", 100);
  Account b("Moira", 100);

  Bank aBank;
  aBank.addAccount(&a);
  aBank.addAccount(&b);

  thread t1(randomTransfer, &aBank, &a, &b);
  thread t2(randomTransfer, &aBank, &b, &a);

  t1.join();
  t2.join();

  return 0;
}

运行异常输出示例

0x16be37000Try to Transfer 51.0047 from Moira(135.637) to Paul(64.363), Bank totalMoney: 200
0x16bdab000Try to Transfer 36.512 from Paul(3.19498) to Moira(196.805), Bank totalMoney: 148.995
0x16bdab000Try to Transfer 97.5377 from Paul(17.6877) to Moira(182.312), Bank totalMoney: 200
...
0x16bdab000Try to Transfer 88.4315 from Paul(37.5622) to Moira(66.5252), Bank totalMoney: 104.087

已尝试的修改

  1. 为getMoney()添加mutex:
double getMoney() {
  lock_guard lk(mMoneyLock);
  return mMoney;
}
  1. 为totalMoney()添加mutex:
double totalMoney() const {
  double sum = 0;
  lock_guard lk(mLock);
  for (auto a : mAccounts) {
    sum += a->getMoney();
  }
  return sum;
}

问题原因分析

  1. 转账操作原子性缺失:transferMoney方法中,转出扣款和转入存款是两个独立操作。多线程场景下,线程A执行完转出扣款后,线程B可能立即读取账户余额计算总金额,此时转入操作尚未完成,导致总金额显示异常。
  2. 打印与转账的竞态条件:打印账户余额、总金额的操作,与后续转账操作之间无同步机制。即使单个方法加锁,打印的余额状态和实际执行转账时的状态已经不一致,且转账过程中总金额存在短暂中间态(扣款完成、存款未完成),此时读取总金额会得到错误值。
  3. 账户锁无全局协调:单个账户的锁仅保护自身余额修改,但跨账户转账时,两个账户的锁是独立获取的,无法保证整个转账流程的原子性,也无法避免其他线程读取中间状态。

修正方案

核心思路

确保转账操作的原子性(扣款+存款必须作为整体完成),同时保证余额读取、总金额计算与转账操作的同步,避免读取到中间态数据。

具体修改

  1. 为Bank类添加全局锁,保护转账操作和总金额计算的原子性:
class Bank {
public:
  void addAccount(Account* account) {
    lock_guard lock(mBankLock);
    mAccounts.insert(account);
  }

  void transferMoney(Account* accountA, Account* accountB, double amount) {
    lock_guard lock(mBankLock); // 全局锁保证转账操作原子性
    accountA->changeMoney(-amount);
    accountB->changeMoney(amount);
  }

  double totalMoney() const {
    lock_guard lock(mBankLock); // 全局锁保证计算总金额时无转账操作
    double sum = 0;
    for (auto a : mAccounts) {
      sum += a->getMoney();
    }
    return sum;
  }

  mutex& getBankLock() { // 提供锁的访问接口,用于同步打印与转账
    return mBankLock;
  }

private:
  set<Account*> mAccounts;
  mutable mutex mBankLock; // mutable允许const方法中使用锁
};
  1. 完善Account类的getMoney方法,确保余额读取线程安全:
double getMoney() {
  lock_guard lk(mMoneyLock);
  return mMoney;
}
  1. 同步打印与转账操作,避免状态不一致:
void randomTransfer(Bank* bank, Account* accountA, Account* accountB) {
  while(true) {
    double randomMoney = ((double)rand() / RAND_MAX) * 100;
    {
      lock_guard bankGuard(bank->getBankLock()); // 先获取银行锁,保证打印和转账的原子性
      lock_guard guard(sCoutLock);
      cout << "Try to Transfer " << randomMoney
           << " from " << accountA->getName() << "(" << accountA->getMoney()
           << ") to " << accountB->getName() << "(" << accountB->getMoney()
           << "), Bank totalMoney: " << bank->totalMoney() << endl;
      bank->transferMoney(accountA, accountB, randomMoney);
    }
    this_thread::sleep_for(chrono::milliseconds(10)); // 可选,降低打印频率便于观察
  }
}

关键说明

  • 银行全局锁:通过全局锁将转账的扣款、存款操作绑定为原子操作,同时保证计算总金额时无任何转账操作执行,彻底避免中间态读取。
  • 同步打印与转账:将打印和转账放在同一锁保护的代码块中,确保打印的余额就是转账执行前的真实状态,消除竞态条件。
  • 账户锁保留:单个账户的锁用于保护自身余额修改的线程安全,配合全局锁实现多层同步,避免细粒度锁的冲突问题。

内容的提问来源于stack exchange,提问作者yongjay

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 07:54:56