C ++ 'da taşınabilir türlerdeki mutekslerle nasıl başa çıkmalıyım?


86

Tasarım gereği std::mutextaşınabilir veya kopyalanamaz. Bu, Amuteks içeren bir sınıfın varsayılan bir hareket yapıcısı almayacağı anlamına gelir .

Bu türü Aiş parçacığı açısından güvenli bir şekilde nasıl taşınabilir hale getirebilirim ?


4
Sorunun bir tuhaflığı var: Taşıma işleminin kendisi de iş parçacığı açısından güvenli mi yoksa nesneye diğer erişimlerin iş parçacığı açısından güvenli olması yeterli mi?
Jonas Schäfer

2
@paulm Bu gerçekten tasarıma bağlı. Sık sık bir sınıfın bir mutex üye değişkeni olduğunu, ardından yalnızca std::lock_guardis yöntemi kapsamına alındığını gördüm .
Cory Kramer

2
@Jonas Wielicki: İlk başta onu hareket ettirmenin de iş parçacığı için güvenli olması gerektiğini düşündüm. Bununla birlikte, bir daha düşündüğümden değil, bu pek mantıklı değil, çünkü bir nesneyi hareket ettirmek genellikle eski nesnenin durumunu geçersiz kılar. Yani diğer bir konu olmamalıdır , aksi takdirde yakında geçersiz nesneye erişim olabilir .. o Taşınacak gidiyorsa, eski nesneye erişmek mümkün. Haklı mıyım
Jack Sabbath

2
lütfen bu bağlantıyı takip edin tam olarak kullanabilir justsoftwaresolutions.co.uk/threading/…
Ravi Chauhan

1
@Dieter Lücking: evet, fikir bu .. mutex M, B sınıfını korur. Ancak, iş parçacığı için güvenli, erişilebilir bir nesneye sahip olmak için her ikisini de nerede saklayabilirim? Hem M hem de B, A sınıfına gidebilir ve bu durumda A sınıfı, sınıf kapsamında bir Mutex'e sahip olacaktır.
Jack Sabbath

Yanıtlar:


105

Biraz kodla başlayalım:

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:
    ...

İçine, C ++ 11'de gerçekten yararlanmayacağımız, ancak C ++ 14'te çok daha kullanışlı hale geldiğimiz oldukça anlamlı tür takma adları koydum. Sabırlı olun, oraya geleceğiz.

Sorunuz şu şekilde özetlenebilir:

Bu sınıf için taşıma yapıcısını ve taşıma atama operatörünü nasıl yazarım?

Hareket oluşturucu ile başlayacağız.

Oluşturucuyu Taşı

Üyenin mutexyapıldığını unutmayın mutable. Kesin konuşmak gerekirse, bu taşınma üyeleri için gerekli değildir, ancak siz de kopya üyeler istediğinizi varsayıyorum. Eğer durum bu değilse, muteks yapmaya gerek yoktur mutable.

İnşa Aederken kilitlemenize gerek yoktur this->mut_. Ancak mut_inşa ettiğiniz nesneyi kilitlemeniz gerekir (taşıyın veya kopyalayın). Bu şu şekilde yapılabilir:

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

thisİlk önce üyelerini varsayılan olarak oluşturmamız ve ardından onlara yalnızca a.mut_kilitlendikten sonra değerler atamamız gerektiğine dikkat edin.

Atamayı Taşı

Taşıma atama operatörü önemli ölçüde daha karmaşıktır çünkü başka bir iş parçacığının atama ifadesinin lhs veya rhs değerlerine erişip erişmediğini bilmiyorsunuz. Ve genel olarak, aşağıdaki senaryoya karşı önlem almanız gerekir:

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

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

Yukarıdaki senaryoyu doğru bir şekilde koruyan taşıma atama operatörü:

    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)İki muteksi birbiri ardına kilitlemek yerine, iki muteksi kilitlemek için kullanılması gerektiğini unutmayın . Bunları birbiri ardına kilitlerseniz, iki iş parçacığı yukarıda gösterildiği gibi iki nesneyi zıt sırayla atadığında, bir kilitlenme elde edebilirsiniz. Noktasıstd::lock bu çıkmazdan kaçınmaktır.

