스레드 안전 규칙에서 제안한 비 const 인수로 생성자를 복사 하시겠습니까?


9

레거시 코드에 래퍼가 있습니다.

class A{
   L* impl_; // the legacy object has to be in the heap, could be also unique_ptr
   A(A const&) = delete;
   L* duplicate(){L* ret; legacy_duplicate(impl_, &L); return ret;}
   ... // proper resource management here
};

이 레거시 코드에서 객체를 "중복"하는 함수는 스레드로부터 안전하지 않으므로 (같은 첫 번째 인수를 호출 할 때) const래퍼에 표시되지 않습니다 . 현대 규칙을 따르는 것 같습니다 : https://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/

이것은 duplicate그렇지 않은 세부 사항을 제외하고 복사 생성자를 구현하는 좋은 방법처럼 보입니다 const. 따라서 나는 이것을 직접 할 수 없다 :

class A{
   L* impl_; // the legacy object has to be in the heap
   A(A const& other) : L{other.duplicate()}{} // error calling a non-const function
   L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};

이 역설적 인 상황에서 벗어나는 방법은 무엇입니까?

( legacy_duplicate쓰레드 안전하지는 않지만 객체가 종료되면 객체를 원래 상태로 유지한다는 것을 알고 있습니다. C 함수이므로 동작은 문서화되어 있지만 constness의 개념은 없습니다.)

가능한 많은 시나리오를 생각할 수 있습니다.

(1) 하나의 가능성은 일반적인 의미론을 가진 복사 생성자를 전혀 구현할 방법이 없다는 것입니다. (예, 객체를 움직일 수 있으며 그것이 필요한 것은 아닙니다.)

(2) 반면에 객체를 복사하는 것은 기본적으로 단순 유형을 복사하면 반 수정 된 상태에서 소스를 찾을 수 있다는 의미에서 스레드로부터 안전하지 않습니다.

class A{
   L* impl_;
   A(A const& other) : L{const_cast<A&>(other).duplicate()}{} // error calling a non-const function
   L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};

(3) 또는 duplicateconst를 선언 하고 모든 상황에서 스레드 안전성에 대해 거짓말합니다. (모든 레거시 함수는 신경 쓰지 const않으므로 컴파일러는 불평하지 않습니다.)

class A{
   L* impl_;
   A(A const& other) : L{other.duplicate()}{}
   L* duplicate() const{L* ret; legacy_duplicate(impl_, &ret); return ret;}
};

(4) 마지막으로, 나는 논리를 따르고 논-스트레스 논증 을 취하는 복사 생성자를 만들 수 있습니다 .

class A{
   L* impl_;
   A(A const&) = delete;
   A(A& other) : L{other.duplicate()}{}
   L* duplicate(){L* ret; legacy_duplicate(impl_, &ret); return ret;}
};

이러한 객체는 일반적으로 그렇지 않기 때문에 많은 상황에서 작동한다는 것이 밝혀졌습니다 const.

문제는 이것이 유효하거나 일반적인 경로입니까?

나는 그것들의 이름을 말할 수는 없지만, 비 const 사본 생성자를 갖는 길에서 직관적으로 많은 문제를 예상합니다. 아마도이 미묘함으로 인해 가치 유형으로 적합하지 않을 것입니다.

(5) 마지막으로, 이것은 과도한 것으로 보이며 런타임 비용이 가파르지만 뮤텍스를 추가 할 수 있습니다.

class A{
   L* impl_;
   A(A const& other) : L{other.duplicate_locked()}{}
   L* duplicate(){
      L* ret; legacy_duplicate(impl_, &ret); return ret;
   }
   L* duplicate_locked() const{
      std::lock_guard<std::mutex> lk(mut);
      L* ret; legacy_duplicate(impl_, &ret); return ret;
   }
   mutable std::mutex mut;
};

그러나 이것을 강요하면 비관 화처럼 보이고 수업이 더 커집니다. 확실하지 않습니다. 나는 현재 (4) 또는 (5) 또는 둘의 조합에 기대어 있습니다.

—— 편집

다른 옵션 :

