İş parçacıkları arasında veri paylaşımı için yazılım geliştirmede kalıplar

Adanali

Active member
İş parçacıkları arasında veri paylaşımı için yazılım geliştirmede kalıplar


  1. İş parçacıkları arasında veri paylaşımı için yazılım geliştirmede kalıplar

Eşzamanlı uygulamalarda veriler paylaşılmazsa veri yarışları oluşamaz. Paylaşım olmaması, iş parçacığının yerel değişkenlerle çalıştığı anlamına gelir. Bu, verileri kopyalayarak, iş parçacığı yerel depolamasını kullanarak veya bir iş parçacığının sonucunu güvenli bir veri kanalı üzerinden ilişkili geleceğe ileterek gerçekleştirilebilir.







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.













Bu bölümdeki kalıplar oldukça açıktır, ancak tam olması için bunları kısa bir açıklama ile tanıtacağım. Kopyalanan değerle başlıyoruz.

Değer kopyalandı


Bir iş parçacığı argümanlarını referans yerine kopya ile aldığında, veri erişimini senkronize etmeye gerek yoktur. Veri yarışları ve veri süresi sorunları yoktur.

Veriler referanslarla çalışır

Aşağıdaki program üç iş parçacığı oluşturur. Bir iş parçacığı bağımsız değişkenini kopyalayarak, diğeri referansla ve sonuncusu da sabit referansla alır.


// copiedValueDataRace.cpp

#include <functional>
#include <iostream>
#include <string>
#include <thread>

using namespace std::chrono_literals;

void byCopy(bool b){
std::this_thread::sleep_for(1ms); // (1)
std::cout << "byCopy: " << b << 'n';
}

void byReference(bool& b){
std::this_thread::sleep_for(1ms); // (2)
std::cout << "byReference: " << b << 'n';
}

void byConstReference(const bool& b){
std::this_thread::sleep_for(1ms); // (3)
std::cout << "byConstReference: " << b << 'n';
}

int main(){

std::cout << std::boolalpha << 'n';

bool shared{false};

std::thread t1(byCopy, shared);
std::thread t2(byReference, std::ref(shared));
std::thread t3(byConstReference, std::cref(shared));

shared = true;

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

std::cout << 'n';

}


Her iş parçacığı, boole değerini göstermeden önce bir milisaniye (1, 2 ve 3) uyur. Yalnızca t1 iş parçacığı boolenin yerel bir kopyasına sahiptir ve bu nedenle yarış verisi yoktur. Program çıktısı, t2 ve t3 iş parçacıklarının boolean değerlerinin senkronize olmayan şekilde değiştiğini gösterir.








Bariz fikir, önceki örnekteki t3 ipliğinin copiedValueDataRace.cpp sadece aracılığıyla std::thread t3(byConstReference, shared) Değiştirilebilir. Program derlenir ve çalışır, ancak referans gibi görünen şey bir kopyadır. Bunun nedeni, Tip Özelliklerinin std::decay her iş parçacığı konusuna uygulanır. std::decay lValue’dan rValue’ye, diziden işaretçiye ve işlevden kendi türündeki işaretçiye örtük dönüştürme gerçekleştirir T Başından sonuna kadar. Spesifik olarak, bu durumda işlevi çağırın std::remove_reference veri türü üzerinde T AÇIK.

Aşağıdaki program perConstReference.cpp kopyalanamayan bir veri türü kullanır NonCopyableClass.


// perConstReference.cpp

#include <thread>

class NonCopyableClass{
public:

// the compiler generated default constructor
NonCopyableClass() = default;

// disallow copying
NonCopyableClass& operator =
(const NonCopyableClass&) = delete;
NonCopyableClass (const NonCopyableClass&) = delete;

};

void perConstReference(const NonCopyableClass& nonCopy){}

int main(){

NonCopyableClass nonCopy; // (1)

perConstReference(nonCopy); // (2)

std::thread t(perConstReference, nonCopy); // (3)
t.join();

}


Eşya nonCopy (1) kopyalanamaz. İşlevi kullanırsam sorun değil perConstReference argüman ile nonCopy (2) çağrı, çünkü işlev bağımsız değişkenini sabit referansla alır. Eğer iş parçacığında aynı işleve sahipsem t (3), GCC, 300’den fazla satır içeren ayrıntılı bir derleyici hatası üretir:








Hata mesajının ana kısmı, ekran görüntüsünün ortasında yuvarlak kırmızı bir dikdörtgen içinde yer alır: “hata: kullanım işlevi silindi”. Sınıf kopya yapıcısı NonCopyableClass Müsait değil.








Bir şeyi ödünç alan herkes, kullanım sırasında temel değerin hala mevcut olduğundan emin olmalıdır.

Referans süresi sorunları

Bir iş parçacığı bağımsız değişkenini referans olarak alıyorsa ve bir İş Parçacığı detach aramalarda son derece dikkatli olunması gerekir. küçük program copiedValueLifetimeIssues.cpp tanımsız davranışa sahiptir.


// copiedValueLifetimeIssues.cpp

#include <iostream>
#include <string>
#include <thread>

void executeTwoThreads(){ // (1)

const std::string localString("local string"); // (4)

std::thread t1([localString]{
std::cout << "Per Copy: " << localString << 'n';
});

std::thread t2([&localString]{
std::cout << "Per Reference: " << localString << 'n';
});

t1.detach(); // (2)
t2.detach(); // (3)
}

using namespace std::chrono_literals;

int main(){

std::cout << 'n';

executeTwoThreads();

std::this_thread::sleep_for(1s);

std::cout << 'n';

}


