Programing

C ++에서 이동 가능한 유형의 뮤텍스를 어떻게 처리해야합니까?

lottogame 2020. 10. 22. 07:38
반응형

C ++에서 이동 가능한 유형의 뮤텍스를 어떻게 처리해야합니까?


설계 상 std::mutex이동하거나 복사 할 수 없습니다. 이는 A뮤텍스를 보유한 클래스 가 default-move-constructor를받지 않음을 의미합니다 .

A스레드로부터 안전한 방식 으로이 유형을 이동 가능 하게 만드는 방법은 무엇입니까?


약간의 코드로 시작해 보겠습니다.

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. 엄밀히 말해서 이동 멤버에게는 필요하지 않지만 복사 멤버도 원한다고 가정합니다. 그렇지 않은 경우 mutex를 만들 필요가 없습니다 mutable.

시공 할 때 A잠글 필요가 없습니다 this->mut_. 그러나 mut_생성중인 객체 (이동 또는 복사) 를 잠글 필요가 있습니다. 다음과 같이 할 수 있습니다.

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

thisfirst 의 멤버를 기본으로 구성한 다음 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_mutexfor 를 사용하여 자체 검사없이 해결할 수도 있습니다 MutexType.

최신 정보

아래 주석에서 Yakk는 복사 및 이동 생성자에서 기본 구성을해야하는 것에 대해 매우 불행합니다 (그는 요점이 있습니다). 이 문제에 대해 충분히 강하게 느끼고 그것에 대해 기꺼이 기억할 수 있다면 다음과 같이 피할 수 있습니다.

  • 데이터 멤버로 필요한 잠금 유형을 추가하십시오. 이러한 구성원은 보호되는 데이터 앞에 와야합니다.

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

죄송합니다. Yakk가이 업데이트를 완료하기 전에 댓글을 삭제했습니다. 그러나 그는이 문제를 추진하고이 답변에 대한 해결책을 얻은 것에 대해 공로를 인정받을 만합니다.

업데이트 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_)
    {}

이 질문에 답할 수있는 멋지고 깨끗하고 쉬운 방법이없는 것 같습니다. Anton의 솔루션 은 옳다고 생각 하지만 더 나은 답변이 나오지 않는 한 확실히 논쟁의 여지가 있습니다. 그런 클래스를 힙에 넣고 돌보는 것이 좋습니다. 통해 std::unique_ptr:

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

이제 완전히 움직일 수있는 유형이며 이동이 발생하는 동안 내부 뮤텍스에 대한 잠금을 가진 사람은 이것이 좋은 일인지에 대한 논쟁의 여지가 있더라도 여전히 안전합니다.

복사 의미론이 필요한 경우

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

이것은 거꾸로 된 대답입니다. "이 객체는 동기화되어야 함"을 유형의 기본으로 포함하는 대신 모든 유형 아래에 삽입하십시오 .

동기화 된 개체를 매우 다르게 처리합니다. 한 가지 큰 문제는 교착 상태 (여러 객체 잠금)에 대해 걱정해야한다는 것입니다. 또한 기본적으로 "객체의 기본 버전"이되어서는 안됩니다. 동기화 된 객체는 경합 상태가 될 객체를위한 것이며, 목표는 스레드 사이의 경합을 최소화하는 것이어야합니다.

그러나 개체 동기화는 여전히 유용합니다. 동기화 기에서 상속하는 대신 동기화에서 임의의 유형을 래핑하는 클래스를 작성할 수 있습니다. 개체가 동기화되었으므로 사용자는 개체에 대한 작업을 수행하기 위해 몇 가지 후프를 거쳐야하지만 개체에 대해 손으로 코딩 된 제한된 작업 집합에 제한되지 않습니다. 개체에 대한 여러 작업을 하나로 구성하거나 여러 개체에 대해 작업을 수행 할 수 있습니다.

다음은 임의 유형에 대한 동기화 된 래퍼입니다 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);
}

the placement and aligned storage versions are a bit messier. Most access to t would be replaced by a member function T&t() and T const&t()const, except at construction where you'd have to jump through some hoops.

By making synchronized a wrapper instead of part of the class, all we have to ensure is that the class internally respects const as being multiple-reader, and write it in a single-threaded manner.

In the rare cases we need a synchronized instance, we jump through hoops like the above.

Apologies for any typos in the above. There are probably some.

A side benefit to the above is that n-ary arbitrary operations on synchronized objects (of the same type) work together, without having to hard-code it before hand. Add in a friend declaration and n-ary synchronized objects of multiple types might work together. I might have to move access out of being an inline friend to deal with overload conficts in that case.

live example


Using mutexes and C++ move semantics is an excellent way to safely and efficiently transfer data between threads.

Imagine a 'producer' thread that makes batches of strings and provides them to (one or more) consumers. Those batches could be represented by an object containing (potentially large) std::vector<std::string> objects. We absolutely want to 'move' the internal state of those vectors into their consumers without unnecessary duplication.

You simply recognize the mutex as part of the object not part of the object's state. That is, you don't want to move the mutex.

What locking you need depends on your algorithm or how generalized your objects are and what range of uses you permit.

If you only ever move from a shared state 'producer' object to a thread-local 'consuming' object you might be OK to only lock the moved from object.

If it's a more general design you will need to lock both. In such a case you need to then consider dead-locking.

If that is a potential issue then use std::lock() to acquire locks on both mutexes in a deadlock free way.

http://en.cppreference.com/w/cpp/thread/lock

As a final note you need to make sure you understand the move semantics. Recall that the moved from object is left in a valid but unknown state. It's entirely possible that a thread not performing the move has a valid reason to attempt access the moved from object when it may find that valid but unknown state.

Again my producer is just banging out strings and the consumer is taking away the whole load. In that case every time the producer tries to add to the vector it may find the vector non-empty or empty.

In short if the potential concurrent access to the moved from object amounts to a write it's likely to be OK. If it amounts to a read then think about why it's OK to read an arbitrary state.


First of all, there must be something wrong with your design if you want to move an object containing a mutex.

But if you decide to do it anyway, you have to create a new mutex in move constructor, that is e.g:

// movable
struct B{};

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

This is thread-safe, because the move constructor can safely assume that its argument isn't used anywhere else, so the locking of the argument isn't required.

참고URL : https://stackoverflow.com/questions/29986208/how-should-i-deal-with-mutexes-in-movable-types-in-c

반응형