(6) 중복 멤버 함수의 모든 비센스를 잊어 버리고 legacy_duplicate생성자 를 호출 하고 복사 생성자가 스레드로부터 안전하지 않다고 선언하십시오. (필요한 경우 다른 유형의 스레드 안전 버전을 만듭니다. A_mt)

class A{
   L* impl_;
   A(A const& other){legacy_duplicate(other.impl_, &impl_);}
};

편집 2

레거시 기능이 수행하는 작업에 적합한 모델 일 수 있습니다. 입력을 터치하면 첫 번째 인수가 나타내는 값과 관련하여 호출이 스레드로부터 안전하지 않습니다.

void legacy_duplicate(L* in, L** out){
   *out = new L{};
   char tmp = in[0];
   in[0] = tmp; 
   std::memcpy(*out, in, sizeof *in); return; 
}

1
" 이 레거시 코드에서 객체를 복제하는 함수는 스레드로부터 안전하지 않습니다 (같은 첫 번째 인수를 호출 할 때) "확실합니까? LL인스턴스 를 작성하여 수정 한 상태가 포함되어 있지 않습니까? 그렇지 않은 경우이 작업이 스레드로부터 안전하지 않다고 생각하는 이유는 무엇입니까?
Nicol Bolas

네, 그 상황입니다. 첫 번째 인수의 내부 상태가 실행 중에 수정 된 것처럼 보입니다. 어떤 이유로 (일부 "최적화"또는 잘못된 설계 또는 단순히 사양에 의해) legacy_duplicate두 개의 다른 스레드에서 동일한 첫 번째 인수로 함수 를 호출 할 수 없습니다.
alfC

@TedLyngmo 알았어. 비록 기술적으로 c ++ pre 11 const에서는 스레드가있을 때 더 모호한 의미를 갖습니다.
alfC

@TedLyngmo 예, 꽤 좋은 비디오입니다. 비디오가 적절한 멤버를 다루고 구성 문제를 다루지 않는 것이 유감입니다 (구성 요소가 "다른"개체에 있음). 관점에서 볼 때 다른 추상화 계층 (및 콘크리트 뮤텍스)을 추가하지 않고 복사 할 때이 래퍼 스레드를 안전하게 만드는 본질적인 방법이 없을 수 있습니다.
alfC

예, 글쎄요, 혼란 스러워요. 그리고 나는 const정말로 무엇을 의미 하는지 모르는 사람들 중 하나 일 것입니다 . :-) 나는 const&수정하지 않는 한 내 사본 ctor에 대해 두 번 생각하지 않을 것 other입니다. 나는 항상 스레드 안전성을 캡슐화를 통해 여러 스레드에서 액세스 해야하는 항목 위에 추가하는 것으로 생각하며 실제로 답변을 기대하고 있습니다.
Ted Lyngmo

답변:


0

옵션 (4)와 (5)를 모두 포함하지만 성능에 필요하다고 생각할 때 스레드 안전하지 않은 동작을 명시 적으로 선택합니다.

다음은 완전한 예입니다.

#include <cstdlib>
#include <thread>

struct L {
  int val;
};

void legacy_duplicate(const L* in, L** out) {
  *out = new L{};
  std::memcpy(*out, in, sizeof *in);
  return;
}

class A {
 public:
  A(L* l) : impl_{l} {}
  A(A const& other) : impl_{other.duplicate_locked()} {}

  A copy_unsafe_for_multithreading() { return {duplicate()}; }

  L* impl_;

  L* duplicate() {
    printf("in duplicate\n");
    L* ret;
    legacy_duplicate(impl_, &ret);
    return ret;
  }
  L* duplicate_locked() const {
    std::lock_guard<std::mutex> lk(mut);
    printf("in duplicate_locked\n");
    L* ret;
    legacy_duplicate(impl_, &ret);
    return ret;
  }
  mutable std::mutex mut;
};

