多线程转账中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
已尝试的修改
- 为
getMoney()添加mutex:
double getMoney() { lock_guard lk(mMoneyLock); return mMoney; }
- 为
totalMoney()添加mutex:
double totalMoney() const { double sum = 0; lock_guard lk(mLock); for (auto a : mAccounts) { sum += a->getMoney(); } return sum; }
问题原因分析
- 转账操作原子性缺失:
transferMoney方法中,转出扣款和转入存款是两个独立操作。多线程场景下,线程A执行完转出扣款后,线程B可能立即读取账户余额计算总金额,此时转入操作尚未完成,导致总金额显示异常。 - 打印与转账的竞态条件:打印账户余额、总金额的操作,与后续转账操作之间无同步机制。即使单个方法加锁,打印的余额状态和实际执行转账时的状态已经不一致,且转账过程中总金额存在短暂中间态(扣款完成、存款未完成),此时读取总金额会得到错误值。
- 账户锁无全局协调:单个账户的锁仅保护自身余额修改,但跨账户转账时,两个账户的锁是独立获取的,无法保证整个转账流程的原子性,也无法避免其他线程读取中间状态。
修正方案
核心思路
确保转账操作的原子性(扣款+存款必须作为整体完成),同时保证余额读取、总金额计算与转账操作的同步,避免读取到中间态数据。
具体修改
- 为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方法中使用锁 };
- 完善Account类的getMoney方法,确保余额读取线程安全:
double getMoney() { lock_guard lk(mMoneyLock); return mMoney; }
- 同步打印与转账操作,避免状态不一致:
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
相关产品推荐
相关产品推荐

