İş parçacığı güvenlik kuralları tarafından önerilen sabit olmayan bağımsız değişken ile yapıcı kopyalansın mı?


9

Eski bir kod parçası için bir sarıcı var.

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

Bu eski kodda, bir nesneyi “çoğaltan” işlev iş parçacığı için güvenli değildir (aynı ilk bağımsız değişken çağrılırken), bu nedenle constsarmalayıcıda işaretlenmez . Modern kurallara uyduğumu tahmin ediyorum: https://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/

Bu duplicate, olmayan bir ayrıntı dışında bir kopya oluşturucu uygulamak için iyi bir yol gibi görünüyor const. Bu yüzden bunu doğrudan yapamam:

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

Peki bu paradoksal durumun çıkış yolu nedir?

(Diyelim ki bu legacy_duplicateiş parçacığı için güvenli değil ama çıktığı zaman nesneyi orijinal durumda bırakıyor. C işlevi olarak, davranış sadece belgelenmiştir, ancak sabitlik kavramı yoktur.)

Birçok olası senaryo düşünebilirim:

(1) Bir olasılık, olağan anlambilim ile bir kopya oluşturucu uygulamanın hiçbir yolu olmamasıdır. (Evet, nesneyi hareket ettirebilirim ve ihtiyacım olan şey bu değil.)

(2) Öte yandan, bir nesneyi kopyalamak, basit bir tür kopyalamanın kaynağı yarı değiştirilmiş durumda bulabilmesi açısından doğal olarak iş parçacığı açısından güvenli değildir, bu yüzden sadece ileri gidip bunu yapabilirim,

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) veya hatta duplicatetüm bağlamlarda sabitlik ve iplik güvenliği hakkında yalan söylemek. (Sonuçta eski işlev umurunda değil, constbu yüzden derleyici şikayet bile etmeyecektir.)

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

(4) Son olarak, mantığı takip edebilir ve sabit olmayan bir argüman alan bir kopya oluşturucu yapabilirim .

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

Bunun birçok bağlamda işe yaradığı ortaya çıkıyor, çünkü bu nesneler genellikle değil const.

Soru şu, bu geçerli veya ortak bir rota mı?

Onları adlandıramam, ancak sezgisel olarak const olmayan bir kopya oluşturucuya sahip olmanın yolunda birçok sorun bekliyorum. Muhtemelen bu incelik nedeniyle bir değer türü olarak nitelendirilmeyecektir.

(5) Son olarak, bu bir overkill gibi görünüyor ve dik bir çalışma zamanı maliyeti olabilir, ancak bir muteks ekleyebilirim:

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

Ama bunu yapmaya zorlanmak, kötümserlik gibi görünüyor ve sınıfı büyütüyor. Emin değilim. Şu anda (4) veya (5) 'e ya da her ikisinin bir kombinasyonuna yaslanıyorum .


DÜZENLEME 1:

Başka seçenek:

(6) Yinelenen üye işlevinin tüm anlamsızlıklarını unutun ve yalnızca legacy_duplicateyapıcıdan arayın ve kopya yapıcısının iş parçacığı için güvenli olmadığını bildirin. (Gerekirse, tipte başka bir iş parçacığı için güvenli bir sürüm yapın A_mt)

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

DÜZENLEME 2:

Bu, eski fonksiyonun yaptığı şey için iyi bir model olabilir. Girişe dokunulduğunda ilk argümanın temsil ettiği değere göre çağrının güvenli olmadığını unutmayın.

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

DÜZENLEME 3: Son zamanlarda, std::auto_ptrsabit olmayan bir "kopya" kurucuya sahip olmak gibi benzer bir sorunu olduğunu öğrendim . Sonuç, auto_ptrbir kabın içinde kullanılamamasıydı. https://www.quantstart.com/articles/STL-Containers-and-Auto_ptrs-Why-They-Dont-Mix/


1
" Bu eski kodda, bir nesneyi çoğaltan işlev iş parçacığı için güvenli değildir (aynı ilk bağımsız değişkeni çağırırken) " Bundan emin misiniz? İçinde Lyeni bir Lörnek oluşturularak değiştirilen bir durum var mı? Değilse, neden bu işlemin iş parçacığı için güvenli olmadığını düşünüyorsunuz?
Nicol Bolas

Evet, durum bu. İlk argümanın iç durumu, çıkarma sırasında değiştirilmiş gibi görünüyor. Herhangi bir nedenle (bazı "optimizasyon" veya kötü tasarım veya basitçe belirtim), fonksiyon legacy_duplicateiki farklı evreden aynı ilk argümanla çağrılamaz.
alfC

