如何用C++原子操作解决银行账户多线程临界区重复计息问题?
解决原子操作下重复添加利息的问题
你的问题核心在于计数器递增与触发条件检查的非原子性:多个线程可能同时看到m_iCounter为99,各自执行++m_iCounter后都得到100,进而都进入addInterest函数,导致利息被重复添加。用原子操作解决的关键是让“递增计数器+判断是否触发利息”这一逻辑成为原子不可分割的步骤,或者在addInterest内做二次校验确保只有一个线程执行利息添加。
方案一:从源头阻止多线程进入addInterest(推荐)
利用原子类型的fetch_add方法,它会原子性地将计数器加1并返回递增前的旧值。通过判断旧值+1是否为100的倍数,就能确保只有第一个将计数器推到100倍数的线程触发利息添加:
// 确保m_iCounter是std::atomic<int>类型 void transfer(Account& acc, int amount, string& thread) { int old_counter = m_iCounter.fetch_add(1); // 原子递增,返回递增前的值 withdraw(acc, amount); // 只有当递增后的值是100的倍数时,才执行利息添加 if ((old_counter + 1) % 100 == 0) { addInterest(thread); } }
这个方案从根源上避免了多线程同时进入addInterest的问题,效率最高。
方案二:在addInterest内做二次校验(兼容现有transfer逻辑)
如果不想修改transfer的结构,可以在addInterest内部通过一个原子标记,确保同一批次的利息只会被添加一次:
void addInterest(string& thread) { // 计算当前应该触发利息的计数器阈值(比如100、200...) int current_batch = (m_iCounter.load() / 100) * 100; // 用静态原子变量记录已经处理过的最后一个批次 static std::atomic<int> last_processed_batch = 0; int expected = last_processed_batch.load(); // 原子性地尝试更新批次标记,只有第一个线程能成功 while (current_batch > expected && !last_processed_batch.compare_exchange_weak(expected, current_batch)) { // 循环直到获取到最新的last_processed_batch值 } // 如果当前批次已经被其他线程处理过,直接返回 if (current_batch != last_processed_batch) { return; } // 以下是原有的利息添加逻辑,确保m_iBalance是std::atomic<float> float old = m_iBalance.load(); const float interest = 0.05f; const float amount = old * interest; while (!m_iBalance.compare_exchange_weak(old, old + amount)) { // 自旋直到CAS成功 } // 确保m_iInterest也是原子类型,避免计数错误 m_iInterest.fetch_add(1); cout << thread << " interest : Acc" << name << " " << m_iCounter << endl; }
额外注意事项
- 所有共享变量(
m_iCounter、m_iBalance、m_iInterest)都必须声明为对应的原子类型(std::atomic<int>、std::atomic<float>),否则仍会存在线程安全问题。 std::atomic<float>的CAS操作在部分平台可能存在精度问题,如果对精度要求高,可以考虑将余额转换为整数(比如以分为单位存储),再用std::atomic<long long>处理,避免浮点数的原子操作风险。
内容的提问来源于stack exchange,提问作者Ettur




