Как правильно связать мьютекс с его данными?

в классической проблеме перевода денег с одного банковского счета на другой принятое решение (я считаю) заключается в том, чтобы связать мьютекс с каждым банковским счетом, а затем заблокировать оба до снятия денег с одного счета и внесения их на другой. На первый взгляд, я бы сделал это так:

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_guard на transfer, но члены публичных данных тоже пахнут.

в требования к std::lock_guard являются ли его тип удовлетворяет BasicLockable требования, которые только что вызывает lock и unlock действительны. Account удовлетворяет этому требованию, поэтому я мог бы просто использовать std::lock_guard С 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 кажется, вонючий сам по себе.

каков наилучший способ связать мьютекс с данными, которые он защищает в таком сценарии?

UPDATE: в комментариях ниже я отметил, что std::lock можно использовать, чтобы избежать тупика, но я упустил это std::lock полагается на существование try_lock функции (в дополнение к этому для lock и unlock). Добавление try_lock to Accountинтерфейс кажется довольно грубым взломом. Таким образом, кажется, что если мьютекс для Account "объект" остается в Account, он должен быть публичным. Который имеет довольно зловоние.

некоторые предлагаемые решения клиенты используют классы-оболочки для молчаливого связывания мьютексов с Account object, но, как я уже отмечал в своих комментариях, это, похоже, упрощает для разных частей кода использование разных объектов-оболочек вокруг Account, каждый создает свой собственный мьютекс, и это означает, что разные части кода могут попытаться заблокировать Account использование различных мьютексов. Это плохо.

другие предлагаемые решения основаны на замок только один мьютекс за раз. Это устраняет необходимость блокировки более одного мьютекса, но за счет того, что некоторые потоки могут видеть несогласованные представления системы. По сути, это оставляет транзакционную семантику для операций с несколькими объектами.

на данный момент публичный мьютекс начинает выглядеть как наименее вонючий из доступных вариантов, и это вывод, к которому я действительно не хочу приходить. Неужели нет ничего лучше?

8 ответов


проверить Херб Саттер говорить C++ и за пределами 2012: В C++ Многопоточность. Он показывает пример Монитор Объекта-как реализация в C++11.

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

реализация:

LIVE DEMO

#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

нет ничего плохого в том, что деньги "в полете" на некоторое время. Сделайте это так:

Account src, dst;

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

теперь просто сделайте каждый отдельный метод потокобезопасным, например

int Account::withdraw(int n)
{
    std::lock_guard<std::mutex> _(m_);
    balance -= n;
    return n;
}

Я предпочитаю использовать неинтрузивный класс-оболочку вместо загрязнения исходного объекта мьютексом и блокировки его при каждом вызове метода. Этот класс-оболочка (который я назвал Protected<T>) содержит объект user в качестве частной переменной. Protected<T> гранты дружбы в другой класс Locker<T>. Шкафчик принимает оболочку в качестве аргумента конструктора и предоставляет общедоступные методы доступа к объекту пользователя. Шкафчик также держит мьютекс оболочки запертым во время своей жизни. Итак, шкафчик lifetime определяет область, в которой можно безопасно получить доступ к исходному объекту.

на 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


лично я являюсь поклонником 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 класс)

почему это лучше, чем мониторы (ИМО)?

  1. вы можете создать свой класс учетной записи, не думая о потокобезопасности; потокобезопасность является свойством объекта, который использует ваш класс, а не самого класса.
  2. нет необходимости в рекурсивных мьютексах при вложении вызовов функций-членов в вашем классе.
  3. вы четко документируете в своем коде, блокируете ли вы мьютекс или нет (и вы можете полностью предотвратить использование-без-блокировки, не реализуя get_account). Как get_and_lock() и get() функции сил вы думать о потокобезопасности.
  4. при определении функций (global или member) у вас есть чистая семантика, чтобы указать, требует ли функция блокировки мьютекса объекта (просто передайте 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]);

код ИМО, как это действительно плохо, потому что это заставляет вас чувствовать себя safe думая, что мониторы позаботятся о потокобезопасности для вас, в то время как здесь у нас есть состояние гонки, которое невероятно трудно обнаружить.


ваша задача-связать замок с Данные. В моих глазах, набивая mutex на объект в порядке. Вы можете пойти еще дальше, сделав объекты по существу в мониторы: блокировка для ввода члена функции, разблокировка при выходе.


Я считаю, что предоставление каждой учетной записи с собственной блокировкой в порядке. Он предоставляет четкий сигнал любому читателю вашего кода, который обращается к 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_;
};

затем transfer - это протоколированием вывода и депозита.

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);
    //...
  }
}

обратите внимание, что идея журнала транзакций ортогональна тому, как вы решаете развернуть свои блокировки.


Я думаю, что ваш ответ-сделать, как вы предлагаете, и использовать std::lock (), но поместить его в функцию друга. Таким образом, вам не нужно публиковать учетную запись mutex. Функции deposit() и withdraw () не используются новой функцией friend и должны будут отдельно блокировать и разблокировать мьютекс. Помните, что функции friend не являются функциями-членами, но имеют доступ к частным членам.

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;
};

большинство решений имеют проблему, что данные хранятся общедоступными, таким образом, можно получить доступ к нему без блокировки блокировки.

есть способ исправить это, но вы не можете использовать шаблоны, и поэтому приходится прибегать к макросам. Гораздо приятнее реализовать на C++11 и вместо того, чтобы повторять все обсуждение здесь, я ссылаюсь на свою реализацию на:https://github.com/sveljko/lockstrap