Tasarım gereği std::mutex
taşınabilir veya kopyalanamaz. Bu, A
muteks içeren bir sınıfın varsayılan bir hareket yapıcısı almayacağı anlamına gelir .
Bu türü A
iş parçacığı açısından güvenli bir şekilde nasıl taşınabilir hale getirebilirim ?
Tasarım gereği std::mutex
taşınabilir veya kopyalanamaz. Bu, A
muteks içeren bir sınıfın varsayılan bir hareket yapıcısı almayacağı anlamına gelir .
Bu türü A
iş parçacığı açısından güvenli bir şekilde nasıl taşınabilir hale getirebilirim ?
std::lock_guard
is yöntemi kapsamına alındığını gördüm .
Yanıtlar:
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 mutex
yapı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 A
ederken 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 ReadLock
bunun 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.
A
Birden ç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::swap
işi yapmaya bağlıysanız , kilitlemenin yanlış ayrıntı düzeyinde std::swap
olacağını ve dahili olarak gerçekleştirilecek üç hareket arasında kilitlenip açılacağını unutmayın.
Aslında, hakkında düşünmek swap
size API konusunda size bir "iş parçacığı güvenli" sağlamanız gerekebilecek bir fikir verebilir. A
Bu, 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_)
{}
mutexes
sı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.
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>();
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, const
işlemlerin çok okuyucunun güvenli olduğunu varsayar (bu, std
kapsayı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 int
senkronize 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 T
ondan 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 t
bir üye işlevi ile değiştirilir T&t()
veT const&t()const
bazı çemberlerden atlamanız gereken inşaatlar dışında.
synchronized
Sı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ı, synchronized
nesneler (aynı türden) üzerinde keyfi işlemlerin , önceden sabit kodlamaya gerek kalmadan birlikte çalışmasıdır. Bir arkadaş bildirimi ekleyin ve synchronized
birden çok türden farklı nesneler birlikte çalışabilir. access
Bu durumda aşırı çatışmalarla başa çıkmak için sıralı bir arkadaş olmaktan çıkmam gerekebilir .
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.
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.
A a; A a2(std::move(a)); do some stuff with a
.
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.