Oluşturucuyu Kopyala

Kopya üyeler hakkında soru sormadınız, ancak şimdi onlar hakkında konuşabiliriz (siz değilseniz, birinin onlara ihtiyacı olacak).

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

Kopya yapıcısı, taşıma yapıcısına çok benziyor, ancak ReadLockbunun dışında takma ad WriteLock. Şu anda bunlar hem takma adlar hem de bu std::unique_lock<std::mutex>yüzden gerçekten bir fark yaratmıyor.

Ancak C ++ 14'te şunu söyleme seçeneğiniz olacak:

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

Bu bir optimizasyon olabilir , ancak kesinlikle değil. Öyle olup olmadığını belirlemek için ölçmeniz gerekecek. Ama bu değişiklik ile, bir yapısı kopyalayabilirsiniz dan aynı anda birden fazla iş parçacığı aynı rhs'sine. C ++ 11 çözümü, rhs değiştirilmese bile sizi bu tür konuları sıralı yapmaya zorlar.

Ödevi Kopyala

Tamlık için, diğer her şeyi okuduktan sonra oldukça açıklayıcı olması gereken kopya atama operatörü burada:

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

Ve benzeri.

ABirden çok iş parçacığının aynı anda arayabilmesini bekliyorsanız, durumuna erişen diğer üyelerin veya ücretsiz işlevlerin de korunması gerekir. Örneğin, burada 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_);
        }
    }

Yalnızca std::swapişi yapmaya bağlıysanız , kilitlemenin yanlış ayrıntı düzeyinde std::swapolacağını ve dahili olarak gerçekleştirilecek üç hareket arasında kilitlenip açılacağını unutmayın.

Aslında, hakkında düşünmek swapsize API konusunda size bir "iş parçacığı güvenli" sağlamanız gerekebilecek bir fikir verebilir. ABu, genel olarak "parçacıklı kilitlenme" sorunu nedeniyle "iş parçacığı güvenli olmayan" bir API'den farklı olacaktır.

Ayrıca "kendi kendini değiştirmeye" karşı koruma gerekliliğine de dikkat edin. "kendi kendini değiştirme" işlemsiz olmalıdır. Kendi kendine kontrol olmadan, aynı muteksi özyinelemeli olarak kilitlerdi. Bu, kendi kendine kontrol olmadan da for kullanılarak çözülebilir std::recursive_mutex.MutexType .

Güncelleme

Aşağıdaki yorumlarda Yakk, kopyalamadaki şeyleri varsayılan olarak inşa etmek ve kurucuları taşımak zorunda kaldığı için oldukça mutsuzdur (ve bir anlamı var). Bu konu hakkında yeterince güçlü hissediyorsan, o kadar ki konu üzerine hafıza harcamaya istekli olursan, bundan böyle kaçınabilirsin:

  • Veri üyeleri olarak ihtiyacınız olan kilit türlerini ekleyin. Bu üyeler, korunan verilerden önce gelmelidir:

    mutable MutexType mut_;
    ReadLock  read_lock_;
    WriteLock write_lock_;
    // ... other data members ...
    
  • Ve sonra yapıcılarda (ör. Kopya oluşturucu) şunu yapın:

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

Hay aksi, Yakk, ben bu güncellemeyi tamamlama şansım olmadan yorumunu sildi. Ancak bu sorunu zorladığı ve bu cevaba bir çözüm bulduğu için övgüyü hak ediyor.

Güncelleme 2

Ve dyp şu güzel öneriyle geldi:

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

2
Kopyalama yapıcınız alanları atar, kopyalamaz. Bu, talihsiz bir kısıtlama olan varsayılan olarak yapılandırılabilir olmaları gerektiği anlamına gelir.
Yakk - Adam Nevraumont

@Yakk: Evet, mutexessınıf tiplerine girmek "tek doğru yol" değildir. Araç kutusundaki bir araçtır ve kullanmak istiyorsanız, bu nasıl yapılır.
Howard Hinnant

@Yakk: Cevabımda "C ++ 14" dizesini arayın.
Howard Hinnant

ah, üzgünüm, o C ++ 14 bitini kaçırdım.
Yakk - Adam Nevraumont

