10

在将钱从一个银行帐户转移到另一个帐户的经典问题中,公认的解决方案(我相信)是将互斥锁与每个银行帐户相关联,然后在从一个帐户提取资金并将其存入另一个帐户之前锁定两者。乍一看,我会这样做:

class Account {
public:
  void deposit(const Money& amount);
  void withdraw(const Money& amount);
  void lock() { m.lock(); }
  void unlock() { m.unlock(); }

private:
  std::mutex m;
};

void transfer(Account& src, Account& dest, const Money& amount)
{
  src.lock();
  dest.lock();

  src.withdraw(amount);
  dest.deposit(amount);

  dest.unlock();
  src.unlock();
}

但是手动解锁有异味。我可以公开互斥体,然后使用std::lock_guardin transfer,但公共数据成员也有味道。

的要求std::lock_guard是它的类型满足BasicLockablelock的要求,也就是调用unlock有效。Account满足该要求,因此我可以直接使用std::lock_guardwith Account

void transfer(Account& src, Account& dest, const Money& amount)
{
  std::lock_guard<Account> g1(src);
  std::lock_guard<Account> g2(dest);

  src.withdraw(amount);
  dest.deposit(amount);
}

这似乎没问题,但我以前从未见过这种事情,并且复制互斥锁的锁定和解锁Account本身似乎有点臭。

在这种情况下,将互斥锁与其保护的数据相关联的最佳方法是什么?

更新:在下面的评论中,我注意到它std::lock可以用来避免死锁,但我忽略了它std::lock依赖于try_lock功能的存在(除了 forlockunlock)。添加try_lockAccount的界面似乎是一个相当严重的黑客攻击。因此,如果Account要保留对象的互斥锁Account,它似乎必须是公共的。这有相当的恶臭。

一些提议的解决方案让客户使用包装器类将互斥锁与Account对象静默关联,但是,正如我在评论中指出的那样,这似乎使代码的不同部分可以轻松使用不同的包装器对象Account,每个都创建自己的互斥体,这意味着代码的不同部分可能会尝试Account使用不同的互斥体来锁定。那很糟。

其他提议的解决方案依赖于一次仅锁定一个互斥锁。这消除了锁定多个互斥体的需要,但代价是某些线程可能看到系统的不一致视图。从本质上讲,这放弃了涉及多个对象的操作的事务语义。

在这一点上,公共互斥锁开始看起来像是可用选项中最不臭的,这是我真的不想得出的结论。真的没有更好的了吗?

4

8 回答 8

5

查看Herb Sutter在C++ 和 Beyond 2012上的演讲:C++ 并发。他展示了 C++11 中类似Monitor Object的实现示例。

monitor<Account> m[2];
transaction([](Account &x,Account &y)
{
    // Both accounts are automaticaly locked at this place.
    // Do whatever operations you want to do on them.
    x.money-=100;
    y.money+=100;
},m[0],m[1]);
// transaction - is variadic function template, it may accept many accounts

执行:

现场演示

#include <iostream>
#include <utility>
#include <ostream>
#include <mutex>

using namespace std;

typedef int Money;

struct Account
{
    Money money = 1000;
    // ...
};

template<typename T>
T &lvalue(T &&t)
{
    return t;
}

template<typename T>
class monitor
{
    mutable mutex m;
    mutable T t;
public:
    template<typename F>
    auto operator()(F f) const -> decltype(f(t))
    {
        return lock_guard<mutex>(m),
               f(t);
    }
    template<typename F,typename ...Ts> friend
    auto transaction(F f,const monitor<Ts>& ...ms) ->
        decltype(f(ms.t ...))
    {
        return lock(lvalue(unique_lock<mutex>(ms.m,defer_lock))...),
        f(ms.t ...);
    }
};

int main()
{
    monitor<Account> m[2];

    transaction([](Account &x,Account &y)
    {
        x.money-=100;
        y.money+=100;
    },m[0],m[1]);

    for(auto &&t : m)
        cout << t([](Account &x){return x.money;}) << endl;
}

输出是:

900
1100
于 2013-04-06T01:06:00.710 回答
1

让钱“在外流”一段时间并没有错。让它像这样:

Account src, dst;

dst.deposit(src.withdraw(400));

现在只需使每个单独的方法线程安全,例如

int Account::withdraw(int n)
{
    std::lock_guard<std::mutex> _(m_);
    balance -= n;
    return n;
}
于 2013-04-05T23:12:51.437 回答
1