executeTwoThreads (1) iki iş parçacığı başlatın. Her iki iş parçacığının bağlantısı kesilir (2 ve 3) ve yerel değişkeni döndürür localString (4)’ten. İlk iş parçacığı, yerel değişkeni kopyalayarak ve ikincisini referans olarak bağlar. Basit olması için, her iki durumda da bağımsız değişkenleri bağlamak için bir lambda ifadesi kullandım. Çünkü fonksiyon executeTwoThreads iki iş parçacığının bitmesini beklemez, iş parçacığı başvurur t2 çağıran işlevin süresiyle ilişkili yerel dizeye. Bu tanımsız davranışa yol açar. Garip bir şekilde, GCC ile, optimize edilmemiş yürütülebilir dosya çökerken, maksimum optimize edilmiş yürütülebilir dosya -O3 çalışıyor gibi görünüyor.








İş parçacığı yerel depolaması sayesinde, bir iş parçacığı verileri üzerinde kolayca çalışabilir.

Yerel depolama iş parçacığı


İş parçacığı yerel depolaması, birden çok iş parçacığının yerel depolamayı küresel bir erişim noktası aracılığıyla paylaşmasına olanak tanır. Belirleyiciyi kullanma thread_local bir değişken iş parçacığı yerel değişkeni haline gelir. Bu senin öldüğün anlamına geliyor thread-local senkronizasyon olmadan değişken. Bir vektörün tüm öğelerinin toplamını istediğimizi varsayalım. randValues hesaplamak. Bu, aralık tabanlı bir for döngüsü ile kolayca uygulanabilir.


unsigned long long sum{};
for (auto n: randValues) sum += n;


Dört çekirdekli bir PC için sıralı programı eşzamanlı bir programa çevirin:


// threadLocallSummation.cpp

#include <atomic>
#include <iostream>
#include <random>
#include <thread>
#include <utility>
#include <vector>

constexpr long long size = 10000000;

constexpr long long fir = 2500000;
constexpr long long sec = 5000000;
constexpr long long thi = 7500000;
constexpr long long fou = 10000000;

thread_local unsigned long long tmpSum = 0;

void sumUp(std::atomic<unsigned long long>& sum,
const std::vector<int>& val,
unsigned long long beg, unsigned long long end) {
for (auto i = beg; i < end; ++i){
tmpSum += val;
}
sum.fetch_add(tmpSum);
}

int main(){

std::cout << 'n';

std::vector<int> randValues;
randValues.reserve(size);

std::mt19937 engine;
std::uniform_int_distribution<> uniformDist(1, 10);
for (long long i = 0; i < size; ++i)
randValues.push_back(uniformDist(engine));

std::atomic<unsigned long long> sum{};

std::thread t1(sumUp, std::ref(sum),
std::ref(randValues), 0, fir);
std::thread t2(sumUp, std::ref(sum),
std::ref(randValues), fir, sec);
std::thread t3(sumUp, std::ref(sum),
std::ref(randValues), sec, thi);
std::thread t4(sumUp, std::ref(sum),
std::ref(randValues), thi, fou);

t1.join();
t2.join();
t3.join();
t4.join();

std::cout << "Result: " << sum << 'n';

std::cout << 'n';

}


Menzil tabanlı for döngüsünü bir işleve sararsınız ve her iş parçacığının toplamın dörtte birini dosyada tutmasına izin verirsiniz thread_local-değişken tmpSum hesaplamak. çizgilerum.fetch_add(tmpSum) (1) nihayet atomik toplamdaki tüm değerleri toplayın. Dahası thread_local Bellek, “Yerel iş parçacığı verileri” makalesinde okunabilir.

Vaatler ve vadeli işlemler güvenli bir veri kanalını paylaşır.

gelecek


C++11, üç farklı şekilde vadeli işlemler ve vaatler sunar: std::async, std::packaged_task ve çift std::promise VE std::future. Gelecek, sözle oluşturulan değer için korunan bir yer tutucudur. Senkronizasyon açısından, bir söz/gelecek çiftinin can alıcı özelliği, güvenli bir veri kanalının ikisini birbirine bağlamasıdır. Bir geleceği hayata geçirirken alınması gereken bazı kararlar vardır.

  • Bir gelecek, değerini açıkça ifade edebilir. get– arama sorgusu ve
  • tembel (yalnızca talep üzerine) veya istekli (hemen) hesaplamaya başlayabilir. Sadece söz std::async kullanıma sunma politikasıyla tembel değerlendirmeyi destekler.

auto lazyOrEager = std::async([]{ return "LazyOrEager"; });
auto lazy = std::async(std::launch::deferred,
[]{ return "Lazy"; });
auto eager = std::async(std::launch::async, []{ return "Eager"; });

lazyOrEager.get();
lazy.get();
eager.get();


Bir başlatma politikası belirtmezseniz, sistem işlemin hemen mi yoksa istek üzerine mi başlatılacağına karar verir. Başlatma politikası ile std::launch::async yeni bir iş parçacığı oluşturulur ve söz hemen çalışmaya başlar. Bu fırlatma politikasına aykırı std::launch::deferred. Arama lazy.get() söz başlıyor. Ayrıca, söz, sonucu alan iş parçacığında yürütülür. get istekler.

“Eşzamansız İşlev Çağrıları” makalesinde C++’daki vadeli işlemler hakkında daha fazla bilgi edinin.

Sıradaki ne?


Veriler aynı anda yazılmaz ve okunmazsa veri yarışları gerçekleşemez. Bir sonraki yazımda sizi değişimden korumaya yardımcı olan şablonlar hakkında yazacağım.


(rm)



Haberin Sonu
 
Üst