Как я должен иметь дело с мьютексами в подвижных типах в C++?



конструкция std::mutex не движимости, ни скопировать-технологичная. Это означает, что класс A, который содержит мьютекс, не получит конструктор перемещения по умолчанию.

как бы я сделал этот тип A движимость в потокобезопасным способом?

139   5  

5 ответов:

давайте начнем с небольшого кода:

class A
{
    using MutexType = std::mutex;
    using ReadLock = std::unique_lock<MutexType>;
    using WriteLock = std::unique_lock<MutexType>;

    mutable MutexType mut_;

    std::string field1_;
    std::string field2_;

public:
    ...

я поместил туда некоторые довольно наводящие на размышления псевдонимы типов, которые мы не будем использовать в C++11, но станем гораздо более полезными в C++14. Потерпи, мы доберемся туда.

Ваш вопрос сводится к:

Как написать конструктор перемещения и оператор присваивания перемещения для этого класса?

начнем с переезда конструктор.

Переместить Конструктор

обратите внимание, что член mutex была произведена mutable. Строго говоря, это не обязательно для членов move, но я предполагаю, что вы также хотите копировать членов. Если это не так, то нет необходимости делать мьютекс mutable.

при строительстве A, вам не нужно замок this->mut_. Но вам нужно заблокировать mut_ объекта, из которого вы строите (переместить или скопировать). Это можно сделать так:

    A(A&& a)
    {
        WriteLock rhs_lk(a.mut_);
        field1_ = std::move(a.field1_);
        field2_ = std::move(a.field2_);
    }

обратите внимание, что мы должны были по умолчанию построить члены this во-первых, а затем присвоить им значения только после a.mut_ заблокирована.

Переместить Задание

оператор присваивания перемещения существенно сложнее, потому что вы не знаете, обращается ли какой-либо другой поток к LHS или rhs выражения присваивания. И вообще, нужно остерегаться следующего сценарий:

// Thread 1
x = std::move(y);

// Thread 2
y = std::move(x);

вот оператор присваивания перемещения, который правильно защищает приведенный выше сценарий:

    A& operator=(A&& a)
    {
        if (this != &a)
        {
            WriteLock lhs_lk(mut_, std::defer_lock);
            WriteLock rhs_lk(a.mut_, std::defer_lock);
            std::lock(lhs_lk, rhs_lk);
            field1_ = std::move(a.field1_);
            field2_ = std::move(a.field2_);
        }
        return *this;
    }

обратите внимание, что нужно использовать std::lock(m1, m2) чтобы заблокировать два мьютекса, а не просто блокировать их один за другим. Если вы блокируете их один за другим, то когда два потока назначают два объекта в противоположном порядке, как показано выше, вы можете получить взаимоблокировку. Суть std::lock чтобы избежать этого тупика.

копировать Конструктор

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

    A(const A& a)
    {
        ReadLock  rhs_lk(a.mut_);
        field1_ = a.field1_;
        field2_ = a.field2_;
    }

конструктор копирования очень похож на конструктор перемещения, за исключением ReadLock псевдоним используется вместо WriteLock. В настоящее время эти оба псевдонима std::unique_lock<std::mutex> и так это действительно не имеет никакого значения.

а в C++14, вы будете иметь возможность сказать это:

    using MutexType = std::shared_timed_mutex;
    using ReadLock  = std::shared_lock<MutexType>;
    using WriteLock = std::unique_lock<MutexType>;

этот мая быть оптимизацией, но не определенно. Вам придется измерить, чтобы определить, если это так. Но с этим изменением, можно скопировать конструкцию С тот же rhs в нескольких потоках одновременно. Решение C++11 заставляет вас делать такие потоки последовательными, даже если rhs не изменяется.

Скопировать Задание

для полноты, вот оператор присваивания копии, который должен быть довольно самостоятельным пояснение после прочтения обо всем остальном:

    A& operator=(const A& a)
    {
        if (this != &a)
        {
            WriteLock lhs_lk(mut_, std::defer_lock);
            ReadLock  rhs_lk(a.mut_, std::defer_lock);
            std::lock(lhs_lk, rhs_lk);
            field1_ = a.field1_;
            field2_ = a.field2_;
        }
        return *this;
    }

и т. д.

любые другие члены или свободные функции, которые имеют доступ Aсостояние также должно быть защищено, если вы ожидаете, что несколько потоков смогут вызывать их одновременно. Например, вот swap:

    friend void swap(A& x, A& y)
    {
        if (&x != &y)
        {
            WriteLock lhs_lk(x.mut_, std::defer_lock);
            WriteLock rhs_lk(y.mut_, std::defer_lock);
            std::lock(lhs_lk, rhs_lk);
            using std::swap;
            swap(x.field1_, y.field1_);
            swap(x.field2_, y.field2_);
        }
    }

обратите внимание, что если вы просто полагаться на std::swap делая работу, блокировка будет при неправильной детализации, блокировки и разблокировки между тремя ходами это std::swap будет внутренне проанализировать.

в самом деле, думал о swap может дать вам представление об API, который вам может понадобиться для обеспечения "потокобезопасности"A, который в целом будет отличаться от" не-потокобезопасного "API, из-за проблемы" блокировки детализации".

Также обратите внимание на необходимость защиты от "собственной своп". "я-своп" должна быть пустой. Без самопроверки можно было бы рекурсивно замок один и тот же мьютекс. Это также может быть решена без самоконтроль с помощью std::recursive_mutex на MutexType.

обновление

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

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

    mutable MutexType mut_;
    ReadLock  read_lock_;
    WriteLock write_lock_;
    // ... other data members ...
    
  • и затем в конструкторах (например, конструктор копирования) сделайте следующее:

    A(const A& a)
        : read_lock_(a.mut_)
        , field1_(a.field1_)
        , field2_(a.field2_)
    {
        read_lock_.unlock();
    }
    

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

обновление 2

и dyp придумал это хорошо предложение:

    A(const A& a)
        : A(a, ReadLock(a.mut_))
    {}
private:
    A(const A& a, ReadLock rhs_lk)
        : field1_(a.field1_)
        , field2_(a.field2_)
    {}

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

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

но синхронизация объектов по-прежнему полезен. Вместо наследования от синхронизатора, мы можем написать класс, который обертывает произвольный тип в синхронизации. Пользователи должны прыгать через несколько обручей, чтобы выполнять операции над объектом теперь, когда он синхронизирован, но они не ограничены некоторым ограниченным набором операций над объектом с ручным кодом. Они могут составлять несколько операций над объектом в один или иметь работа с несколькими объектами.

вот синхронизированная оболочка вокруг произвольного типа T:

template<class T>
struct synchronized {
  template<class F>
  auto read(F&& f) const&->std::result_of_t<F(T const&)> {
    return access(std::forward<F>(f), *this);
  }
  template<class F>
  auto read(F&& f) &&->std::result_of_t<F(T&&)> {
    return access(std::forward<F>(f), std::move(*this));
  }
  template<class F>
  auto write(F&& f)->std::result_of_t<F(T&)> {
    return access(std::forward<F>(f), *this);
  }
  // uses `const` ness of Syncs to determine access:
  template<class F, class... Syncs>
  friend auto access( F&& f, Syncs&&... syncs )->
  std::result_of_t< F(decltype(std::forward<Syncs>(syncs).t)...) >
  {
    return access2( std::index_sequence_for<Syncs...>{}, std::forward<F>(f), std::forward<Syncs>(syncs)... );
  };
  synchronized(synchronized const& o):t(o.read([](T const&o){return o;})){}
  synchronized(synchronized && o):t(std::move(o).read([](T&&o){return std::move(o);})){}  
  // special member functions:
  synchronized( T & o ):t(o) {}
  synchronized( T const& o ):t(o) {}
  synchronized( T && o ):t(std::move(o)) {}
  synchronized( T const&& o ):t(std::move(o)) {}
  synchronized& operator=(T const& o) {
    write([&](T& t){
      t=o;
    });
    return *this;
  }
  synchronized& operator=(T && o) {
    write([&](T& t){
      t=std::move(o);
    });
    return *this;
  }
private:
  template<class X, class S>
  static auto smart_lock(S const& s) {
    return std::shared_lock< std::shared_timed_mutex >(s.m, X{});
  }
  template<class X, class S>
  static auto smart_lock(S& s) {
    return std::unique_lock< std::shared_timed_mutex >(s.m, X{});
  }
  template<class L>
  static void lock(L& lockable) {
      lockable.lock();
  }
  template<class...Ls>
  static void lock(Ls&... lockable) {
      std::lock( lockable... );
  }
  template<size_t...Is, class F, class...Syncs>
  friend auto access2( std::index_sequence<Is...>, F&&f, Syncs&&...syncs)->
  std::result_of_t< F(decltype(std::forward<Syncs>(syncs).t)...) >
  {
    auto locks = std::make_tuple( smart_lock<std::defer_lock_t>(syncs)... );
    lock( std::get<Is>(locks)... );
    return std::forward<F>(f)(std::forward<Syncs>(syncs).t ...);
  }