int main() {
  A a(new L{1});
  const A b(new L{2});

  A c = a;
  A d = b;

  A e = a.copy_unsafe_for_multithreading();
  A f = const_cast<A&>(b).copy_unsafe_for_multithreading();

  printf("\npointers:\na=%p\nb=%p\nc=%p\nc=%p\nd=%p\nf=%p\n\n", a.impl_,
     b.impl_, c.impl_, d.impl_, e.impl_, f.impl_);

  printf("vals:\na=%d\nb=%d\nc=%d\nc=%d\nd=%d\nf=%d\n", a.impl_->val,
     b.impl_->val, c.impl_->val, d.impl_->val, e.impl_->val, f.impl_->val);
}

산출:

in duplicate_locked
in duplicate_locked
in duplicate
in duplicate

pointers:
a=0x7f85e8c01840
b=0x7f85e8c01850
c=0x7f85e8c01860
c=0x7f85e8c01870
d=0x7f85e8c01880
f=0x7f85e8c01890

vals:
a=1
b=2
c=1
c=2
d=1
f=2

다음은 스레드 안전성 을 전달 하는 Google 스타일 가이드const따르지만 API를 호출하는 코드는const_cast


대답 주셔서 감사합니다, 나는 당신의 asnwer를 변경하지 않습니다 생각 나는 모르겠지만위한 더 나은 모델 legacy_duplicate이 될 수있다 void legacy_duplicate(L* in, L** out) { *out = new L{}; char tmp = in[0]; /*some weird call here*/; in[0] = tmp; std::memcpy(*out, in, sizeof *in); return; }(즉, const가 아닌 in)
alfC

귀하의 답변은 옵션 (4) 및 명시적인 버전의 옵션 (2)와 결합 될 수 있기 때문에 매우 흥미 롭습니다. 즉, A a2(a1)스레드 안전을 시도하거나 삭제할 A a2(const_cast<A&>(a1))수 있으며 스레드 안전을 시도하지 않습니다.
alfC

2
예, A스레드 안전 및 스레드 안전하지 않은 컨텍스트에서 모두 사용하려는 경우 const_cast스레드 안전이 위반 된 것으로 알려진 위치를 명확하게 표시하도록 호출 코드를 가져와야합니다 . API (mutex) 뒤에 추가 안전을 적용하는 것은 좋지만 안전 (const_cast)을 숨기는 것은 좋지 않습니다.
Michael Graczyk

0

TLDR : 복제 기능의 구현을 수정하거나 뮤텍스 (또는 더 적절한 잠금 장치, 아마도 스핀 락을 도입하거나 더 무거운 것을하기 전에 뮤텍스가 회전하도록 구성) 복제 한 다음 복제 구현을 수정하십시오. 잠금이 실제로 문제가 될 때 잠금을 제거하십시오.

주목할 점은 이전에는 없었던 기능, 즉 여러 스레드에서 객체를 동시에 복제하는 기능을 추가하는 것입니다.

분명히, 당신이 규정 한 조건 하에서, 그것은 일종의 외부 동기화를 사용하지 않고, 이전에 그렇게했다면, 경쟁 조건 인 버그 일 것입니다.

따라서이 새로운 기능을 사용하면 기존 기능으로 상속되지 않고 코드에 추가 할 수 있습니다. 이 새로운 기능을 얼마나 자주 사용할 지에 따라 여분의 잠금 장치를 추가하는 것이 실제로 많은 비용이 드는지 아는 사람이어야 합니다.

또한 객체의 인식 된 복잡성에 따라-당신이 그것을 제공하는 특별한 치료에 의해, 복제 절차가 사소한 것이 아니라고 가정하므로 성능 측면에서 이미 상당히 비쌉니다.

위의 내용에 따라 두 가지 경로를 따를 수 있습니다.

A) 여러 스레드에서이 객체를 복사하는 것은 추가 잠금의 오버 헤드가 많은 비용이들 정도로 충분히 자주 발생하지 않는다는 것을 알고 있습니다. 적어도 기존 복제 절차가 자체적으로 비용이 많이 들기 때문에 사소하게 저렴할 것입니다 spinlock / pre-spinning mutex에 대한 경합이 없습니다.