2
harika açıklama @HowardHinnant! C ++ 17'de std :: scoped_lock lock (x.mut_, y_mut_) da kullanabilirsiniz; Bu şekilde, birkaç muteksi uygun bir sırayla kilitlemek için uygulamaya güvenirsiniz
fen

7

Buna cevap vermenin güzel, temiz ve kolay bir yolu yok gibi görünüyor - Anton'un çözümünün doğru olduğunu düşünüyorum ama kesinlikle tartışmalı, daha iyi bir cevap gelmedikçe, yığına böyle bir sınıf koymanızı ve ona bakmanızı tavsiye ederim. aracılığıyla std::unique_ptr:

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

Artık tamamen hareketli bir tür ve bir hareket olurken dahili mutekse kilitlenen herkes, bunun yapılacak iyi bir şey olup olmadığı tartışılabilir olsa bile hala güvenlidir.

Kopyalama anlamlarına ihtiyacınız varsa, sadece

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

5

Bu ters bir cevaptır. Tipin temeli olarak "bu nesnelerin senkronize edilmesi gerekir" yerine, onu altına enjekte edin herhangi bir tür.

Senkronize bir nesneyle çok farklı şekilde ilgilenirsiniz. Büyük bir sorun, kilitlenmeler konusunda endişelenmenizdir (birden fazla nesneyi kilitlemek). Ayrıca, temelde asla "bir nesnenin varsayılan sürümü" olmamalıdır: senkronize nesneler çekişme içinde olacak nesneler içindir ve amacınız ipler arasındaki çekişmeyi asgariye indirmek olmalıdır, halının altına süpürmek değil.

Ancak nesneleri senkronize etmek yine de faydalıdır. Bir eşzamanlayıcıdan miras almak yerine, eşzamanlamada rastgele bir türü saran bir sınıf yazabiliriz. Kullanıcılar artık senkronize olduğundan nesne üzerinde işlem yapmak için birkaç çemberden geçmek zorundadır, ancak bunlar nesne üzerinde elle kodlanmış bazı sınırlı işlemler ile sınırlı değildir. Nesne üzerinde birden çok işlemi bir arada oluşturabilir veya birden çok nesne üzerinde işlem yapabilirler.

Aşağıda, rastgele bir tür etrafında senkronize edilmiş bir sarmalayıcı verilmiştir 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 ve C ++ 1z özellikleri dahildir.

bu, constişlemlerin çok okuyucunun güvenli olduğunu varsayar (bu, stdkapsayıcıların varsaydığı şeydir ).

Kullanım şöyle görünür:

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

Bir için intsenkronize erişimli.

Sahip olmamanızı tavsiye ederim synchronized(synchronized const&). Nadiren ihtiyaç duyulur.

Eğer ihtiyacın olursa synchronized(synchronized const&), T t;şununla değiştirmek isterim kistd::aligned_storage manuel yerleştirme konstrüksiyona sahip olan ve manuel yıkımını yapmak. Bu, uygun ömür yönetimi sağlar.

Bunun dışında kaynağı kopyalayabilir ve Tondan okuyabiliriz:

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

görev için:

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

yerleştirme ve hizalanmış depolama sürümleri biraz daha karmaşıktır. Öğesine erişimin çoğu tbir üye işlevi ile değiştirilir T&t()veT const&t()const bazı çemberlerden atlamanız gereken inşaatlar dışında.

synchronizedSınıfın bir parçası yerine bir sarmalayıcı yaparak , tek yapmamız gereken sınıfın içsel olarak saygı duyduğudur.const olarak çok okuyuculu ve onu tek iş parçacıklı bir şekilde yazmasıdır.

In nadir bir senkronize örneğini ihtiyaç durumlarda, yukarıdaki gibi çemberin içinden atlamak.

Yukarıdaki yazım hataları için özür dileriz. Muhtemelen vardır.

Yukarıdakilerin bir yan faydası, synchronizednesneler (aynı türden) üzerinde keyfi işlemlerin , önceden sabit kodlamaya gerek kalmadan birlikte çalışmasıdır. Bir arkadaş bildirimi ekleyin ve synchronizedbirden çok türden farklı nesneler birlikte çalışabilir. accessBu durumda aşırı çatışmalarla başa çıkmak için sıralı bir arkadaş olmaktan çıkmam gerekebilir .