我更喜欢使用非侵入式包装类,而不是使用互斥锁污染原始对象并在每个方法调用上锁定它。这个包装类(我命名为Protected<T>)包含用户对象作为私有变量。Protected<T>将友谊授予另一个名为Locker<T>. 储物柜将包装器作为其构造函数参数,并为用户对象提供公共访问器方法。locker 还保持包装器的互斥锁在其生命周期内处于锁定状态。因此,locker 的生命周期定义了一个可以安全访问原始对象的范围。

Protected<T>可以实现operator->快速调用单个方法。

工作示例:

#include <iostream>
#include <mutex>


template<typename>
struct Locker;


template<typename T>
struct Protected
{
    template<typename ...Args>
    Protected(Args && ...args) :
        obj_(std::forward<Args>(args)...)
    {        
    }

    Locker<const T> operator->() const;
    Locker<T> operator->();

private:    
    friend class Locker<T>;
    friend class Locker<const T>;
    mutable std::mutex mtx_;
    T obj_;
};


template<typename T>
struct Locker
{
    Locker(Protected<T> & p) :
        lock_(p.mtx_),
        obj_(p.obj_)
    {
        std::cout << "LOCK" << std::endl;
    }

    Locker(Locker<T> && rhs) = default;

    ~Locker()
    {
        std::cout << "UNLOCK\n" << std::endl;
    }

    const T& get() const { return obj_; }
    T& get() { return obj_; }

    const T* operator->() const { return &get(); }
    T* operator->() { return &get(); }

private:    
    std::unique_lock<std::mutex> lock_;
    T & obj_;    
};


template<typename T>
struct Locker<const T>
{
    Locker(const Protected<T> & p) :
        lock_(p.mtx_),
        obj_(p.obj_)
    {
        std::cout << "LOCK (const)" << std::endl;
    }

    Locker(Locker<const T> && rhs) = default;

    ~Locker()
    {
        std::cout << "UNLOCK (const)\n" << std::endl;
    }

    const T& get() const { return obj_; }    
    const T* operator->() const { return &get(); }

private:    
    std::unique_lock<std::mutex> lock_;
    const T & obj_;
};


template<typename T>
Locker<T> Protected<T>::operator->()
{
    return Locker<T>(const_cast<Protected<T>&>(*this));
}


template<typename T>
Locker<const T> Protected<T>::operator->() const
{
    return Locker<T>(const_cast<Protected<T>&>(*this));
}

struct Foo
{
    void bar() { std::cout << "Foo::bar()" << std::endl; }
    void car() const { std::cout << "Foo::car() const" << std::endl; }
};

int main()
{
    Protected<Foo> foo;

    // Using Locker<T> for rw access
    {
        Locker<Foo> locker(foo);
        Foo & foo = locker.get();
        foo.bar();
        foo.car();
    }

    // Using Locker<const T> for const access
    {
        Locker<const Foo> locker(foo);
        const Foo & foo = locker.get();
        foo.car();
    }


    // Single actions can be performed quickly with operator-> 
    foo->bar();
    foo->car();
}

生成此输出:

LOCK
Foo::bar()
Foo::car() const
UNLOCK

LOCK (const)
Foo::car() const
UNLOCK (const)

LOCK
Foo::bar()
UNLOCK

LOCK
Foo::car() const
UNLOCK

使用在线编译器进行测试。

更新:固定 const 正确性。

PS:还有一个异步变体

于 2013-04-06T00:39:29.617 回答
1

就我个人而言,我是LockingPtr范式的粉丝(这篇文章已经过时了,我个人不会听从它的所有建议):

struct thread_safe_account_pointer {
     thread_safe_account_pointer( std::mutex & m,Account * acc) : _acc(acc),_lock(m) {}

     Account * operator->() const {return _acc;}
     Account& operator*() const {return *_acc;}
private:
     Account * _acc;
     std::lock_guard<std::mutex> _lock;
};

并实现包含这样一个Account对象的类:

class SomeTypeWhichOwnsAnAccount {
public:
     thread_safe_account_pointer get_and_lock_account() const {return thread_safe_account_pointer(mutex,&_impl);}

      //Optional non thread-safe
      Account* get_account() const {return &_impl;}

      //Other stuff..
private:
     Account _impl;
     std::mutex mutex;
};

如果合适,可以用智能指针替换指针,您可能需要一个const_thread_safe_account_pointer(甚至更好的通用模板thread_safe_pointer类)

