İplikler arasında veri paylaşmak için yazılım geliştirmedeki modeller
Veriler Cut uygulamalarında paylaşılmıyorsa, veri oluşturmak mümkün değildir. Yaygın kullanım, iş parçacığının yerel değişkenlerle çalıştığı anlamına gelir. Bu, verilerin kopyalanması, iş parçacıklı bellek kullanımı veya gelecekte korunan bir veri kanalı aracılığıyla ilişkili bir iş parçacığının sonucunun aktarılması ile elde edilebilir.
Rainer Grimm yıllardır yazılım mimarı, ekip ve eğitim müdürü olarak çalıştı. C ++ programlama dilleri, Python ve Haskell hakkında makaleler yazmayı seviyor, ancak uzman konferanslarla konuşmayı da seviyor. Modern C ++ blogunda, C ++ tutkusuyla yoğun bir şekilde ilgileniyor.
Bu bölümdeki modeller oldukça açıktır, ancak bunları kısa bir açıklama ile bütünlük için sunacağım. Kopyalanan değerle başlayalım.
Kopyalanan değer
Bir iş parçacığı konularını kopyalayarak alırsa, referans olarak değil, verilere erişim senkronize edilmemelidir. Veri yarışmaları ve verilerin ömrü boyunca sorun yoktur.
Referanslarla Veri Gacre
Aşağıdaki program üç iş parçacığı oluşturur. Bir iş parçacığı konusunu kopyalayarak, diğerini referans için alır ve sonuncusu sürekli referans için 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 iplik boolean değerini görüntülemeden önce bir milisaniye (1, 2 ve 3) uyur. Yalnızca T1 iş parçacığında Boolean değerinin yerel bir kopyası vardır ve bu nedenle bir veri yarışı yoktur. Programın çıktısı, T2 ve T3 ipliklerinin boole değerlerinin senkronizasyon olmadan değiştirildiğini gösterir.
Düşünce, önceki örneğin T3 iş parçacığının copiedValueDataRace.cpp basitçe std::thread t3(byConstReference, shared) Değiştirilebilir. Program derledi ve çalıştı, ancak referans gibi görünen bir kopya. Bunun nedeni, tip türlerinin çalışması std::decay Her iş parçacığı konusuna uygulanır. std::decay Örtük lValue, rvalue, diziye işaretçi ve işlevden türünü işaretçiye dönüştürmeye yol açar T Başından sonuna kadar. Özellikle, bu durumda işlevi arayın std::remove_reference Veri türünde T AÇIK.
Aşağıdaki program perConstReference.cpp Kaplı olmayan bir tür veri kullanın 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();
}
Nesne nonCopy (1) kopyalanamaz. Tamam, çalıştığımda perConstReference Konuyla nonCopy (2) Görüşler çünkü işlev konusunu sürekli referansla kabul eder. İplikte aynı işleve sahip olduğumda t (3) Kullanın, GCC 300'den fazla satırla ayrıntılı bir derleyici hatası oluşturur:
Hata mesajının temel kısmı ekran görüntüsünün ortasında kırmızı ve yuvarlak bir dikdörtgende bulunur: “Hata: Eliminasyon İşlevinin Kullanımı”. Sınıf kopya üreticisi NonCopyableClass Mevcut değil.
Bir şey ödünç alırsanız, kullanırken aşağıdaki değerin hala kullanılabilir olduğundan emin olmalısınız.
Referansların ömrünün süresi ile ilgili sorunlar
Bir iş parçacığı konusunu referans ve iş parçacığı için kullandığında detach Arayın, çok dikkatli olun. Küçük Program copiedValueLifetimeIssues.cpp Belirsiz bir 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) İki iş parçacığı başlatın. Her iki iş parçacığı da ayrılmıştır (2 ve 3) ve yerel değişkeni verir localString (4) 'den. İlk iş parçacığı, kopya için yerel değişkeni, ikincisini referans için bağlar. Basitlik nedeniyle, her iki durumda da konuları bağlamak için bir lambda ifadesi kullandım. İşlev çağından itibaren executeTwoThreads İki iş parçacığının sonuna kadar beklemeyin, iş parçacığı ifade eder t2 çağrı işlevinin süresine bağlı yerel dizeye. Bu belirsiz davranışa yol açar. Garip bir şekilde, GCC, optimize edilmiş maksimum yürütülebilir dosya ile çalışıyor gibi görünürken, unutulmaz yürütülebilir dosyası anormal olarak durur.
İş parçacığı belleği sayesinde, bir iş parçacığı verileriniz üzerinde kolayca çalışabilir.
Yerel Depolama Tartışması
Bellek iplikçisi kilitler, birkaç iş parçacığının küresel bir erişim noktası kullanarak yerel belleği kullanmasına izin verir. Spesifikasyonu kullanmak thread_local Bir değişken iş parçacığının yerel bir değişkeni haline gelirse. Bu senin demek thread-local Senkronizasyon olmadan değişkeni kullanabilir. Diyelim ki bir taşıyıcının tüm unsurlarının toplamını istiyoruz randValues hesaplamak. Bu, menzil temelli bir döngü ile kolayca uygulanabilir.
unsigned long long sum{};
for (auto n: randValues) sum += n;
Dört tohumu olan bir PC için, sıralı program eşzamanlı bir programı dönüştürür:
// 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';
}
Döngüyü bir fonksiyondaki akış hızına göre koyar ve her ipliği toplamın dörtte birini bırakır. thread_local-değişken tmpSum hesaplamak. S hattıum.fetch_add(tmpSum) (1) Sonunda tüm değerleri atom toplamına ekler. Daha fazla thread_local Arşivleme, “Yerel Veriler İş parçacığı” makalesinde okunabilir.
Vaatler ve vadeli işlemler korunan bir veri kanalını paylaşır.
Futures
C ++ 11 üç varyantta vadeli işlemler ve ünlüler sunar: std::async,, std:
ackaged_task Ve çift std:
romise VE std::future. Gelecek, sözün verdiği değer için korunan bir yer tutucudur. Senkronizasyon açısından bakıldığında, birkaç ünlünün/geleceğin belirleyici kalitesi, korunan bir veri kanalının her ikisini de birleştirmesidir. Bir geleceği uygularken bazı kararlar alınmalıdır.
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 lansman politikam yoksa, sistem işin hemen veya istek üzerine başlatılmayacağına karar verir. Lansman politikası ile std::launch::async Yeni bir iş parçacığı oluşturulur ve vaat derhal çalışmalarına başlar. Bu lansman politikasının aksine std::launch::deferred. Arama lazy.get() Söz başlıyor. Ayrıca, vaat, sonuçla birlikte get İstekler.
C ++ 'da gelecek hakkında daha fazla bilgi “Asenkron işlevin çağrıları” makalesinde mevcuttur.
Sırada ne var?
Veriler yazılmamış ve aynı anda okunmazsa veri yarışları oluşamaz. Bir sonraki makalemde, sizi değişimden korumanıza yardımcı olan şemalar üzerine yazacağım.
(RME)
Ne yazık ki, bu bağlantı artık geçerli değil.
Boşa harcanan eşyalara olan bağlantılar, 7 günlük daha büyükse veya çok sık çağrılmışsa gerçekleşmez.
Bu makaleyi okumak için bir Haberler+ paketine ihtiyacınız var. Şimdi yükümlülük olmadan bir hafta deneyin – yükümlülük olmadan!
İplikler arasında veri paylaşmak için yazılım geliştirmedeki modeller
Veriler Cut uygulamalarında paylaşılmıyorsa, veri oluşturmak mümkün değildir. Yaygın kullanım, iş parçacığının yerel değişkenlerle çalıştığı anlamına gelir. Bu, verilerin kopyalanması, iş parçacıklı bellek kullanımı veya gelecekte korunan bir veri kanalı aracılığıyla ilişkili bir iş parçacığının sonucunun aktarılması ile elde edilebilir.

