C ++如何在银行账户转账时避免竞争条件

C++ How to avoid race conditions when transfering money at bank accounts

我有点卡在这里..... 我想从一个银行账户转账到另一个银行账户。有一群用户,每个用户都是一个线程,在银行账户上进行一些交易。 我尝试了不同的解决方案,但似乎在进行交易时总是会导致竞争条件。我的代码是这个:

#include <mutex>


class Account
{
private:

    std::string name_;

    unsigned int balance_;

    std::mutex classMutex_;

public:
Account(std::string name, unsigned int balance);
virtual ~Account();

void makePayment_sync(unsigned int payment);
void takeMoney_sync(unsigned int payout);
void makeTransaction_sync(unsigned int transaction, Account& toAccount);

};



unsigned int Account::getBalance_sync()
{
    std::lock_guard<std::mutex> guard(classMutex_);
    return balance_;
}

void Account::makePayment_sync(unsigned int payment)
{
    std::lock_guard<std::mutex> guard(classMutex_);
    balance_ += payment;
}

void Account::takeMoney_sync(unsigned int payout)
{
    std::lock_guard<std::mutex> guard(classMutex_);
    balance_ -= payout;
}

void Account::makeTransaction_sync(unsigned int transaction, Account& toAccount)
{

    std::lock_guard<std::mutex> lock(classMutex_);
    this->balance_ -= transaction;
    toAccount.balance_ += transaction; 

}

注意:我将这些方法称为 foo_sync 是因为还应该存在结果应显示竞争条件的情况。 但是,是的,我有点被困在这里......也尝试了这种方法,我在其中创建了一个新的互斥锁:mutex_

class Account
    {
private:

    std::string name_;

    unsigned int balance_;

    std::mutex classMutex_, mutex_;

...

void Account::makeTransaction_sync(unsigned int transaction, Account& toAccount)
{
    std::unique_lock<std::mutex> lock1(this->mutex_, std::defer_lock);
    std::unique_lock<std::mutex> lock2(toAccount.mutex_, std::defer_lock);

    // lock both unique_locks without deadlock
    std::lock(lock1, lock2);

    this->balance_ -= transaction;
    toAccount.balance_ += transaction;

}

但是我在运行时遇到了一些奇怪的错误!任何suggestions/hints/ideas解决这个问题!提前致谢:)

好的,我认为这是您 class 的合理起点。

这不是唯一的方法,但我在项目中采用了一些原则。请参阅内嵌评论以获取解释。

这是一个完整的例子。对于 clang/gcc 编译和 运行 使用:

c++ -o payment -O2 -std=c++11 payment.cpp && ./payment

如果您需要进一步说明,请随时询问:

#include <iostream>
#include <mutex>
#include <cassert>
#include <stdexcept>
#include <thread>
#include <vector>
#include <random>

class Account
{
    using mutex_type = std::mutex;
    using lock_type = std::unique_lock<mutex_type>;

    std::string name_;

    int balance_;

    // mutable because we'll want to be able to lock a const Account in order to get a balance
    mutable mutex_type classMutex_;


public:
    Account(std::string name, int balance)
    : name_(std::move(name))
    , balance_(balance)
    {}

    // public interface takes a lock and then defers to internal interface
    void makePayment(int payment) {
        auto lock = lock_type(classMutex_);
        modify(lock, payment);
    }

    void takeMoney(int payout) {
        makePayment(-payout);
    }

    int balance() const {
        auto my_lock = lock_type(classMutex_);
        return balance_;
    }

    void transfer_to(Account& destination, int amount)
    {
        // try/catch in case one part of the transaction threw an exception.
        // we don't want to lose money in such a case
        try {
            std::lock(classMutex_, destination.classMutex_);
            auto my_lock = lock_type(classMutex_, std::adopt_lock);
            auto his_lock = lock_type(destination.classMutex_, std::adopt_lock);

            modify(my_lock, -amount);
            try {
                destination.modify(his_lock, amount);
            } catch(...) {
                modify(my_lock, amount);
                std::throw_with_nested(std::runtime_error("failed to transfer into other account"));
            }
        } catch(...) {
            std::throw_with_nested(std::runtime_error("failed to transfer from my account"));
        }
    }

    // provide a universal write
    template<class StreamType>
    StreamType& write(StreamType& os) const {
        auto my_lock = lock_type(classMutex_);
        return os << name_ << " = " << balance_;
    }

private:
    void modify(const lock_type& lock, unsigned int amount)
    {
        // for internal interfaces where the mutex is expected to be locked,
        // i like to pass a reference to the lock.
        // then I can assert that all preconditions are met

        // precondition 1 : the lock is active
        assert(lock.owns_lock());

        // precondition 2 : the lock is actually locking our mutex
        assert(lock.mutex() == &classMutex_);

        balance_ += amount;
    }

};

// public overload or ostreams, loggers etc
template<class StreamType>
StreamType& operator<<(StreamType& os, const Account& a) {
    return a.write(os);
}

void blip()
{
    using namespace std;
    static mutex m;
    lock_guard<mutex> l(m);
    cout << '.';
    cout.flush();
}


// a test function to peturb the accounts
void thrash(Account& a, Account& b)
{
    auto gen = std::default_random_engine(std::random_device()());
    auto amount_dist = std::uniform_int_distribution<int>(1, 20);
    auto dist = std::uniform_int_distribution<int>(0, 1);
    for (int i = 0 ; i < 10000 ; ++i)
    {
        if ((i % 1000) == 0)
            blip();

        auto which = dist(gen);
        auto amount = amount_dist(gen);

        // make sure we transfer in both directions in order to
        // cause std::lock() to resolve deadlocks

        if (which == 0)
        {
            b.takeMoney(1);
            a.transfer_to(b, amount);
            a.makePayment(1);
        }
        else {
            a.takeMoney(1);
            b.transfer_to(a, amount);
            b.makePayment(1);
        }
    }
}


auto main() -> int
{
    using namespace std;
    Account a("account 1", 100);
    Account b("account 2", 0);

    cout << "a : " << a << endl;
    cout << "b : " << b << endl;

    // thrash 50 threads to give it a thorough test
    vector<thread> threads;
    for(int i = 0 ; i < 50 ; ++i) {
        threads.emplace_back(std::bind(thrash, ref(a), ref(b)));
    }
    for (auto& t : threads) {
        if (t.joinable())
            t.join();
    }

    cout << endl;

    cout << "a : " << a << endl;
    cout << "b : " << b << endl;

    // check that no money was lost
    assert(a.balance() + b.balance() == 100);

    return 0;
}

示例输出:

a : account 1 = 100
b : account 2 = 0
....................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................
a : account 1 = 7338
b : account 2 = -7238