B) 여러 스레드에서 복사하면 여분의 잠금이 문제가 될 정도로 자주 발생한다고 생각합니다. 그런 다음 실제로 하나의 옵션 만 있습니다-복제 코드를 수정하십시오. 수정하지 않으면이 추상화 계층 또는 다른 곳에서 어쨌든 잠금이 필요하지만 버그를 원하지 않는 경우에는 필요합니다. 설정 한대로이 경로에서 가정합니다. 잠금이 너무 비싸기 때문에 유일한 옵션은 복제 코드를 수정하는 것입니다.

나는 당신이 실제로 상황 A에 있고 의심되지 않을 때 성능 저하가 거의없는 spinlock / spinning mutex를 추가하는 것만으로도 잘 작동 할 것이라고 생각합니다 (벤치마킹).

이론적으로 또 다른 상황이 있습니다.

C) 복제 기능의 복잡해 보이는 것과는 달리, 실제로는 사소한 것이지만 어떤 이유로 고칠 수는 없습니다. 경합되지 않은 스핀 록조차도 용인 할 수없는 성능 저하를 복제에 도입하는 것은 매우 사소한 일입니다. 병렬 스레드의 복제는 거의 사용되지 않습니다. 단일 스레드의 복제는 항상 사용되므로 성능 저하를 절대로 용납 할 수 없습니다.

이 경우 다음을 제안합니다. 기본 복사본 생성자 / 연산자를 삭제하여 실수로 다른 사람이 실수로 사용하지 못하도록합니다. 스레드 안전 메소드와 안전하지 않은 스레드 메소드를 작성하십시오. 상황에 따라 사용자가 명시 적으로 호출하도록합니다. 다시 말하지만, 실제로 이러한 상황에 있고 기존 복제 구현을 수정할 수없는 경우 수용 가능한 단일 스레드 성능과 안전한 멀티 스레딩을 달성 할 수있는 다른 방법은 없습니다 . 그러나 나는 당신이 실제로 그런 것 같지 않다고 생각합니다.

그 뮤텍스 / 스핀 락과 벤치 마크를 추가하십시오.


C ++에서 spinlock / pre-spinning mutex에 대한 자료를 알려줄 수 있습니까? 그것이 제공하는 것보다 더 복잡한 것 std::mutex입니까? 중복 기능은 비밀이 아니며 문제를 높은 수준으로 유지하고 MPI에 대한 답변을받지 못한다는 언급은하지 않았습니다. 그러나 당신이 그 깊이로 갔기 때문에 나는 당신에게 더 자세한 것을 줄 수 있습니다. 레거시 기능이며 MPI_Comm_dup효과적인 비 스레드 안전성은 github.com/pmodels/mpich/issues/3234에 설명되어 있습니다 . 이것이 중복을 고칠 ​​수없는 이유입니다. (또한 뮤텍스를 추가하면 모든 MPI 호출을 스레드로부터 안전하도록 유혹 할 것입니다.)
alfC

슬프게도 나는 std :: mutex를 잘 모르지만 프로세스가 잠들기 전에 약간의 회전을한다고 생각합니다. 이것을 수동으로 제어하는 ​​잘 알려진 동기화 장치는 다음과 같습니다. docs.microsoft.com/en-us/windows/win32/api/synchapi/… 성능을 비교하지는 않았지만 std :: mutex는 이제 우수함 : stackoverflow.com/questions/9997473/…docs.microsoft.com/en-us/windows/win32/sync/…를
DeducibleSteak

이것은 고려해야 할 일반적인 고려 사항에 대한 좋은 설명 인 것 같습니다 : stackoverflow.com/questions/5869825/…
DeducibleSteak

다시 한 번 감사드립니다. 저는 Linux에 있습니다.
alfC

여기에 다소 자세한 성능 비교입니다 (다른 언어에 대한,하지만 난이 유익하고 무엇을 기대해야하는지 나타내는 것 같다) : matklad.github.io/2020/01/04/... TLDR은 - 스핀 락은 매우 작은에서 승리 경합이 없으면 여백, 경합이있을 때 심하게 잃을 수 있습니다.
DeducibleSteak
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.