@TedLyngmo tamam yaptım. Her ne kadar teknik olarak c ++ pre 11 const iş parçacığı varlığında daha bulanık bir anlama sahiptir.
alfC

@TedLyngmo evet, oldukça iyi bir video. videonun sadece uygun üyelerle ilgilenmesi ve inşaat sorununa değmemesi üzücüdür (ayrıca, sabitlik “diğer” nesnede ise). Perspektifte, başka bir soyutlama katmanı (ve somut bir muteks) eklemeden kopyalamadan sonra bu sargı ipliğini güvenli hale getirmenin kendine özgü bir yolu olmayabilir.
alfC

Evet, bu beni karıştırdı ve muhtemelen constgerçekten ne anlama geldiğini bilmeyen insanlardan biriyim . :-) Ben const&değiştirmedikçe benim kopya ctor bir almak hakkında iki kez düşünmek olmaz other. Ben iplik güvenliği her zaman bir kapsülleme yoluyla, birden çok iş parçacığı erişilmesi gereken her şeyin üzerine eklediği bir şey olarak düşünüyorum ve gerçekten cevapları dört gözle bekliyorum.
Ted Lyngmo

Yanıtlar:


0

Sadece seçeneklerinizi (4) ve (5) dahil edeceğim, ancak performans için gerekli olduğunu düşündüğünüzde açıkça iş parçacığı güvensiz davranışını tercih ediyorum.

İşte tam bir örnek.

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

Çıktı:

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

Bu izler , Google stil rehberi olan constiplik güvenlik iletişim kurar, ancak API çağrısı kod devre dışı bırakabilirsiniz kullanarakconst_cast


Cevabınız için teşekkür ederim, sanırım asnwer'ınızı değiştirmez ve emin değilim ama daha iyi bir model legacy_duplicateolabilir 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; }(yani const olmayan in)
alfC

Cevabınız çok ilginç çünkü seçenek (4) ve seçenek (2) 'nin açık bir versiyonu ile birleştirilebilir. Yani, A a2(a1)iş parçacığı güvenli olmaya çalışabilir (veya silinebilir) ve A a2(const_cast<A&>(a1))hiç iş parçacığı güvenli olmaya çalışamazdı.
alfC

2
Evet, Ahem iş parçacığı için güvenli hem de iş parçacığı için güvenli olmayan bağlamlarda kullanmayı planlıyorsanız, iş const_castparçacığı güvenliğinin nerede ihlal edildiğinin bilinmesi için arama kodunu çekmelisiniz. API'nın (mutex) arkasına fazladan güvenlik sağlamak iyidir, ancak güvensizliği gizlemek uygun değildir (const_cast).
Michael Graczyk

0

TLDR: Ya çoğaltılması fonksiyonunun uygulamayı düzeltin veya muteksi tanıtmak (veya biraz daha uygun kilitleme cihazı, belki bir sayaç kilidi veya daha ağır bir şey yapmadan önce emin muteks dönüş için yapılandırılmış olduğundan emin olun) şimdilik , daha sonra çoğaltılması uygulamayı düzeltin ve kilitlenme gerçekten bir sorun haline geldiğinde kilidi çıkarın.

Dikkat edilmesi gereken önemli bir nokta, daha önce var olmayan bir özellik eklediğinizdir: Bir nesneyi aynı anda birden çok iş parçacığından çoğaltma yeteneği.

Açıkçası, beyan ettiğiniz koşullar altında, bu bir hata olurdu - daha önce yapmış olsaydınız, bir tür harici senkronizasyon kullanmadan bir yarış koşulu.

Bu nedenle, bu yeni özelliğin herhangi bir şekilde kullanılması, mevcut işlevsellik olarak miras değil, kodunuza eklediğiniz bir şey olacaktır. Sen biri olmalıdır bilir bu yeni özelliğini kullanarak olacak ne sıklıkta bağlı olarak - Ekstra kilitleme ekleyerek aslında masraflı olup olmayacağını.

Ayrıca, nesnenin algılanan karmaşıklığına dayanarak - ona verdiğiniz özel işlemle, çoğaltma prosedürünün önemsiz olmadığını varsayıyorum, bu nedenle performans açısından zaten oldukça pahalı.

Yukarıdakilere dayanarak, takip edebileceğiniz iki yol vardır:

A) Bu nesneyi birden çok iş parçacığından kopyalamanın, ek kilitlemenin ek yükünün maliyetli olması için yeterli olmayacağını biliyorsunuz - en azından mevcut çoğaltma prosedürünün kendi başına yeterince pahalı olduğu göz önüne alındığında, belki de önemsiz derecede ucuz. spinlock / pre-spinning mutex, ve üzerinde çekişme yok.

B) Birden fazla iş parçacığından kopyalamanın, ekstra kilitlemenin bir sorun olmasına yetecek kadar sık ​​olacağından şüpheleniyorsunuz. O zaman gerçekten sadece bir seçeneğiniz var - çoğaltma kodunuzu düzeltin. Düzeltmezseniz, bu soyutlama katmanında veya başka bir yerde yine de kilitlemeniz gerekir, ancak böcek istemiyorsanız buna ihtiyacınız olacaktır - ve belirlediğimiz gibi, bu yolda, kilitleme çok maliyetli olacaktır, bu nedenle tek seçenek çoğaltma kodunu düzeltmektir.

Gerçekten A durumunda olduğunuzdan ve sadece tartışmasız performans cezasına yakın olmayan bir spinlock / spinning muteksi eklediğinizden şüpheleniyorum, iyi çalışacaktır (yine de karşılaştırmayı unutmayın).

Teorik olarak başka bir durum daha vardır:

C) Çoğaltma fonksiyonunun karmaşıklığının aksine, aslında önemsizdir, ancak bir nedenden dolayı düzeltilemez; o kadar önemsizdir ki, tartışmasız bir spinlock bile tekrarlamaya kabul edilemez bir performans düşüşü getirir; paralel ipliklerin çoğaltılması nadiren kullanılır; tek bir iş parçacığında tekrarlama her zaman kullanılır, bu da performans düşüşünü kesinlikle kabul edilemez hale getirir.

Bu durumda, aşağıdakileri öneriyorum: herhangi birinin yanlışlıkla kullanmasını önlemek için varsayılan kopya kurucularının / işleçlerinin silindiğini bildirme. Açıkça çağrılabilir iki çoğaltma yöntemi, güvenli bir iş parçacığı ve güvenli olmayan bir iş parçacığı oluşturma; bağlama bağlı olarak kullanıcılarınızın onları açıkça aramalarını sağlayın. Eğer gerçekten bu durumdayız ve sadece, yine, kabul edilebilir tek iplik performansı ve güvenli çoklu diş elde etmenin başka hiçbir yolu yoktur olamaz mevcut çoğaltılması uygulamayı düzeltin. Ama gerçekten olman pek olası değil.

Mutex / spinlock ve benchmark'ı eklemeniz yeterli.


Beni C ++ 'da spinlock / spinning mutex üzerindeki malzemeye yönlendirebilir misiniz? Sağladığı şeyden daha karmaşık bir şey std::mutexmi var? Yinelenen işlev bir sır değildir, sorunu yüksek seviyede tutmayı ve MPI hakkında cevap almamayı söylemedim. Ama o kadar derine inince sana daha fazla ayrıntı verebilirim. Eski işlev MPI_Comm_dupve etkili iş parçacığı olmayan güvenlik burada açıklanmıştır (onayladım) github.com/pmodels/mpich/issues/3234 . Bu yüzden çoğaltmayı düzeltemiyorum. (Ayrıca, bir muteks
eklersem

Ne yazık ki fazla std :: mutex bilmiyorum, ama sanırım işlem uyku izin vermeden önce bazı spinning yapar. Bunu manuel olarak kontrol edebileceğiniz iyi bilinen bir senkronizasyon cihazı: docs.microsoft.com/en-us/windows/win32/api/synchapi/… Performansı karşılaştırmadım, ancak std :: mutex artık üstün: stackoverflow.com/questions/9997473/… ve şununla
DeducibleSteak

Bu dikkate alınması gereken genel hususların iyi bir açıklaması gibi görünüyor: stackoverflow.com/questions/5869825/…
DeducibleSteak

Tekrar teşekkürler, eğer önemliyse Linux'tayım.
alfC

İşte biraz ayrıntılı performans karşılaştırması (farklı bir dil için, ama sanırım bu bilgilendirici ve ne olacağını gösterecek şekilde): matklad.github.io/2020/01/04/… TLDR - spinlocks son derece küçük bir kazanır çekişme olmadığında marj, çekişme olduğunda kötü kaybedebilir.
DeducibleSteak
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.