  mutable std::shared_timed_mutex m;
  T t;
};
template<class T>
synchronized< T > sync( T&& t ) {
  return {std::forward<T>(t)};
}

C++14 и C++1z функции включены.

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

использовать выглядит так:

synchronized<int> x = 7;
x.read([&](auto&& v){
  std::cout << v << '\n';
});

на int с синхронизированным доступом.

я бы не советовал иметь synchronized(synchronized const&). Это редко бывает необходимый.

Если вам нужно synchronized(synchronized const&), Я был бы соблазн заменить T t; С std::aligned_storage, позволяющ ручной конструкции размещения, и делает ручное разрушение. Это позволяет правильно управлять временем жизни.

за исключением этого, мы могли бы скопировать источник T, а затем читать из него:

synchronized(synchronized const& o):
  t(o.read(
    [](T const&o){return o;})
  )
{}
synchronized(synchronized && o):
  t(std::move(o).read(
    [](T&&o){return std::move(o);})
  )
{}

задание:

synchronized& operator=(synchronized const& o) {
  access([](T& lhs, T const& rhs){
    lhs = rhs;
  }, *this, o);
  return *this;
}
synchronized& operator=(synchronized && o) {
  access([](T& lhs, T&& rhs){
    lhs = std::move(rhs);
  }, *this, std::move(o));
  return *this;
}
friend void swap(synchronized& lhs, synchronized& rhs) {
  access([](T& lhs, T& rhs){
    using std::swap;
    swap(lhs, rhs);
  }, *this, o);
}

размещение и выровненные версии хранения немного грязнее. Самый доступ к t будет заменен функцией-членом T&t() и T const&t()const, за исключением строительства, где вам придется прыгать через обручи.

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

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

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

побочным преимуществом вышеизложенного является то, что N-арные произвольные операции над synchronized объекты (одного и того же типа) работают вместе, без необходимости жестко кодировать его перед рукой. Добавьте в объявление друга и n-ary synchronized объекты различных типов могут работать вместе. Возможно, мне придется переехать access из того, чтобы быть встроенным другом, чтобы иметь дело с перегрузкой конфиктов в этом случае.

видео

учитывая, что, похоже, нет хорошего, чистого, простого способа ответить на это - решение Антона I думаю правильно, но его определенно спорно, если не появится лучший ответ, я бы рекомендовал поместить такой класс в кучу и следить за ним через std::unique_ptr:

auto a = std::make_unique<A>();

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

Если вам нужно скопировать семантику просто используйте

auto a2 = std::make_shared<A>();

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

представьте себе поток "производитель", который делает партии строк и предоставляет их (одному или нескольким) потребителям. Эти пакеты могут быть представлены объектом, содержащим (потенциально большой)std::vector<std::string> объекты. Мы абсолютно хотим "переместить" внутреннее состояние этих векторов в их потребителей без ненужного дублирования.

вы просто признать мьютекс как часть объекта не является частью состояния объекта. То есть, вы не хотите перемещать мьютекс.

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

Если вы только когда-либо переходите от объекта "производитель" общего состояния к объекту "потребление" локального потока, вы можете быть в порядке, чтобы заблокировать только перемещенный С

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

но если вы все равно решите это сделать, вам нужно создать новый мьютекс в конструкторе move, например:

// movable
struct B{};

class A {
    B b;
    std::mutex m;
public:
    A(A&& a)
        : b(std::move(a.b))
        // m is default-initialized.
    {
    }
};

это потокобезопасно, потому что конструктор перемещения может безопасно предположить, что его аргумент не используется нигде, поэтому блокировка аргумента не требуется.

    Ничего не найдено.

Добавить ответ:
Отменить.