为什么这比显示器(IMO)好?

  1. 您可以在不考虑线程安全的情况下设计您的 Account 类;线程安全是使用您的类的对象的属性,而不是您的类本身的属性。
  2. 在类中嵌套对成员函数的调用时,不需要递归互斥锁。
  3. 无论您是否锁定互斥锁,您都可以在代码中清楚地记录(并且您可以通过不实现来完全防止使用无锁定get_account)。同时拥有 aget_and_lock()get()函数会迫使您考虑线程安全性。
  4. 定义函数(全局或成员)时,您有一个清晰的语义来指定函数是否需要锁定对象的互斥锁(只需传递 a thread_safe_pointer)还是线程安全不可知(使用Account&)。
  5. 最后但并非最不重要的一点是,thread_safe_pointer与监视器有完全不同的语义:

考虑一个MyVector通过监视器实现线程安全的类,以及以下代码:

MyVector foo;
// Stuff.. , other threads are using foo now, pushing and popping elements

int size = foo.size();
for (int i=0;i < size;++i)
   do_something(foo[i]);

像这样的 IMO 代码真的很糟糕,因为它让你觉得safe监视器会为你处理线程安全,而这里我们有一个非常难以发现的竞争条件。

于 2013-04-06T00:42:59.767 回答
0

您的问题是将锁定与数据相关联。在我看来,将其mutex塞入对象中很好。你可以更进一步,使对象本质上成为监视器:锁定进入函数成员,离开时解锁。

于 2013-04-05T23:17:12.080 回答
0

我相信为每个帐户提供自己的锁是可以的。它向您的代码的任何读者提供了一个明确的信号,即访问Account是一个关键部分。

任何涉及每个帐户一个锁的解决方案的缺点是,当您编写同时操作多个帐户的代码时,您必须注意死锁。但是,避免该问题的直接方法是将您的交互限制为一次只与一个帐户进行交互。这不仅避免了潜在的死锁问题,还增加了并发性,因为当当前线程忙于不同的事情时,您不会阻止其他线程访问其他帐户。

您对一致视图的担忧是有效的,但可以通过记录当前事务中发生的操作来实现。例如,您可以使用事务日志来装饰您的deposit()withdraw()操作。

class Account {
  void deposit(const Money &amount);
  void withdraw(const Money &amount);
public:
  void deposit(const Money &amount, Transaction& t) {
    std::lock_guard<std::mutex> _(m_);
    deposit(amount);
    t.log_deposit(*this, amount);
  }
  void withdraw(const Money &amount, Transaction& t) {
    std::lock_guard<std::mutex> _(m_);
    withdraw(amount);
    t.log_withdraw(*this, amount);
  }
private:
  std::mutex m_;
};

然后,atransfer是记录的提款和存款。

void transfer (Account &src, Account &dest, const Money &amount,
               Transaction &t) {
  t.log_transfer(src, dest, amount);
  try {
    src.withdraw(amount, t);
    dest.deposit(amount, t);
    t.log_transfer_complete(src, dest, amount);
  } catch (...) {
    t.log_transfer_fail(src, dest, amount);
    //...
  }
}

请注意,事务日志的概念与您选择部署锁的方式是正交的。

于 2013-04-06T01:10:02.487 回答
0

我认为您的答案是按照您的建议使用 std::lock(),但将其放入朋友函数中。这样您就不需要公开帐户互斥锁。新朋友函数不使用 deposit() 和withdraw() 函数,需要单独锁定和解锁互斥锁。请记住,朋友函数不是成员函数,但可以访问私有成员。

typedef int Money;

class Account {
public:
  Account(Money amount) : balance(amount)
  {
  }

  void deposit(const Money& amount);
  bool withdraw(const Money& amount);

  friend bool transfer(Account& src, Account& dest, const Money& amount)
  {
     std::unique_lock<std::mutex> src_lock(src.m, std::defer_lock);
     std::unique_lock<std::mutex> dest_lock(dest.m, std::defer_lock);
     std::lock(src_lock, dest_lock);

     if(src.balance >= amount)
     {
        src.balance -= amount;
        dest.balance += amount;
        return true;
     }
     return false;
  }
private:
  std::mutex m;
  Money balance;
};
于 2013-04-06T07:11:39.923 回答
0

大多数解决方案都有一个问题,即数据是公开的,因此人们可以在不锁定锁的情况下访问它。

有一种方法可以解决这个问题,但是您不能使用模板,因此必须求助于宏。在 C++11 中实现要好得多,而不是在这里重复整个讨论,我链接到我的实现:https ://github.com/sveljko/lockstrap

于 2014-11-10T04:25:21.273 回答