canlı örnek


4

Muteksleri ve C ++ hareket semantiğini kullanmak, iş parçacıkları arasında güvenli ve verimli bir şekilde veri aktarımı için mükemmel bir yoldur.

Dizi grupları oluşturan ve bunları (bir veya daha fazla) tüketiciye sunan bir "üretici" ipliği hayal edin. Bu gruplar (potansiyel olarak büyük) std::vector<std::string>nesneler içeren bir nesne ile temsil edilebilir . Bu vektörlerin iç durumunu, gereksiz çoğaltma yapmadan tüketicilerine kesinlikle 'taşımak' istiyoruz.

Muteksi basitçe nesnenin durumunun bir parçası olarak değil nesnenin bir parçası olarak tanırsınız. Yani, muteksi taşımak istemezsiniz.

İhtiyacınız olan kilit, algoritmanıza veya nesnelerinizin ne kadar genel olduğuna ve ne tür kullanımlara izin verdiğinize bağlıdır.

Yalnızca paylaşılan durum 'üretici' nesnesinden iş parçacığı yerel 'tüketen' nesneye geçerseniz, yalnızca taşınan nesneyi kilitlemeniz uygun olabilir . nesne.

Daha genel bir tasarımsa, ikisini de kilitlemeniz gerekir. Böyle bir durumda, kilitlenmeyi düşünmeniz gerekir.

Bu potansiyel bir std::lock()sorunsa, her iki mutekste kilitleri kilitlenmeden almak için kullanın .

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

Son bir not olarak, hareket semantiğini anladığınızdan emin olmalısınız. Nesneden taşınan şeyin geçerli ancak bilinmeyen bir durumda kaldığını hatırlayın. Taşımayı gerçekleştirmeyen bir iş parçacığının, bu geçerli ancak bilinmeyen durumu bulduğunda taşınan nesneye erişmeye çalışmak için geçerli bir nedeni olması tamamen mümkündür.

Yine benim yapımcım ipleri dağıtıyor ve tüketici tüm yükü alıyor. Bu durumda, üretici vektöre her ekleme yapmaya çalıştığında vektörü boş veya boş bulabilir.

Kısacası, taşınan nesneye olası eşzamanlı erişim bir yazma anlamına geliyorsa, muhtemelen sorun yok. Okumaya denk geliyorsa, keyfi bir durumu okumanın neden uygun olduğunu düşünün.


3

Muteks içeren bir nesneyi taşımak istiyorsanız öncelikle tasarımınızda bir sorun olmalı.

Ancak yine de yapmaya karar verirseniz, hareket oluşturucuda yeni bir muteks oluşturmanız gerekir, yani:

// movable
struct B{};

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

Bu, iş parçacığı açısından güvenlidir, çünkü move yapıcısı, bağımsız değişkeninin başka hiçbir yerde kullanılmadığını güvenle varsayabilir, bu nedenle bağımsız değişkenin kilitlenmesi gerekmez.


2
Bu iş parçacığı için güvenli değil. Ya a.mutexkilitliyse ,: Bu durumu kaybedersiniz. -1

2
@ DieterLücking Bağımsız değişken, taşınan nesneye tek referans olduğu sürece, muteksin kilitlenmesi için mantıklı bir neden yoktur. Ve öyle olsa bile, yeni oluşturulan bir nesnenin bir muteksini kilitlemek için hiçbir neden yoktur. Ve eğer varsa, bu, mutekslere sahip hareketli nesnelerin genel olarak kötü tasarımı için bir argümandır.
Anton Savin

1
@ DieterLücking Bu sadece doğru değil. Problemi açıklayan bir kod verebilir misiniz? Ve formda değil A a; A a2(std::move(a)); do some stuff with a.
Anton Savin

2
Bununla birlikte, en iyi yol bu olsaydı new, örneği yukarı kaldırıp yerleştirmeyi tavsiye ederim std::unique_ptr- bu daha temiz görünüyor ve karışıklık sorunlarına yol açması muhtemel değil. İyi soru.
Mike Vine

1
@MikeVine Bence bunu bir cevap olarak eklemelisin.
Anton Savin
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.