账户转账加锁问题

问题描述

  银行账户的转账问题需要保证转账业务的原子性操作,所以必须对转账操作进行加锁。但是,每一笔转账业务需要同时对两个账户进行加锁,那么可能出现死锁现象。比如,一个线程正在从A转账给B,另一个线程从B转账给A,当第一个线程给A加锁的同时,第二个线程给B加锁了,那么就会出现死锁现象。

死锁的条件

  1.互斥访问:对共享资源的操作具有互斥性,每次只能被一个线程占用。
  2.不可抢占:对于共享资源,当被一个线程占用时,其他线程不会抢占已被占用的资源。
  3.请求和保持:线程申请了一部分资源之后,申请新资源时候不会释放已经分配到的资源。
  4.循环等待:多个线程之间循环等待对方已经保有的资源,导致资源占用申请形成环路。

解决方案

  为了解决银行转账过程中可能出现的死锁问题,我们可以通过破坏死锁的条件来防止死锁的发生。对于转账问题来说,第一个和第二个条件属于共享资源的特性不可能改变,因此我们可以从后两个条件出发:指定加锁顺序防止循环等待条件;通过尝试加锁的方式破坏请求和保持条件。

指定加锁顺序

  我们可以通过特定的方式指定加锁顺序。这里我们创建一个账户类,类主要的成员变量为自身的互斥锁和账户余额。同时,为了方便指定加锁顺序,我们给类添加一个静态变量,为互斥锁指定id,那么每次我们加锁的时候按照互斥锁id从小到大的顺序进行加锁。

代码实现

  账户类的实现:

#include <mutex>
#include <map>
#include <iostream>
#include <unistd.h>
#include <chrono>

//通过确定加锁顺序的方式完成加锁
class Balance{
private:
    int balance;
    std::mutex mtx;
    static std::map<std::mutex*,int> lockMap;   //给每个对象的锁记录ID
    static time_t startTime;                    //初始时间,为了让每个线程能够尽可能在同一时间进行转账操作
public:
    Balance(int balance=0):balance(balance){
        lockMap[&mtx]=lockMap.size();           //暂时不考虑对lockMap的访问线程安全问题
    }
    ~Balance(){
        lockMap.erase(&mtx);
    }
    bool deposit(int money){
        balance += money;
        return true;
    }
    bool withdraw(int money){
        if(balance<money)
            return false;
        balance -= money;
        return true;
    }
    int getBalance(){
        return balance;
    }
    static void transfer(Balance& from,Balance& to,int money){
        time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
        //延时等待5秒后,所有的线程都开始执行转账操作
        while(now-startTime<=5){
            usleep(100);
            now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
        }
        std::cout<<"Try transfer "<<money<<" from "<<from.getBalance()<<" to "<<to.getBalance()<<std::endl;
        int formLock=lockMap[&from.mtx];
        int toLock=lockMap[&to.mtx];
        //通过加锁顺序来避免死锁
        if(formLock<toLock){
            std::lock_guard<std::mutex> lock1(from.mtx);
            std::lock_guard<std::mutex> lock2(to.mtx);
            if(from.withdraw(money)&&to.deposit(money)){
                std::cout<<"Transfer success !"<<std::endl;
            }
        }else{
            std::lock_guard<std::mutex> lock1(to.mtx);
            std::lock_guard<std::mutex> lock2(from.mtx);
            if(from.withdraw(money)&&to.deposit(money)){
                std::cout<<"Transfer success !"<<std::endl;
            }
        }
        std::cout<<"now: from balance "<<from.getBalance()<<" , to balance"<<to.getBalance()<<std::endl;
    }
};
std::map<std::mutex*,int> Balance::lockMap;
time_t Balance::startTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());

代码测试

  使用多个线程在同一时刻进行转账操作,代码如下所示:

#include "balance.h"
#include <thread>
#include <vector>
#include <iostream>

int main(){
    Balance a(100);
    Balance b(100);
    std::thread t1(Balance::transfer,std::ref(a),std::ref(b),10);   //a转账10给b
    std::thread t2(Balance::transfer,std::ref(b),std::ref(a),20);   //b转账20给10
    t1.join();
    t2.join();
    std::cout<<"a balance "<<a.getBalance()<<std::endl;             //最终结果,a剩余110
    std::cout<<"b balance "<<b.getBalance()<<std::endl;             //最终结果,b剩余90
    return 0;
}

  测试结果:

Try transfer 10 from 100 to 100
Transfer success !
now: from balance 90 , to balance110
Try transfer 20 from 110 to 90
Transfer success !
now: from balance 90 , to balance110
a balance 110
b balance 90

使用尝试加锁

  C++11中的unique_lock可以使用try_lock()方法对互斥锁进行尝试加锁,我们可是通过尝试加锁的方式,破坏请求和保持的条件。只有当一个线程同时拿到两个账户的锁时,才进行转账操作,否则不会对其中任意一个账户进行加锁。

代码实现

  账户类的实现:

#include <mutex>
#include <map>
#include <iostream>
#include <unistd.h>
#include <chrono>

// 通过尝试加锁的方式实现加锁
class Balance{
private:
    int balance;
    std::mutex mtx;
    static time_t startTime;
public:
    Balance(int balance = 0):balance(balance){}
    bool deposit(int money){
        balance += money;
        return true;
    }
    bool withdraw(int money){
        if(balance<money)
            return false;
        balance -= money;
        return true;
    }
    int getBalance(){
        return balance;
    }
    static void transfer(Balance& from,Balance& to,int money){
        time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
        //延时等待5秒后,所有的线程都开始执行转账操作
        while(now-startTime<=5){
            usleep(100);
            now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
        }
        std::cout<<"Try transfer "<<money<<" from "<<from.getBalance()<<" to "<<to.getBalance()<<std::endl;
        std::unique_lock<std::mutex> lock1(from.mtx,std::defer_lock);
        std::unique_lock<std::mutex> lock2(to.mtx,std::defer_lock);
        //尝试加锁,如果能够同时获得两把锁那么继续执行转账操作
        while(true){
            if(lock1.try_lock()&&lock2.try_lock()){
                if(from.withdraw(money)&&to.deposit(money)){
                    std::cout<<"Transfer success !"<<std::endl;
                }
                break;
            }
            //等待100ms后再次尝试加锁
            usleep(100);
        }
        std::cout<<"now: from balance "<<from.getBalance()<<" , to balance"<<to.getBalance()<<std::endl;
    }
};

代码测试

  使用多个线程在同一时刻进行转账操作,代码如下所示:

#include "balance.h"
#include <thread>
#include <vector>
#include <iostream>

int main(){
    Balance a(100);
    Balance b(100);
    std::thread t1(Balance::transfer,std::ref(a),std::ref(b),10);   //a转账10给b
    std::thread t2(Balance::transfer,std::ref(b),std::ref(a),20);   //b转账20给10
    t1.join();
    t2.join();
    std::cout<<"a balance "<<a.getBalance()<<std::endl;             //最终结果,a剩余110
    std::cout<<"b balance "<<b.getBalance()<<std::endl;             //最终结果,b剩余90
    return 0;
}

  测试结果:

Try transfer 20 from 100 to 100
Transfer success !
now: from balance 80 , to balance120
Try transfer 10 from 120 to 80
Transfer success !
now: from balance 110 , to balance90
a balance 110
b balance 90

当珍惜每一片时光~