Yazılım Geliştirme: Değişimle Yüzleşmek: İş Parçacığı İçin Güvenli Arayüz

Adanali

Active member
Yazılım Geliştirme: Değişimle Yüzleşmek: İş Parçacığı İçin Güvenli Arayüz


  1. Yazılım Geliştirme: Değişimle Yüzleşmek: İş Parçacığı İçin Güvenli Arayüz

Kalıplar, modern yazılım geliştirmede önemli bir soyutlamadır. İyi tanımlanmış terminoloji, açık belgeler sunar ve en iyisinden öğrenirler. Bugünkü yazımda yarışan modellerle ve değişimle başa çıkma yolculuğuma devam ediyorum. İş parçacığı güvenli arayüzü, nesneleri senkronize etmek için kanıtlanmış bir modeldir.







Rainer Grimm, uzun yıllardır yazılım mimarı, ekip lideri ve eğitim yöneticisi olarak çalışmaktadır. C++, Python ve Haskell programlama dilleri üzerine makaleler yazmaktan hoşlanır, aynı zamanda sık sık uzmanlık konferanslarında konuşmaktan da keyif alır. Modernes C++ blogunda yoğun bir şekilde C++ tutkusundan bahsediyor.













Bir sınıfın tüm üye işlevlerini bloke etme naif fikri, en iyi ihtimalle bir performans sorununa ve en kötü ihtimalle bir çıkmaza yol açar.

çıkmaz sokak


Aşağıdaki küçük kod parçacığında bir kilitlenme var:


struct Critical{
void memberFunction1(){
lock(mut);
memberFunction2();
...
}

void memberFunction2(){
lock(mut);
...
}

mutex mut;
};

Critical crit;
crit.memberFunction1();


çağrısı crit.memberFunction1 mutekse neden olur mut iki kez cezbedilir. Basit olması için, blok kapsamlı bir bloktur. İşte iki sorun:


  • öz lock özyinelemeli bir blok ikinci lock(mut) işlevde memberFunction2 gereksiz.
  • öz lock özyinelemeli olmayan bir bloktur, ikincisi yürütür lock(mut) İçinde memberFunction2 tanımsız davranışlara Çoğu zaman bu bir çıkmaz sokaktır.
İş parçacığı güvenli arayüzü her iki sorunu da çözer.

İş parçacığı güvenli arayüzü


Bu, iş parçacığı güvenli arayüzünün basit fikridir:

  • Arayüzün tüm işlevleri (public) bir asma kilit kullanın.
  • Tüm uygulama işlevleri (protected VE privat) asma kilit kullanmamalıdır.
  • Arabirim işlevleri yalnızca uygulama işlevlerini çağırır, ancak arabirim işlevlerini çağırmaz.
Aşağıdaki program, iş parçacığı güvenli arabiriminin kullanımını gösterir:


// threadSafeInterface.cpp

#include <iostream>
#include <mutex>
#include <thread>

class Critical{

public:
void interface1() const {
std::lock_guard<std::mutex> lockGuard(mut);
implementation1();
}

void interface2(){
std::lock_guard<std::mutex> lockGuard(mut);
implementation2();
implementation3();
implementation1();
}

private:
void implementation1() const {
std::cout << "implementation1: "
<< std::this_thread::get_id() << 'n';
}
void implementation2(){
std::cout << " implementation2: "
<< std::this_thread::get_id() << 'n';
}
void implementation3(){
std::cout << " implementation3: "
<< std::this_thread::get_id() << 'n';
}


mutable std::mutex mut; // (1)

};

int main(){

std::cout << 'n';

std::thread t1([]{
const Critical crit;
crit.interface1();
});

std::thread t2([]{
Critical crit;
crit.interface2();
crit.interface1();
});

Critical crit;
crit.interface1();
crit.interface2();

t1.join();
t2.join();

std::cout << 'n';

}


Ana iş parçacığı da dahil olmak üzere üç iş parçacığı, örneklerini kullanır Critical. İş parçacığı güvenli arayüzü sayesinde, tüm genel API çağrıları senkronize edilir. muteks mut satır (1) değişkendir ve sabit üye işlevinde kullanılabilir interface1 kullanılmalı

İş parçacığı güvenli arabiriminin avantajları üç yönlüdür:

  • Bir muteksin özyinelemeli çağrısı mümkün değildir. Özyinelemeli olmayan bir mutekse yapılan özyinelemeli çağrılar, C++’ta tanımsız davranıştır ve genellikle bir kilitlenmeye yol açar.
  • Program minimum engelleme ve dolayısıyla minimum senkronizasyon kullanır. kullanımı bir std::recursive_mutex sınıfın her üye işlevinde Critical maliyetli senkronizasyonlara yol açacaktır.
  • Kullanıcının bakış açısından Critical senkronizasyon sadece bir uygulama detayı olduğu için kullanımı kolaydır.