Rainer Grimm yıllardır yazılım mimarı, ekip ve eğitim müdürü olarak çalıştı. C ++ programlama dilleri, Python ve Haskell hakkında makaleler yazmayı seviyor, ancak uzman konferanslarla konuşmayı da seviyor. Modern C ++ blogunda, C ++ tutkusuyla yoğun bir şekilde ilgileniyor.

Bu bölümdeki modeller oldukça açıktır, ancak bunları kısa bir açıklama ile bütünlük için sunacağım. Kopyalanan değerle başlayalım.
Kopyalanan değer
Bir iş parçacığı konularını kopyalayarak alırsa, referans olarak değil, verilere erişim senkronize edilmemelidir. Veri yarışmaları ve verilerin ömrü boyunca sorun yoktur.
Referanslarla Veri Gacre
Aşağıdaki program üç iş parçacığı oluşturur. Bir iş parçacığı konusunu kopyalayarak, diğerini referans için alır ve sonuncusu sürekli referans için 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 iplik boolean değerini görüntülemeden önce bir milisaniye (1, 2 ve 3) uyur. Yalnızca T1 iş parçacığında Boolean değerinin yerel bir kopyası vardır ve bu nedenle bir veri yarışı yoktur. Programın çıktısı, T2 ve T3 ipliklerinin boole değerlerinin senkronizasyon olmadan değiştirildiğini gösterir.

Düşünce, önceki örneğin T3 iş parçacığının copiedValueDataRace.cpp basitçe std::thread t3(byConstReference, shared) Değiştirilebilir. Program derledi ve çalıştı, ancak referans gibi görünen bir kopya. Bunun nedeni, tip türlerinin çalışması std::decay Her iş parçacığı konusuna uygulanır. std::decay Örtük lValue, rvalue, diziye işaretçi ve işlevden türünü işaretçiye dönüştürmeye yol açar T Başından sonuna kadar. Özellikle, bu durumda işlevi arayın std::remove_reference Veri türünde T AÇIK.
Aşağıdaki program perConstReference.cpp Kaplı olmayan bir tür veri kullanın 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();
}
Nesne nonCopy (1) kopyalanamaz. Tamam, çalıştığımda perConstReference Konuyla nonCopy (2) Görüşler çünkü işlev konusunu sürekli referansla kabul eder. İplikte aynı işleve sahip olduğumda t (3) Kullanın, GCC 300'den fazla satırla ayrıntılı bir derleyici hatası oluşturur:

Hata mesajının temel kısmı ekran görüntüsünün ortasında kırmızı ve yuvarlak bir dikdörtgende bulunur: “Hata: Eliminasyon İşlevinin Kullanımı”. Sınıf kopya üreticisi NonCopyableClass Mevcut değil.

Bir şey ödünç alırsanız, kullanırken aşağıdaki değerin hala kullanılabilir olduğundan emin olmalısınız.
Referansların ömrünün süresi ile ilgili sorunlar
Bir iş parçacığı konusunu referans ve iş parçacığı için kullandığında detach Arayın, çok dikkatli olun. Küçük Program copiedValueLifetimeIssues.cpp Belirsiz bir 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) İki iş parçacığı başlatın. Her iki iş parçacığı da ayrılmıştır (2 ve 3) ve yerel değişkeni verir localString (4) 'den. İlk iş parçacığı, kopya için yerel değişkeni, ikincisini referans için bağlar. Basitlik nedeniyle, her iki durumda da konuları bağlamak için bir lambda ifadesi kullandım. İşlev çağından itibaren executeTwoThreads İki iş parçacığının sonuna kadar beklemeyin, iş parçacığı ifade eder t2 çağrı işlevinin süresine bağlı yerel dizeye. Bu belirsiz davranışa yol açar. Garip bir şekilde, GCC, optimize edilmiş maksimum yürütülebilir dosya ile çalışıyor gibi görünürken, unutulmaz yürütülebilir dosyası anormal olarak durur.

İş parçacığı belleği sayesinde, bir iş parçacığı verileriniz üzerinde kolayca çalışabilir.
Yerel Depolama Tartışması
Bellek iplikçisi kilitler, birkaç iş parçacığının küresel bir erişim noktası kullanarak yerel belleği kullanmasına izin verir. Spesifikasyonu kullanmak thread_local Bir değişken iş parçacığının yerel bir değişkeni haline gelirse. Bu senin demek thread-local Senkronizasyon olmadan değişkeni kullanabilir. Diyelim ki bir taşıyıcının tüm unsurlarının toplamını istiyoruz randValues hesaplamak. Bu, menzil temelli bir döngü ile kolayca uygulanabilir.
unsigned long long sum{};
for (auto n: randValues) sum += n;
Dört tohumu olan bir PC için, sıralı program eşzamanlı bir programı dönüştürür:
// 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';
}
Döngüyü bir fonksiyondaki akış hızına göre koyar ve her ipliği toplamın dörtte birini bırakır. thread_local-değişken tmpSum hesaplamak. S hattıum.fetch_add(tmpSum) (1) Sonunda tüm değerleri atom toplamına ekler. Daha fazla thread_local Arşivleme, “Yerel Veriler İş parçacığı” makalesinde okunabilir.
Vaatler ve vadeli işlemler korunan bir veri kanalını paylaşır.
Futures
C ++ 11 üç varyantta vadeli işlemler ve ünlüler sunar: std::async,, std:
- Bir gelecek açıkça get-Fan arayın ve
- Tembel hesaplamayı (yalnızca istek üzerine) veya istekli (hemen) başlatabilir. Sadece Söz std::async Bir başlatma politikası ile 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 lansman politikam yoksa, sistem işin hemen veya istek üzerine başlatılmayacağına karar verir. Lansman politikası ile std::launch::async Yeni bir iş parçacığı oluşturulur ve vaat derhal çalışmalarına başlar. Bu lansman politikasının aksine std::launch::deferred. Arama lazy.get() Söz başlıyor. Ayrıca, vaat, sonuçla birlikte get İstekler.
C ++ 'da gelecek hakkında daha fazla bilgi “Asenkron işlevin çağrıları” makalesinde mevcuttur.
Sırada ne var?
Veriler yazılmamış ve aynı anda okunmazsa veri yarışları oluşamaz. Bir sonraki makalemde, sizi değişimden korumanıza yardımcı olan şemalar üzerine yazacağım.
(RME)
Ne yazık ki, bu bağlantı artık geçerli değil.
Boşa harcanan eşyalara olan bağlantılar, 7 günlük daha büyükse veya çok sık çağrılmışsa gerçekleşmez.
Bu makaleyi okumak için bir Haberler+ paketine ihtiyacınız var. Şimdi yükümlülük olmadan bir hafta deneyin – yükümlülük olmadan!