Her arabirim üye işlevi, işini karşılık gelen uygulayan üye işleve devreder. Dolaylı ek yük, iş parçacığı güvenli arabiriminin tipik bir dezavantajıdır.

Program çıktısı, üç iş parçacığının iç içe geçmesini gösterir.








İş parçacığı güvenli arayüzün uygulanması kolay görünse de, dikkat edilmesi gereken iki büyük tuzak vardır.

Sürmüş


Statik bir sınıf üyesi veya sanal arabirim kullanmak özel dikkat gerektirir.

statik üyeler

Sınıfınızda const olmayan bir statik üye varsa, sınıfın örneklerinde üye işlevlere yapılan tüm çağrıları eşitlemeniz gerekir.


class Critical{

public:
void interface1() const {
std::lock_guard<std::mutex> lockGuard(mut);
implementation1();
}
void interface2(){
std::lock_guard<std::mutex> lockGuard(mut);
implementation2();
implementation3();
implementation1();
}

private:
void implementation1() const {
std::cout << "implementation1: "
<< std::this_thread::get_id() << 'n';
++called;
}
void implementation2(){
std::cout << " implementation2: "
<< std::this_thread::get_id() << 'n';
++called;
}
void implementation3(){
std::cout << " implementation3: "
<< std::this_thread::get_id() << 'n';
++called;
}

inline static int called{0}; // (1)
inline static std::mutex mut;

};


Şimdi sınıf Critical statik üye called (32) uygulama işlevlerinin kaç kez çağrıldığını saymak için. Tüm örnekleri Critical aynı statik üyeyi kullanırlar ve bu nedenle eşitlenmeleri gerekir. C++17’den itibaren statik veri üyeleri de bunu yapabilir inline ilan edilecek Bir satır içi statik veri üyesi, sınıf tanımında tanımlanabilir ve başlatılabilir.

sanallık

Bir sanal arabirim işlevini geçersiz kılan herhangi biri, geçersiz kılma işlevinin bir blok kullansa bile bir blok kullandığından emin olmalıdır. privat VE.


// threadSafeInterfaceVirtual.cpp

#include <iostream>
#include <mutex>
#include <thread>

class Base{

public:
virtual void interface() {
std::lock_guard<std::mutex> lockGuard(mut);
std::cout << "Base with lock" << 'n';
}
virtual ~Base() = default;
private:
std::mutex mut;
};

class Derived: public Base{

void interface() override {
std::cout << "Derived without lock" << 'n';
}

};

int main(){

std::cout << 'n';

Base* base1 = new Derived;
base1->interface();

Derived der;
Base& base2 = der;
base2.interface();

std::cout << 'n';

}


aramalarda base1->interface VE base2.interface statik veri türüdür base1 VE base2 Base ve öyle interface erişilebilir. Çünkü üye işlevi interface sanaldır, çalışma zamanında dinamik tip aracılığıyla çağrılır Derived. Son olarak, özel üye işlevi interface sınıf Derived isminde.

Programın çıktısı, programın arayüz işlevine senkronize edilmemiş çağrıyı gösterir. Derived.








Bu sorunu çözmenin iki tipik yolu vardır:

  • Arayüz işlevi, sanal olmayan bir üye işlev haline gelir. Bu tekniğe NVI (Sanal Olmayan Arayüz) denir. Sanal olmayan üye işlevi, temel sınıf arayüz işlevinin Base kullanıldı. Ayrıca, arabirim işlevi geçersiz kılma beraberinde getirir override arabirim işlevi nedeniyle bir derleme hatasına interface sanal değil.
  • Üye işlevini bildirirsiniz interface son olarak: virtual void interface() final. Sayesinde final üzerine yazar final sanal üye işlevini bir derleme hatası olarak bildirdi.
Bu sanallık sorununu çözmek için iki yol sunmuş olsam da, NVI deyimini tercih etmenizi şiddetle tavsiye ederim. Geç bağlamaya (sanallık) ihtiyaç duymayanlar erken bağlamayı kullanmalıdır. NVI hakkında daha fazla bilgiyi Yazılım Geliştirmede Modeller: Şablon Yöntemi makalemde bulabilirsiniz.

Sıradaki ne?


Guarded Suspension, değişimle başa çıkmak için farklı bir strateji kullanır. Değişikliğiniz bittiğinde rapor verin. Bir sonraki makalemde denetimli uzaklaştırma hakkında daha fazla ayrıntıya gireceğim.


(rm)



Haberin Sonu
 
Üst