C++ Senkronizasyon Mekanizmaları
Giriş
Bu yazıda C++ konusunda threading, senkronizasyon mekanizmaları, semaforlar ve async fonksiyonlar gibi konuları ele aldım. Senkronizasyon mekanizmaları arasında mutex, lock_guard, unique_lock ve condition_variable gibi yapıları öğreneceğiz. Aynı zamanda semafor türlerini ve senkronizasyon problemlerini de ele alacağız. Ayrıca, async fonksiyonları ve std::future kütüphanesini kullanarak asenkron işlemleri yönetmeyi öğreneceğiz.
Bu yazı, C++ dilinde çoklu iş parçacıklı ve paralel programlamaya giriş niteliğindedir. Bu konuların anlaşılması, performansı artırmak ve verimli kod yazmak için önemlidir.
Senkronizasyon Mekanizmaları Hakkında Özet
C++ dilindeki senkronizasyon mekanizmaları, farklı senaryolara göre çeşitlilik gösterir. İşte C++’ta kullanılan bazı temel senkronizasyon mekanizmaları:
-
std::mutex
:std::mutex
(mutex) bir kilit mekanizmasıdır ve paylaşılan bir kaynağa eşzamanlı erişimi kontrol etmek için kullanılır. İş parçacıklarının birstd::mutex
kilidini almasına izin verilirken, diğer iş parçacıklarının aynı kilit üzerinde çalışmasını engeller. -
std::recursive_mutex
:std::recursive_mutex
(yeniden girişimli mutex), birstd::mutex
gibi çalışır, ancak aynı iş parçacığı tarafından birden fazla kez kilitleme yapılmasına izin verir. Yeniden girişimli mutex, aynı iş parçacığı içinde fonksiyon çağrıları veya özyinelemeli işlemler gibi durumlarda kullanışlı olabilir. -
std::lock_guard
:std::lock_guard
bir kilit tutucu sınıftır ve otomatik kilit açma ve kilitleme sağlar.std::lock_guard
nesnesi, oluşturulduğunda birstd::mutex
‘i kilitleyerek, nesnenin kapsamından çıkıldığında otomatik olarak kilidi açar. -
std::unique_lock
:std::unique_lock
,std::lock_guard
gibi bir kilit tutucu sınıftır, ancak daha fazla esneklik sağlar.std::unique_lock
, farklı kilitleme stratejilerini destekler ve kilidin manuel olarak açılıp kapatılmasına izin verir. -
std::condition_variable
:std::condition_variable
, iş parçacıklarının bir olayın gerçekleşmesini beklemesini ve bildirmesini sağlayan bir senkronizasyon mekanizmasıdır. İş parçacıkları, bir koşulu kontrol eder ve o koşulun gerçekleşmesini beklerkenstd::condition_variable
‘ınwait
işlevini kullanır. Diğer iş parçacıkları, koşulu sağlayan bir olayın gerçekleştiğini bildirirkennotify_one
veyanotify_all
işlevlerini kullanır. -
std::atomic
:std::atomic
veri türleri, belirli bir bellek konumunda eşzamanlı erişimi sağlamak için kullanılır. Bu türler, belirli bir operasyonu atomik olarak gerçekleştirir ve senkronizasyon mekanizmaları olmadan iş parçacıkları arasında güvenli bir şekilde değiş tokuş yapmayı sağlar. -
std::atomic_flag
:std::atomic_flag
, bir bayrak türüdür ve sadece iki durumu (set ve clear) olan basit bir senkronizasyon mekanizması sağlar.std::atomic_flag
‘ıntest_and_set
veyaclear
işlevleri kullanılarak bayrağın durumu değiştirilir.
Bu, C++ dilinde kullanılan bazı temel senkronizasyon mekanizmalarının bir özeti idi. Her senaryo farklı gereksinimler gerektirebilir, bu nedenle doğru senkronizasyon mekanizmasını seçmek önemlidir. C++ standart kütüphanesi bu mekanizmaları sağlar ve bu mekanizmaların kullanımı, paralel programlama ve eşzamanlılık konularında daha fazla bilgi ve anlayış gerektirebilir.
1. Mutex
C++ programlamasında senkronizasyonu sağlamak için kullanılan bir senkronizasyon mekanizmasıdır. Mutex, bir kilit (lock) olarak da adlandırılır ve paylaşılan bir kaynağa aynı anda sadece bir iş parçacığının erişmesine izin verir. Bu şekilde, senkronizasyon eksikliği nedeniyle kaynakların tutarlılığına veya güvenliğine yönelik sorunlar önlenir.
Mutex’ler, iki temel işlemi destekler:
-
Kilitleme (Locking): Bir iş parçacığı, kaynağa erişmek istediğinde mutex’i kilitleyerek diğer iş parçacıklarının aynı kaynağa erişmesini engeller. Diğer bir deyişle, mutex’i kilitleyen iş parçacığı, kaynağı geçici olarak kendine tahsis eder.
-
Kilidi Açma (Unlocking): İş parçacığı, kaynağı kullanımını tamamladıktan sonra mutex’in kilidini açarak diğer iş parçacıklarının kaynağa erişimine izin verir.
C++ dilinde, std::mutex
sınıfı C++ standart kütüphanesinde mutex’i temsil eder. İşte bir örnek:
#include <iostream>
#include <mutex>
#include <thread>
std::mutex mtx; // Mutex nesnesi
void WorkerThread() {
mtx.lock(); // Mutex'i kilitle
// Kritik bölge: Paylaşılan kaynağa erişim
std::cout << "İş parçacığı çalışıyor..." << std::endl;
// Kaynağa erişim işlemleri
std::this_thread::sleep_for(std::chrono::seconds(2));
mtx.unlock(); // Mutex kilidini aç
}
int main() {
std::thread t1(WorkerThread);
std::thread t2(WorkerThread);
t1.join();
t2.join();
return 0;
}
Bu örnekte, std::mutex
sınıfı kullanılarak bir mutex nesnesi oluşturulur. WorkerThread
işlevi, mutex’i kilitleyerek ve kilidi açarak kritik bölgeye erişir. Kritik bölge, paylaşılan kaynağa erişim sağlayan işlemleri içerir.
Ana iş parçacığı, iki WorkerThread
işlevini ayrı iş parçacıklarında çalıştırır. Her iş parçacığı sırayla mutex’i kilitleyerek kritik bölgeye erişir ve ardından mutex kilidini açar.
Bu şekilde, mutex kullanarak paylaşılan bir kaynağa eşzamanlı erişimi kontrol edebilir ve senkronizasyon sağlayabilirsiniz. Mutex’ler, paralel programlama ve eşzamanlılık konularında güvenli ve tutarlı bir paylaşımı sağlamak için önemli bir araçtır.
2. Recursive Mutex
Recursive mutex (yeniden girişimli mutex), bir mutex türüdür ve aynı iş parçacığı tarafından birden fazla kez kilitleme yapılmasına izin verir. Yani, bir iş parçacığı aynı mutex’i tekrar tekrar kilitleyebilir.
C++ dilinde, std::recursive_mutex
sınıfı C++ standart kütüphanesinde yeniden girişimli mutex’i temsil eder. İşte bir örnek:
#include <iostream>
#include <mutex>
#include <thread>
std::recursive_mutex rmtx; // Yeniden girişimli mutex nesnesi
void RecursiveFunction(int depth) {
rmtx.lock(); // Mutex'i kilitle
// Kritik bölge: Paylaşılan kaynağa erişim
std::cout << "Derinlik: " << depth << std::endl;
if (depth > 0) {
RecursiveFunction(depth - 1); // Yeniden girişim: Kendi kendini çağır
}
rmtx.unlock(); // Mutex kilidini aç
}
int main() {
RecursiveFunction(3);
return 0;
}
Bu örnekte, std::recursive_mutex
sınıfı kullanılarak bir yeniden girişimli mutex nesnesi oluşturulur. RecursiveFunction
işlevi, mutex’i kilitleyerek ve kilidi açarak kritik bölgeye erişir. Kritik bölgede, derinlik seviyesini ekrana yazdırır ve eğer derinlik sıfırdan büyükse, kendini tekrar çağırarak yeniden girişim yapar.
Ana iş parçacığı, RecursiveFunction
işlevini başlangıç derinliğiyle çağırır. Bu işlev, kendini tekrar çağırdığı için aynı mutex’i tekrar tekrar kilitleyebilir.
Yeniden girişimli mutex, aynı iş parçacığı içinde fonksiyon çağrıları veya özyinelemeli işlemler gibi durumlarda kullanışlı olabilir. Örneğin, bir iş parçacığı bir kaynağa erişim sağlarken birden fazla alt fonksiyon kullanıyorsa, yeniden girişimli mutex ile bu alt fonksiyonların da aynı mutex’i kilitleyerek kaynağa erişim sağlaması mümkündür.
Bu şekilde, yeniden girişimli mutex kullanarak paylaşılan bir kaynağa aynı iş parçacığı içinde birden fazla kez erişim sağlayabilirsiniz. Ancak, dikkat edilmesi gereken nokta, mutex’in her kilitleme işlemine karşılık bir kilidin açılması gerektiğidir. Aksi takdirde, mutex kilidi açılmadan iş parçacığı hala mutex’i kilitleyerek bekleyecektir.
2.1. Mutex vs Recursive Mutex
İşte şimdi bir karşılaştırmalı örnek verelim, aşağıdaki örneği recursive mutex (std::recursive_mutex
) ile normal mutex (std::mutex
) arasındaki farkı göstermek için kullanacağız:
#include <iostream>
#include <mutex>
#include <thread>
std::mutex mtx; // Normal mutex nesnesi
std::recursive_mutex rmtx; // Yeniden girişimli mutex nesnesi
void NormalFunction() {
mtx.lock(); // Normal mutex'i kilitle
std::cout << "Normal iş parçacığı" << std::endl;
mtx.unlock(); // Normal mutex kilidini aç
}
void RecursiveFunction(int depth) {
rmtx.lock(); // Yeniden girişimli mutex'i kilitle
std::cout << "Derinlik: " << depth << std::endl;
if (depth > 0) {
RecursiveFunction(depth - 1); // Yeniden girişim: Kendi kendini çağır
}
rmtx.unlock(); // Yeniden girişimli mutex kilidini aç
}
int main() {
std::thread t1(NormalFunction);
std::thread t2(RecursiveFunction, 3);
t1.join();
t2.join();
return 0;
}
Bu örnekte, iki farklı fonksiyon olan NormalFunction
ve RecursiveFunction
kullanılmıştır. Her iki fonksiyon da farklı bir mutex türü ile çalışır.
NormalFunction
, normal bir mutex (std::mutex
) kullanır. Her iş parçacığı, mutex’i kilitleyerek kritik bölgeye erişir. Ancak, aynı iş parçacığı tarafından tekrar çağrılmadığı sürece, mutex’i tekrar kilitleyemez. Bu durumda, aynı iş parçacığı tekrar mutex’i kilitlemeye çalıştığında, bekler ve program deadlock (kitlenme) durumuna düşer.
RecursiveFunction
, yeniden girişimli bir mutex (std::recursive_mutex
) kullanır. Bu, aynı iş parçacığı tarafından tekrar tekrar çağrılan alt fonksiyonlarla çalışmak için uygun bir seçenektir. Yeniden girişimli mutex, aynı iş parçacığı tarafından birden fazla kez kilitleme yapılmasına izin verir.
Örnekte, RecursiveFunction
derinliği azaltarak kendini tekrar tekrar çağırırken, her seferinde aynı mutex’i tekrar kilitleyebilir.
Bu şekilde, normal mutex ve yeniden girişimli mutex arasındaki farkı görebilirsiniz. Normal mutex, aynı iş parçacığı tarafından tekrar kilitleme yapamazken, yeniden girişimli mutex aynı iş parçacığı tarafından birden fazla kez kilitleme yapabilir.
3. Lock Guard
std::lock_guard
, C++ standart kütüphanesinde yer alan bir sınıftır ve mutex kilitleme ve kilidi otomatik olarak açma işlemlerini yönetmek için kullanılır. std::lock_guard
, bir mutex nesnesini kilitleyerek bir kritik bölgeye giriş yapar ve std::lock_guard
nesnesi kapsamdan çıktığında, otomatik olarak mutex’i serbest bırakır. Böylece, unutulmuş veya hatalı bir şekilde kilidi açmayı önler ve kaynakların doğru şekilde serbest bırakılmasını sağlar.
std::lock_guard
, std::mutex
veya std::recursive_mutex
gibi mutex türlerini destekler.
İşte bir örnek:
#include <iostream>
#include <mutex>
#include <thread>
std::mutex mtx; // Mutex nesnesi
void WorkerThread() {
std::lock_guard<std::mutex> lock(mtx); // Mutex'i kilitle
// Kritik bölge: Paylaşılan kaynağa erişim
std::cout << "İş parçacığı çalışıyor..." << std::endl;
// Kaynağa erişim işlemleri
std::this_thread::sleep_for(std::chrono::seconds(2));
// Otomatik olarak mutex'i serbest bırakır
}
int main() {
std::thread t1(WorkerThread);
std::thread t2(WorkerThread);
t1.join();
t2.join();
return 0;
}
Bu örnekte, std::lock_guard
sınıfı kullanılarak bir lock
nesnesi oluşturulur. lock_guard
‘ın yapıcı fonksiyonu, verilen mutex’i otomatik olarak kilitleyerek iş parçacığının kritik bölgeye giriş yapmasını sağlar. lock_guard
nesnesi, kapsamdan çıktığında (yani WorkerThread
fonksiyonundan çıkıldığında), otomatik olarak mutex’i serbest bırakır ve böylece kaynaklar doğru şekilde temizlenir.
Ana iş parçacığı, iki WorkerThread
işlevini ayrı iş parçacıklarında çalıştırır. Her iş parçacığı, std::lock_guard
kullanarak mutex’i kilitleyerek kritik bölgeye erişir ve ardından kapsamdan çıkıldığında otomatik olarak mutex’i serbest bırakır.
std::lock_guard
sınıfı, RAII (Resource Acquisition Is Initialization) idiyomunu kullanarak, kaynakların otomatik olarak temizlenmesini sağlar. Bu sayede, mutex kilidinin unutulması veya hatalı bir şekilde serbest bırakılması gibi sorunlar ortadan kalkar ve kodun daha güvenli hale gelmesi sağlanır.
4. Unique Lock
std::unique_lock
, C++ standart kütüphanesinde yer alan bir sınıftır ve mutex kilitleme ve kilidi açma işlemlerini yönetmek için kullanılır. std::unique_lock
sınıfı, std::lock_guard
sınıfına benzer, ancak daha fazla esneklik sunar. std::unique_lock
, mutex’i kilitleme ve kilidi açma işlemlerini kontrol etmek için manuel olarak kullanılabilir. Ayrıca, kilidi isteğe bağlı olarak serbest bırakabilir ve tekrar kilitleyebilir.
std::unique_lock
, std::mutex
veya std::recursive_mutex
gibi mutex türlerini destekler.
İşte bir örnek:
#include <iostream>
#include <mutex>
#include <thread>
std::mutex mtx; // Mutex nesnesi
void WorkerThread() {
std::unique_lock<std::mutex> lock(mtx); // Mutex'i kilitle
// Kritik bölge: Paylaşılan kaynağa erişim
std::cout << "İş parçacığı çalışıyor..." << std::endl;
// Kaynağa erişim işlemleri
std::this_thread::sleep_for(std::chrono::seconds(2));
// Mutex kilidini serbest bırakma
lock.unlock();
// Diğer işlemler
// Mutex kilidini tekrar alma
lock.lock();
// Yeniden kritik bölgeye erişim
std::cout << "İş parçacığı devam ediyor..." << std::endl;
// Otomatik olarak mutex'i serbest bırakır
}
int main() {
std::thread t1(WorkerThread);
std::thread t2(WorkerThread);
t1.join();
t2.join();
return 0;
}
Bu örnekte, std::unique_lock
sınıfı kullanılarak bir lock
nesnesi oluşturulur. unique_lock
‘ın yapıcı fonksiyonu, verilen mutex’i kilitleyerek iş parçacığının kritik bölgeye giriş yapmasını sağlar. unique_lock
nesnesi, kapsamdan çıktığında (yani WorkerThread
fonksiyonundan çıkıldığında), otomatik olarak mutex’i serbest bırakır.
Örnekte, lock.unlock()
kullanarak mutex kilidini serbest bırakıyoruz. Ardından, başka işlemler yapabiliriz. Daha sonra, lock.lock()
kullanarak mutex kilidini tekrar alabiliriz ve yeniden kritik bölgeye erişebiliriz.
std::unique_lock
, lock.unlock()
ve lock.lock()
gibi işlemleri kullanarak, mutex kilidini kontrol etmek ve tekrar kilitlemek için daha fazla esneklik sağlar. Bu, bazı senaryolarda mutex kilidini isteğe bağlı olarak serbest bırakmak ve tekrar kilitlemek için kullanışlı olabilir.
Şimdi std::unique_lock
‘un std::guard_lock
‘a göre kullanımının esnek yönlerinden bahsederek karşılaştırmalı örnek verelim.
std::unique_lock
ve std::lock_guard
arasındaki en önemli fark, std::unique_lock
‘ın daha fazla esneklik sağlamasıdır. İşte std::unique_lock
‘un guard_lock’a göre kullanımının esnek yönleri:
-
Kilidi İstediğiniz Zaman Serbest Bırakabilme:
std::unique_lock
,unlock()
işlevini kullanarak kilidi istediğiniz zaman serbest bırakmanıza olanak tanır. Bu, kritik bölgenin tamamı yerine sadece bir kısmı üzerinde çalışmanız gerektiğinde veya mutex kilidini diğer işlemlere açmanız gerektiğinde faydalı olabilir. -
Kilidi İstediğiniz Zaman Tekrar Kilitleyebilme:
std::unique_lock
,lock()
işlevini kullanarak kilidi istediğiniz zaman tekrar kilitleyebilmenize olanak tanır. Bu, kilidin serbest bırakıldığı bir bölge sonrasında tekrar kritik bölgeye erişim sağlamanız gerektiğinde veya kilitli olan kaynağa erişim gerektiren başka bir işlem yapmanız gerektiğinde kullanışlıdır.
4.1. Unique Lock ve Lock Guard Karşılaştırması
İşte bir karşılaştırmalı örnek, std::unique_lock
ve std::lock_guard
‘ın kullanımını göstermek için:
#include <iostream>
#include <mutex>
#include <thread>
std::mutex mtx; // Mutex nesnesi
void UniqueLockExample() {
std::unique_lock<std::mutex> lock(mtx); // Mutex'i kilitle
// Kritik bölge: Paylaşılan kaynağa erişim
std::cout << "Unique Lock ile çalışan iş parçacığı" << std::endl;
// Kaynağa erişim işlemleri
std::this_thread::sleep_for(std::chrono::seconds(2));
// Kilidi isteğe bağlı olarak serbest bırakma
if (someCondition) {
lock.unlock();
// Diğer işlemler
lock.lock(); // Kilidi tekrar al
// Yeniden kritik bölgeye erişim
}
// Otomatik olarak mutex'i serbest bırakır
}
void LockGuardExample() {
std::lock_guard<std::mutex> lock(mtx); // Mutex'i kilitle
// Kritik bölge: Paylaşılan kaynağa erişim
std::cout << "Lock Guard ile çalışan iş parçacığı" << std::endl;
// Kaynağa erişim işlemleri
std::this_thread::sleep_for(std::chrono::seconds(2));
// Kilidi isteğe bağlı olarak serbest bırakma mümkün değil
// Diğer işlemler
// Otomatik olarak mutex'i serbest bırakır
}
int main() {
std::thread t1(UniqueLockExample);
std::thread t2(LockGuardExample);
t1.join();
t2.join();
return 0;
}
Bu örnekte, UniqueLockExample
işlevi, std::unique_lock
kullanarak mutex kilidini isteğe bağlı olarak serbest bırakır ve tekrar kilitleme yapabilir. LockGuardExample
işlevi ise std::lock_guard
kullanarak mutex kilidini otomatik olarak serbest bırakır, ancak tekrar kilitleme yapma yeteneği yoktur.
Bu örnekte, UniqueLockExample
işlevi, belirli bir koşulu kontrol ederek mutex kilidini serbest bırakır ve ardından başka işlemler yapar. Daha sonra, mutex kilidini tekrar alarak kritik bölgeye erişir. LockGuardExample
ise kilidi otomatik olarak serbest bırakır ve başka işlemler yapmaz.
Bu şekilde, std::unique_lock
‘un unlock()
ve lock()
işlevlerini kullanarak daha fazla esneklik sağladığını ve mutex kilidini daha ince bir kontrolle yönetmenize olanak tanıdığını görebilirsiniz.
5. Condition Variable
std::condition_variable
, C++ standart kütüphanesindeki senkronizasyon mekanizmalarından biridir. std::condition_variable
, iş parçacıkları arasında iletişim kurmayı sağlar ve iş parçacıklarının birbirlerini beklemesine veya birbirlerini uyandırmasına olanak tanır. Bir iş parçacığı, belirli bir koşulu kontrol ederek diğer iş parçacıklarını uyandırabilir veya bekleyebilir.
std::condition_variable
, bir mutex ile birlikte kullanılır ve genellikle şu işlemlerle birlikte kullanılır:
-
wait()
: Bir iş parçacığı, belirli bir koşulu kontrol ederek diğer iş parçacıklarının bir olayı veya koşulu beklemesini sağlar.wait()
işlevi çağrıldığında, mutex kilidi serbest bırakılır ve iş parçacığı uyandırılıncaya kadar bekler. -
notify_one()
: Bekleyen iş parçacıklarından birini uyandırır. Eğer birden fazla iş parçacığı bekliyorsa, hangi iş parçacığının uyandırılacağı belirsizdir. -
notify_all()
: Bekleyen tüm iş parçacıklarını uyandırır. Tüm iş parçacıkları tekrar mutex kilidini almaya çalışacaktır.
İşte bir örnek verelim:
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
std::condition_variable cv; // Condition variable nesnesi
std::mutex mtx; // Mutex nesnesi
bool ready = false; // Paylaşılan değişken
void WorkerThread1() {
std::unique_lock<std::mutex> lock(mtx); // Mutex'i kilitle
// Koşulu beklemek için wait()
cv.wait(lock, []{ return ready; });
// Koşul gerçekleşti, devam eden işlemler
std::cout << "WorkerThread1: Koşul gerçekleşti, devam eden işlemler" << std::endl;
}
void WorkerThread2() {
std::this_thread::sleep_for(std::chrono::seconds(2));
std::unique_lock<std::mutex> lock(mtx); // Mutex'i kilitle
// Koşulu gerçekleştir
ready = true;
// Bekleyen iş parçacığını uyandır
cv.notify_one();
}
int main() {
std::thread t1(WorkerThread1);
std::thread t2(WorkerThread2);
t1.join();
t2.join();
return 0;
}
Bu örnekte, std::condition_variable
ve std::mutex
kullanılarak iki iş parçacığı arasında iletişim sağlanır. WorkerThread1
, bir koşulu beklerken (ready
değişkeninin true
olmasını beklerken) wait()
işlevini kullanır ve mutex kilidini serbest bırakır. WorkerThread2
, 2 saniye bekledikten sonra koşulu gerçekleştirir (ready
değişkenini true
yapar) ve bekleyen iş parçacığını uyandırmak için notify_one()
işlevini kullanır.
Bu şekilde, std::condition_variable
kullanarak bir iş parçacığının diğerini beklemesini sağlayabilir ve belirli bir koşul gerçekleştiğinde uyandırabilirsiniz. wait()
, notify_one()
ve notify_all()
işlevlerini kullanarak iş parçacıkları arasında iletişim ve senkronizasyon sağlayabilirsiniz.
5.1. Timed Condition Variable
Aşağıda, C++’ın std::condition_variable
sınıfını ve std::chrono
kütüphanesini kullanarak bir timed conditional variable (zamana bağlı koşullu değişken) örneği verilmiştir:
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <chrono>
std::condition_variable cv;
std::mutex mtx;
bool flag = false;
void WorkerThread() {
std::this_thread::sleep_for(std::chrono::seconds(2));
{
std::lock_guard<std::mutex> lock(mtx);
flag = true;
}
cv.notify_one();
}
int main() {
std::cout << "Ana iş parçacığı başladı" << std::endl;
std::thread t(WorkerThread);
{
std::unique_lock<std::mutex> lock(mtx);
if (cv.wait_for(lock, std::chrono::seconds(5), [] { return flag; })) {
std::cout << "Bekleme süresi içinde koşul sağlandı" << std::endl;
} else {
std::cout << "Bekleme süresi içinde koşul sağlanmadı" << std::endl;
}
}
t.join();
std::cout << "Ana iş parçacığı tamamlandı" << std::endl;
return 0;
}
Bu örnekte, WorkerThread
adlı bir iş parçacığı oluşturulur. Bu iş parçacığı, 2 saniye bekledikten sonra flag
değişkenini true
olarak ayarlar ve cv.notify_one()
işlevini kullanarak bekleyen iş parçacıklarını uyandırır.
Ana iş parçacığında, std::condition_variable
ve std::mutex
kullanılarak bir zamana bağlı koşullu değişken senaryosu oluşturulur. std::unique_lock
sınıfı kullanılarak std::mutex
kilidi alınır. Ardından, cv.wait_for()
işlevi çağrılarak bir süre boyunca beklenir ve flag
koşulunun sağlanması beklenir. Eğer belirtilen süre içinde koşul sağlanırsa, "Bekleme süresi içinde koşul sağlandı"
mesajı ekrana yazdırılır. Aksi halde, "Bekleme süresi içinde koşul sağlanmadı"
mesajı ekrana yazdırılır.
Bu şekilde, std::condition_variable
ve std::chrono
kütüphanelerini kullanarak zamana bağlı bir koşul kontrolü yapabilir ve belirli bir süre boyunca bekleyebilirsiniz. Bu senaryoda, wait_for()
işlevi belirtilen süre boyunca bekler ve beklenen koşulun sağlanıp sağlanmadığını kontrol eder.
6. Atomic
Elbette! std::atomic
, C++ standart kütüphanesinde yer alan bir sınıftır ve paralel programlamada senkronizasyonu sağlamak için kullanılır. std::atomic
, belirli bir veri türünü atomik olarak işlem yapılabilen bir veri türü haline getirir. Bu, birden fazla iş parçacığı arasında aynı anda okuma/yazma işlemlerini gerçekleştirirken verinin tutarlılığını sağlar.
std::atomic
, veri yarışmaları (race conditions) ve senkronizasyon eksiklikleri gibi sorunları önlemek için kullanılır. Bu sınıf, belirli işlemlerin atomik olarak gerçekleştirilmesini sağlamak için donanım veya kompiler destekli atomik operasyonları kullanır.
std::atomic
, birçok operasyonu destekler, bunlar arasında okuma/yazma işlemleri, artırma/azaltma, takas (swap), karşılaştırma ve daha fazlası bulunur.
İşte bir örnek:
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> counter(0); // Atomik sayaç
void WorkerThread() {
for (int i = 0; i < 10000; ++i) {
counter.fetch_add(1); // Atomik artırma işlemi
}
}
int main() {
std::thread t1(WorkerThread);
std::thread t2(WorkerThread);
t1.join();
t2.join();
std::cout << "Sonuç: " << counter << std::endl;
return 0;
}
Bu örnekte, std::atomic<int>
kullanılarak bir atomik sayaç oluşturulur. İki iş parçacığı, her biri 10.000 kez sayaçı artırmak için fetch_add()
işlevini kullanır. fetch_add()
işlevi, veriyi atomik olarak artıran bir işlemdir ve senkronizasyonu sağlar.
Ana iş parçacığı, iki iş parçacığı tamamladıktan sonra sayaç değerini ekrana yazdırır.
Bu şekilde, std::atomic
kullanarak birden fazla iş parçacığı arasında senkronizasyon sağlanabilir ve veri yarışmaları gibi sorunlar önlenir. Atomik operasyonlar, belirli bir veri türü üzerinde aynı anda yapılan işlemlerin tutarlı ve güvenli bir şekilde gerçekleştirilmesini sağlar.
Ek olarak atomic türdeki bir nesnenin değerini değiştirebilmek için load
ve store
işlevlerini de kullanabiliriz.
#include <iostream>
#include <atomic>
int main() {
std::atomic<int> value(10); // Atomik değer
// Değeri okuma
int loadedValue = value.load();
std::cout << "Yüklenen Değer: " << loadedValue << std::endl;
// Değeri değiştirme
value.store(20);
std::cout << "Yeni Değer: " << value.load() << std::endl;
return 0;
}
Bu örnekte, std::atomic<int>
türünde bir atomik değer value
oluşturulur. İlk olarak, load()
işlevi kullanılarak değer okunur ve loadedValue
değişkenine atanır. Ardından, store()
işlevi kullanılarak değer 20 olarak değiştirilir. Son olarak, load()
işlevi tekrar çağrılarak yeni değer okunur ve ekrana yazdırılır.
load()
işlevi, atomik değeri okurken senkronizasyonu sağlar. Yani, bir iş parçacığı atomik değeri okurken, başka bir iş parçacığı tarafından değer değiştirilirse, load()
işlevi güncel değeri döndürür. store()
işlevi ise atomik değeri güncellerken senkronizasyonu sağlar. Diğer iş parçacıkları, store()
işlemi tamamlanmadan yeni değeri okuyamaz.
Bu şekilde, load()
ve store()
işlevlerini kullanarak std::atomic
türündeki değerleri güvenli bir şekilde okuyabilir ve değiştirebilirsiniz. Bu işlevler, veri yarışmalarını önler ve tutarlı bir davranış sağlar.
6.1. Memory Order Acquire Nedir?
std::memory_order_acquire
, C++’taki std::atomic
türünün load
işlemi sırasında kullanılan bir bellek düzenleme (memory ordering) seçeneğidir. Bu bellek düzenlemesi, iş parçacığının bellekteki veriyi okurken, diğer iş parçacıklarının kendi bellek erişimlerini düzenlemesini sağlar.
std::memory_order_acquire
bellek düzenlemesi, bir iş parçacığının okuma işlemi sırasında bellek erişimlerinin sıralamasını kontrol etmek için kullanılır. Bu düzenleme seçeneği, okunan verinin iş parçacığı tarafından kullanılabilmesi için, okuma işleminin tamamlandığından ve bellekteki verinin güncel olduğundan emin olunmasını sağlar. Diğer bir deyişle, std::memory_order_acquire
seçeneği, bellekteki verinin güncel ve tutarlı bir şekilde okunmasını sağlamak için gereken senkronizasyonu sağlar.
Aşağıda, std::memory_order_acquire
seçeneğini kullanarak bir örnek verilmiştir:
#include <atomic>
#include <iostream>
#include <thread>
std::atomic<int> data(0);
std::atomic<bool> ready(false);
void ProducerThread() {
// Veriyi hazırla
data.store(42, std::memory_order_release);
// Hazırlığın tamamlandığını bildir
ready.store(true, std::memory_order_release);
}
void ConsumerThread() {
// Hazırlığın tamamlanmasını bekle
while (!ready.load(std::memory_order_acquire))
;
// Veriyi oku
int value = data.load(std::memory_order_acquire);
// Okunan değeri kullan
std::cout << "Received value: " << value << std::endl;
}
int main() {
std::thread producer(ProducerThread);
std::thread consumer(ConsumerThread);
producer.join();
consumer.join();
return 0;
}
Bu örnekte, std::memory_order_acquire
seçeneği load
ve store
işlemleri için kullanılmıştır. ProducerThread
iş parçacığı, data
değişkenine bir değer atar ve ready
bayrağını true olarak ayarlar. Bu işlemler, std::memory_order_release
seçeneğiyle gerçekleştirilir.
ConsumerThread
iş parçacığı, ready
bayrağının true olmasını beklerken, std::memory_order_acquire
seçeneğini kullanır. Bu sayede, ready
bayrağı true olduğunda, data
değişkenini okuyarak güncel veriye erişir.
Bu örnekte, std::memory_order_acquire
kullanarak okuma işlemi sırasında gerekli senkronizasyonu sağlamış oluyoruz, böylece verinin güncel olduğundan emin olabiliriz.
7. Atomic Flag
std::atomic_flag
, C++ standart kütüphanesinde yer alan bir sınıftır ve tek bir işlemin atomik bir şekilde gerçekleştirilmesini sağlar. std::atomic_flag
sınıfı, özellikle senkronizasyon işlemleri için kullanılır ve bir iş parçacığı tarafından kullanıldığında diğer iş parçacıklarının aynı anda aynı işlemi yapmasını engeller.
std::atomic_flag
, iki durumdan oluşur: set (ayarla) ve clear (temizle). Bu durumlar, iş parçacıklarının bir kritik bölgeye giriş yapmasını ve çıkmasını kontrol etmek için kullanılır.
std::atomic_flag
‘in test_and_set()
ve clear()
işlevleri vardır:
-
test_and_set()
:std::atomic_flag
‘in durumunu set (true) olarak ayarlar ve önceki durumunu döndürür. Bu işlev, bir iş parçacığının kritik bölgeye giriş yapmak için kullanılır. Eğertest_and_set()
işlemi başarılıysa, yani önceki durumtrue
ise, iş parçacığı kritik bölgeye girebilir. Eğer önceki durumfalse
ise, bir başka iş parçacığı kritik bölgededir ve bu iş parçacığı beklemelidir. -
clear()
:std::atomic_flag
‘in durumunu clear (false) olarak ayarlar. Bu işlev, bir iş parçacığının kritik bölgeden çıkış yapmak için kullanılır. İş parçacığı kritik bölgeden çıktıktan sonra,clear()
işlemi ilestd::atomic_flag
‘in durumufalse
olarak ayarlanır, böylece başka bir iş parçacığı kritik bölgeye girebilir.
İşte bir örnek:
#include <iostream>
#include <atomic>
#include <thread>
std::atomic_flag lock = ATOMIC_FLAG_INIT; // Atomik bayrak
void WorkerThread() {
while (lock.test_and_set(std::memory_order_acquire)) {
// Diğer iş parçacığı kritik bölgede, bekle
}
// Kritik bölge: Paylaşılan kaynağa erişim
std::cout << "İş parçacığı kritik bölgede" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
lock.clear(std::memory_order_release); // Kritik bölgeden çıkış
// Kritik bölgeden çıkış yapıldı
std::cout << "İş parçacığı kritik bölgeden çıkış yaptı" << std::endl;
}
int main() {
std::thread t1(WorkerThread);
std::thread t2(WorkerThread);
t1.join();
t2.join();
return 0;
}
Bu örnekte, std::atomic_flag
türünde bir atomik bayrak lock
oluşturulur. Her iş parçacığı test_and_set()
işlevini kullanarak bayrağı ayarlamaya çalışır. Eğer bayrak zaten ayarlı ise, yani bir başka iş parçacığı kritik bölgedeyse, iş parçacığı beklemek zorunda kalır. Bayrak başarılı bir şekilde ayarlandığında, iş parçacığı kritik bölgeye girer ve işlemlerini gerçekleştirir. Ardından, clear()
işlevi kullanılarak bayrak temizlenir ve başka bir iş parçacığı kritik bölgeye girebilir.
Bu şekilde, std::atomic_flag
kullanarak bir kritik bölgeye giriş ve çıkış kontrolü sağlayabilirsiniz. Bu, senkronizasyonu kontrol etmek ve veri yarışmalarını önlemek için kullanışlı bir mekanizmadır.
8. Extra
Bu kısımda C++ tarafından direkt olarak desteklenmeyen Semaphore kavramı üzerinde duracağız. Ek olarak thread_local
, spin lock
, defer_lock
, adopt_lock
, scoped_lock
, deadlock
, starvation
, ve livelock
konularına değindikten sonra thread pool
, consumer-producer
, Round-Robin
algoritmasının implementasyonunu senkronizasyon yöntemlerini kullanarak gerçekleştireceğiz.
8.1. Thread Local
thread_local kavramını, en baştan global ve local değişkenlerin tanımlamalarını yaparak açıklamak istiyorum.
- Global Değişken: Global değişkenler, programın herhangi bir noktasından erişilebilen ve geçerliliği programın ömrü boyunca devam eden değişkenlerdir. Global değişkenler, programın tüm iş parçacıkları veya fonksiyonlar tarafından paylaşılır. Bir global değişkenin bellekteki adresi her zaman sabittir ve programın herhangi bir yerinde erişilebilir.
#include <iostream>
int globalVariable = 10;
void Function1() {
std::cout << "Function1 - Global Değişken: " << globalVariable << std::endl;
}
void Function2() {
std::cout << "Function2 - Global Değişken: " << globalVariable << std::endl;
}
int main() {
Function1();
Function2();
return 0;
}
Bu örnekte, globalVariable
adlı bir global değişken oluşturulur. Function1()
ve Function2()
işlevleri, bu global değişkene erişebilir. Her iki işlev de aynı global değişkeni kullanır ve çıktı olarak aynı değeri verir.
- Local Değişken: Local değişkenler, bir işlevin içerisinde tanımlanan ve sadece tanımlandıkları blok veya işlev içerisinde geçerli olan değişkenlerdir. Local değişkenler, yalnızca tanımlandıkları işlevin çağrıldığı süre boyunca geçerlidir ve her işlev çağrısı için ayrı bellek alanına sahiptir.
#include <iostream>
void Function() {
int localVariable = 5;
std::cout << "Local Değişken: " << localVariable << std::endl;
}
int main() {
Function();
return 0;
}
Bu örnekte, Function()
adlı bir işlev oluşturulur ve içerisinde localVariable
adlı bir local değişken tanımlanır. Bu local değişken, yalnızca Function()
işlevinin çalıştığı süre boyunca geçerlidir ve yalnızca Function()
işlevi tarafından erişilebilir. Function()
işlevi her çağrıldığında localVariable
adlı değişken 5 değeriyle hayata gelecektir.
-
thread_local
Değişken:thread_local
anahtar kelimesiyle tanımlanan değişkenler, her iş parçacığı için ayrı bellek alanına sahip olan değişkenlerdir. Bu tür değişkenler, her iş parçacığı tarafından ayrı ayrı tutulur ve her iş parçacığı için farklı değerlere sahip olabilir.
#include <iostream>
#include <thread>
thread_local int threadLocalVariable = 3;
void ThreadFunction() {
threadLocalVariable++;
std::cout << "Thread - thread_local Değişken: " << threadLocalVariable << std::endl;
}
int main() {
std::thread t1(ThreadFunction);
std::thread t2(ThreadFunction);
t1.join();
t2.join();
std::cout << "Main Thread - thread_local Değişken: " << threadLocalVariable << std::endl;
return 0;
}
Bu örnekte, ThreadFunction()
adlı bir iş parçacığı fonksiyonu oluşturulur. İş parçacığı fonksiyonu içinde threadLocalVariable
adlı bir thread_local
değişken tanımlanır ve her iş parçacığı tarafından ayrı ayrı tutulur. Her iş parçacığı, kendi threadLocalVariable
değişkenine erişir ve değerini değiştirir. Main iş parçacığı da ayrı bir threadLocalVariable
değişkenine sahiptir ve bu değişkenin değerini yazdırır.
Ekrana yansıyan sonuç aşağıdaki gibi olacaktır:
Thread - thread_local Değişken: 4
Thread - thread_local Değişken: 4
Main Thread - thread_local Değişken: 3
Sonuç olarak, global değişkenler programın her yerinden erişilebilirken, local değişkenler sadece tanımlandıkları blok veya işlev içerisinde geçerlidir. thread_local
değişkenler ise her iş parçacığı için ayrı bellek alanına sahip olup her iş parçacığı tarafından ayrı ayrı kullanılır.
8.2. Semaphore
Semaphore, paralel programlamada senkronizasyon ve eşzamanlılık sağlamak için kullanılan bir senkronizasyon mekanizmasıdır. Semaphore, bir kaynağın aynı anda kaç iş parçacığı tarafından erişilebileceğini kontrol etmek için kullanılır.
Semaphore, bir tam sayı değeri ve iki temel işlemi olan bir veri yapısıdır: “acquire” (al) ve “release” (serbest bırak). Semaphore’un tam sayı değeri, kaynağın mevcut durumunu temsil eder.
-
“acquire” işlemi: Bir iş parçacığı, kaynağa erişmek istediğinde “acquire” işlemini kullanır. Eğer semaforun değeri 0 ise, iş parçacığı beklemeye alınır. Değer 0’dan büyük ise, iş parçacığı kaynağa erişir ve semafor değeri bir azaltılır.
-
“release” işlemi: Bir iş parçacığı, kaynağı serbest bırakmak istediğinde “release” işlemini kullanır. Semafor değeri bir artırılır ve bekleyen iş parçacıklarından biri serbest bırakılarak kaynağa erişim sağlanır.
Semaphore, çeşitli senaryolarda kullanışlı olabilir. Örneğin, bir sınırlı kaynak havuzunu kontrol etmek, kritik bölgeye aynı anda sadece belirli bir sayıda iş parçacığı izin vermek veya veri yapılarının güvenli bir şekilde paylaşılmasını sağlamak için semaphore kullanılabilir.
Semaphore’lar, paralel programlamada senkronizasyon ve eşzamanlılık sorunlarını çözmek için yaygın olarak kullanılan bir araçtır. Bununla birlikte, doğru kullanımı ve senaryolara özgü gereksinimlerin dikkate alınması önemlidir.
C++ standardı, semaforların doğrudan bir sınıf olarak yer almadığı bir sembolik veri yapısına (sembolik olarak mutex ve condition_variable ile benzetilebilir) sahip olmasına rağmen, semafor benzeri davranışları elde etmek için mevcut senkronizasyon mekanizmaları kullanılarak semafor benzeri bir yapı oluşturulabilir.
Bunun için std::mutex
ve std::condition_variable
kullanabiliriz. İşte bir örnek:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
class Semaphore {
public:
explicit Semaphore(int count) : count_(count) {}
void Acquire() {
std::unique_lock<std::mutex> lock(mutex_);
condition_.wait(lock, [this] { return count_ > 0; });
count_--;
}
void Release() {
std::unique_lock<std::mutex> lock(mutex_);
count_++;
condition_.notify_one();
}
private:
std::mutex mutex_;
std::condition_variable condition_;
int count_;
};
// Kullanım örneği
Semaphore semaphore(2);
void WorkerThread(int id) {
semaphore.Acquire();
std::cout << "Thread " << id << " started working." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "Thread " << id << " finished working." << std::endl;
semaphore.Release();
}
int main() {
std::thread t1(WorkerThread, 1);
std::thread t2(WorkerThread, 2);
std::thread t3(WorkerThread, 3);
t1.join();
t2.join();
t3.join();
return 0;
}
Bu örnekte, Semaphore
adlı bir sınıf oluşturulmuştur. Acquire
fonksiyonu, semaforu alırken kaynağın uygun olmasını bekler ve ardından kaynağı kullanır. Release
fonksiyonu ise kaynağı serbest bırakır ve diğer iş parçacıklarının kaynağı kullanmasına izin verir.
Ana iş parçacığı, 2 adet semafor kaynağına sahip bir semafor oluşturur. Ardından, WorkerThread
işlevini kullanarak üç iş parçacığı başlatır. Her iş parçacığı semafor kaynağını alır, bir süre çalışır ve ardından semafor kaynağını serbest bırakır.
Bu şekilde, semafor benzeri bir davranışı elde etmek için std::mutex
ve std::condition_variable
kullanarak basit bir semafor uygulaması yapabilirsiniz. Daha karmaşık senaryolarda veya özel gereksinimlerde farklı bir yaklaşım izlemek gerekebilir.
8.2.1. Binary Semaphore
C++20 standartları ile dile eklenmiştir.
Binary Semaphore, senkronizasyon mekanizmalarından biridir ve iki durumlu (0 ve 1) bir bayrak gibi davranır. İki temel işlemi vardır: “Sinyal Verme” (Signal) ve “Bekleme” (Wait).
-
“Sinyal Verme” (Signal): Bayrağı 1’e ayarlar. Bir iş parçacığı veya süreç, diğer iş parçacıklarına veya süreçlere bayrağı ayarlamak ve onları uyandırmak için kullanır. Bu işlem, diğer iş parçacıklarının veya süreçlerin devam etmesini sağlar.
-
“Bekleme” (Wait): Bayrağın 1 olmasını bekler. Eğer bayrak 1 ise, bayrağı 0’a ayarlar ve devam eder. Eğer bayrak 0 ise, iş parçacığı veya süreç beklemeye alınır ve diğer iş parçacıkları veya süreçler bayrağın 1 olmasını bekler.
Binary Semaphore, özellikle senkronizasyon ve karşılıklı dışlama (mutual exclusion) durumlarında kullanılır. Örneğin, bir kaynağın aynı anda sadece bir iş parçacığı veya süreç tarafından erişilebilmesini sağlamak için kullanılabilir.
Aşağıda, C++ dilinde std::binary_semaphore
‘ı kullanarak basit bir örnek verilmiştir:
#include <iostream>
#include <semaphore>
#include <thread>
std::binary_semaphore sem(0); // Binary Semaphore
void WorkerThread() {
std::cout << "İş parçacığı beklemede..." << std::endl;
sem.acquire(); // Bekleme
// Bayrak 1 olduğunda devam eden işlemler
std::cout << "İş parçacığı çalışıyor..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "İş parçacığı tamamlandı" << std::endl;
}
int main() {
std::thread t(WorkerThread);
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout << "Bayrağı 1'e ayarla" << std::endl;
sem.release(); // Sinyal Verme
t.join();
return 0;
}
Bu örnekte, std::binary_semaphore
türünde bir binary semaphore olan sem
oluşturulur. WorkerThread
iş parçacığı, acquire()
işlevini kullanarak bayrağın 1 olmasını bekler. Ana iş parçacığı, 5 saniye bekledikten sonra release()
işlevi ile bayrağı 1’e ayarlar ve WorkerThread
iş parçacığının devam etmesini sağlar.
İş parçacığı bayrağı beklediği sürece beklemeye alınır ve diğer iş parçacıkları bayrağın 1 olmasını bekler. Bayrak 1 olduğunda, iş parçacığı devam eder ve işlemlerini gerçekleştirir. Bu örnekte, binary semaphore kullanarak iş parçacığının çalışmasını kontrol etmek için basit bir senaryo gösterilmektedir.
8.2.2. Counting Semaphore
C++20 standartları ile dile eklenmiştir.
Counting Semaphore, senkronizasyon mekanizmalarından biridir ve belirli bir kaynağın aynı anda birden fazla iş parçacığı veya süreç tarafından erişilebilmesini kontrol etmek için kullanılır. Sayma (counting) semaforu, içerdiği sayaç değeri ile kaynağın mevcut durumunu temsil eder.
Counting Semaphore, iki temel işlemi destekler: “Sinyal Verme” (Signal) ve “Bekleme” (Wait).
-
“Sinyal Verme” (Signal): Kaynak sayaç değerini artırır. Bir iş parçacığı veya süreç, diğer iş parçacıklarına veya süreçlere kaynak kullanım hakkını vermek için kullanır. Bu işlem, diğer iş parçacıklarının veya süreçlerin kaynağı kullanabilmesini sağlar.
-
“Bekleme” (Wait): Kaynak kullanım hakkını bekler. Eğer sayaç değeri 0 ise, iş parçacığı veya süreç beklemeye alınır. Kaynak kullanım hakkı bir başka iş parçacığı veya süreç tarafından serbest bırakıldığında, bekleyen iş parçacığı veya süreç kaynağı kullanabilir.
Counting Semaphore, özellikle sınırlı sayıda kaynağın paylaşılması veya senkronizasyon gerektiren senaryolarda kullanılır.
Aşağıda, C++ dilinde std::counting_semaphore
‘ı kullanarak basit bir örnek verilmiştir:
#include <iostream>
#include <semaphore>
#include <thread>
std::counting_semaphore<5> sem(0); // Counting Semaphore
void WorkerThread(int id) {
std::cout << "İş parçacığı " << id << " kaynağı bekliyor..." << std::endl;
sem.acquire(); // Bekleme
// Kaynak kullanımı
std::cout << "İş parçacığı " << id << " kaynağı kullandı" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
sem.release(); // Kaynak serbest bırakma
std::cout << "İş parçacığı " << id << " kaynağı serbest bıraktı" << std::endl;
}
int main() {
std::thread t1(WorkerThread, 1);
std::thread t2(WorkerThread, 2);
std::thread t3(WorkerThread, 3);
std::this_thread::sleep_for(std::chrono::seconds(5));
for (int i = 0; i < 3; ++i) {
sem.release(); // İş parçacıklarının beklemesini sonlandırma
}
t1.join();
t2.join();
t3.join();
return 0;
}
Bu örnekte, std::counting_semaphore<5>
türünde bir counting semaphore olan sem
0 başlangıç değeriyle oluşturulur. WorkerThread
iş parçacıkları, acquire()
işlevini kullanarak kaynağı bekler. Ana iş parçacığı, 5 saniye bekledikten sonra release()
işlevini kullanarak kaynağı serbest bırakır ve bekleyen iş parçacıklarının kaynağı kullanabilmesini sağlar.
İş parçacıkları kaynağı beklediği sürece beklemeye alınır. İlk 5 iş parçacığı kaynağı alır ve kullanır. Sonra, ana iş parçacığı release()
işlemini kullanarak bekleyen iş parçacıklarının beklemesini sonlandırır ve kaynağın serbest olduğunu belirtir. Böylece, diğer iş parçacıkları kaynağı kullanabilir.
Bu şekilde, counting semaphore kullanarak belirli sayıda kaynağın aynı anda erişilebilmesini kontrol edebilir ve kaynak kullanımını senkronize edebilirsiniz.
8.2.3. Recursive Semaphore (Yeniden Girişimli Semaphore)
Maalesef, C++ standart kütüphanesi içerisinde doğrudan “Recursive Semaphore” adında bir sınıf yoktur. Ancak, senkronizasyon mekanizmaları için çeşitli kütüphaneler veya üçüncü taraf kütüphaneler Recursive Semaphore’ları destekleyebilir.
Recursive Semaphore, bir kaynağa sahip olan iş parçacıklarının veya süreçlerin aynı kaynağı birden fazla kez kullanmasına izin veren bir senkronizasyon mekanizmasıdır. Örneğin, bir iş parçacığı bir kaynağı kilitlediğinde ve ardından başka bir kaynak kullanırken aynı kaynağı tekrar kilitlemek isteyebilir. Recursive Semaphore, bu durumu destekler ve aynı iş parçacığı veya süreç tarafından kilitlenen kaynak sayısını takip eder.
Bu özellik bazen karmaşık senaryolarda veya bazı özel durumlarda gerekebilir. Eğer Recursive Semaphore benzeri bir senkronizasyon mekanizması kullanmak isterseniz, ilgili kütüphane veya üçüncü taraf çözümleri inceleyebilirsiniz. Örnek olarak, Boost C++ kütüphanesi, boost::recursive_mutex
sınıfını içerir, bu da benzer bir işlevselliği sağlar.
İşte Boost C++ kütüphanesi ile boost::recursive_mutex
kullanarak basit bir örnek:
#include <iostream>
#include <boost/thread/recursive_mutex.hpp>
#include <boost/thread/thread.hpp>
boost::recursive_mutex mtx; // Recursive Mutex
void WorkerThread(int id, int count) {
mtx.lock(); // Mutex kilidi
std::cout << "İş parçacığı " << id << " çalışıyor..." << std::endl;
if (count > 0) {
WorkerThread(id, count - 1); // Kendi kendini çağırma
}
std::cout << "İş parçacığı " << id << " tamamlandı" << std::endl;
mtx.unlock(); // Mutex kilidini serbest bırak
}
int main() {
boost::thread t1(WorkerThread, 1, 2);
boost::thread t2(WorkerThread, 2, 1);
t1.join();
t2.join();
return 0;
}
Bu örnekte, Boost C++ kütüphanesinin boost::recursive_mutex
sınıfı kullanılarak bir recursive mutex olan mtx
oluşturulur. WorkerThread
iş parçacıkları, lock()
ve unlock()
işlevlerini kullanarak mutex kilidini alır ve serbest bırakır.
Örnekte, WorkerThread
fonksiyonu kendini tekrar çağırarak özyinelemeli bir şekilde çalışır. Bu durumda, her iş parçacığı kendini bir kez daha çağırır ve kendisi tarafından kilitlenen mutex’i tekrar kilitlemek için boost::recursive_mutex
kullanır. Bu, aynı iş parçacığı tarafından kilitlenen kaynak sayısını takip edebilmenizi sağlar.
Bu şekilde, Boost C++ kütüphanesinin boost::recursive_mutex
sınıfı ile bir Recursive Mutex kullanarak özyinelemeli bir iş parçacığı senaryosunu ele alabilirsiniz.
veya POSIX senkronizasyon mekanizmaları olan pthread_mutex_t
ve pthread_cond_t
kullanılarak Recursive Semaphore’ı uygulamak mümkündür.
Aşağıda, POSIX senkronizasyon mekanizmaları kullanılarak basit bir Recursive Semaphore örneği:
#include <iostream>
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t condition = PTHREAD_COND_INITIALIZER;
int resourceCount = 0;
void RecursiveSemaphoreLock() {
pthread_mutex_lock(&mutex);
resourceCount++;
pthread_mutex_unlock(&mutex);
}
void RecursiveSemaphoreUnlock() {
pthread_mutex_lock(&mutex);
resourceCount--;
if (resourceCount == 0) {
pthread_cond_signal(&condition);
}
pthread_mutex_unlock(&mutex);
}
void* WorkerThread(void* arg) {
int threadId = *((int*)arg);
RecursiveSemaphoreLock();
std::cout << "Thread " << threadId << " entered critical section" << std::endl;
// Recursive call
if (threadId > 1) {
WorkerThread((void*)&threadId);
}
std::cout << "Thread " << threadId << " exited critical section" << std::endl;
RecursiveSemaphoreUnlock();
return nullptr;
}
int main() {
pthread_t thread1, thread2;
int threadId1 = 1, threadId2 = 2;
pthread_create(&thread1, nullptr, WorkerThread, (void*)&threadId1);
pthread_create(&thread2, nullptr, WorkerThread, (void*)&threadId2);
pthread_join(thread1, nullptr);
pthread_join(thread2, nullptr);
return 0;
}
Bu örnekte, POSIX senkronizasyon mekanizmaları olan pthread_mutex_t
ve pthread_cond_t
kullanılarak Recursive Semaphore benzeri bir senaryo oluşturulur. RecursiveSemaphoreLock()
işlevi, kaynağı kilitlemek için mutex’i kullanır ve kaynak sayacını artırır. RecursiveSemaphoreUnlock()
işlevi ise kaynağı serbest bırakmak için mutex’i kullanır ve kaynak sayacını azaltır. İş parçacığı, WorkerThread()
işlevi içerisinde kendini tekrar çağırarak özyinelemeli bir şekilde çalışır.
Bu örnekte, iki iş parçacığı oluşturulur ve her biri Recursive Semaphore benzeri senaryoyu gerçekleştirir. İş parçacıkları sırayla kritik bölgeye girer ve çıkar. İş parçacığı, WorkerThread()
işlevi içerisinde kendini tekrar çağırarak kritik bölgede özyinelemeli bir şekilde çalışır.
8.2.4. Timed Semaphore (Zaman Aşımı Olan Semaphore)
Maalesef, C++ standart kütüphanesi içerisinde doğrudan “Timed Semaphore” adında bir sınıf yoktur. Ancak, senkronizasyon mekanizmaları için çeşitli kütüphaneler veya üçüncü taraf kütüphaneler Timed Semaphore’ları destekleyebilir.
Timed Semaphore, belirli bir süre boyunca iş parçacıklarının veya süreçlerin bir kaynağı kullanmasına izin veren bir senkronizasyon mekanizmasıdır. Bu, bir iş parçacığının bir kaynağı beklerken belirli bir süre içinde kaynağın serbest bırakılmaması durumunda beklemeyi sonlandırmasını sağlar.
Bu tür senaryolar, bir iş parçacığının veya sürecin belirli bir kaynağa erişimini sınırlamak ve aşırı beklemeyi önlemek için kullanılabilir. Timed Semaphore, belirli bir zaman aşımı süresiyle beklemeyi yönetir.
Eğer Timed Semaphore benzeri bir senkronizasyon mekanizması kullanmak isterseniz, ilgili kütüphane veya üçüncü taraf çözümleri inceleyebilirsiniz. Örneğin, Boost C++ kütüphanesi, boost::interprocess::interprocess_semaphore
sınıfını içerir, bu da zaman aşımıyla beklemeyi sağlar.
Aşağıda, Boost C++ kütüphanesi ile boost::interprocess::interprocess_semaphore
kullanarak basit bir örnek:
#include <iostream>
#include <boost/interprocess/sync/interprocess_semaphore.hpp>
#include <boost/thread/thread.hpp>
boost::interprocess::interprocess_semaphore sem(0); // Timed Semaphore
void WorkerThread(int id) {
std::cout << "İş parçacığı " << id << " kaynağı bekliyor..." << std::endl;
if (sem.timed_wait(boost::posix_time::seconds(3))) {
// Kaynak kullanımı
std::cout << "İş parçacığı " << id << " kaynağı kullandı" << std::endl;
} else {
// Zaman aşımı durumunda
std::cout << "İş parçacığı " << id << " zaman aşımı!" << std::endl;
}
}
int main() {
boost::thread t1(WorkerThread, 1);
boost::thread t2(WorkerThread, 2);
t1.join();
t2.join();
return 0;
}
Bu örnekte, Boost C++ kütüphanesinin boost::interprocess::interprocess_semaphore
sınıfı kullanılarak bir Timed Semaphore olan sem
oluşturulur. WorkerThread
iş parçacıkları, timed_wait()
işlevini kullanarak kaynağı belirli bir süre boyunca bekler. Eğer belirtilen süre içinde kaynak serbest bırakılmazsa, zaman aşımı durumu gerçekleşir.
timed_wait()
işlevi, beklenen sürenin yanı sıra boost::posix_time::seconds
kullanarak zaman aşımı süresini belirtir. İş parçacığı kaynağı başarıyla kullanabilirse, kaynak kullanımını gerçekleştirir. Zaman aşımı durumunda ise, ilgili mesajı ekrana yazdırır.
Bu şekilde, Boost C++ kütüphanesinin boost::interprocess::interprocess_semaphore
sınıfı ile Timed Semaphore kullanarak belirli bir süre boyunca kaynak bekleyen iş parçacıklarını ele alabilirsiniz.
8.3. Spin Lock
Spin Lock, bir senkronizasyon mekanizmasıdır ve bir mutex benzeri bir işlevi yerine getirir. Spin lock, bir iş parçacığı, bir kaynağa erişim sağlamak istediğinde sürekli bir döngü içinde kaynağın serbest bırakılmasını bekler. Yani, diğer iş parçacıkları kaynağı serbest bırakana kadar sürekli olarak tekrar tekrar deneme yapar. Bu, iş parçacıkları arasında geçiş yapmak yerine aktif olarak beklemeyi içerir.
Spin lock’lar, kısa süreli kritik bölgeler veya düşük yoğunluklu iş yükleri gibi senaryolarda faydalı olabilir. Ancak, yüksek yoğunluklu iş yükleri veya uzun süreli beklemeler gerektiren senaryolarda, spin lock’ların kullanılması performans sorunlarına yol açabilir. Bu nedenle, spin lock’lar dikkatlice kullanılmalı ve senaryoya göre değerlendirilmelidir.
işte basit bir spin lock örneği:
#include <atomic>
class SpinLock {
public:
void Lock() {
while (flag_.test_and_set(std::memory_order_acquire))
;
}
void Unlock() {
flag_.clear(std::memory_order_release);
}
private:
std::atomic_flag flag_ = ATOMIC_FLAG_INIT;
};
// Kullanım örneği
SpinLock spinLock;
int sharedVariable = 0;
void WorkerThread() {
spinLock.Lock();
// Kritik bölge
sharedVariable++;
// Diğer işlemler
spinLock.Unlock();
}
int main() {
// İş parçacıklarını başlat
std::thread t1(WorkerThread);
std::thread t2(WorkerThread);
// İş parçacıklarının tamamlanmasını bekle
t1.join();
t2.join();
// Sonuç kontrolü
std::cout << "Shared variable value: " << sharedVariable << std::endl;
return 0;
}
8.4. Deadlock, Starvation, Livelock
Bu kısımda deadlock, starvation ve livelock kavramlarını açıklayıp, örnekler vereceğiz.
8.4.1. Deadlock
Deadlock, birbirini bekleyen iki veya daha fazla iş parçacığının veya sürecin, bir kaynağı elinde tutması ve diğerinin serbest bırakmasını beklemesi durumudur. Hiçbiri ilerleyemez ve program durur. Deadlock genellikle dört temel özelliğe sahip oluşur:
-
Karşılıklı Bekleme (Mutual Exclusion): Kaynaklar, yalnızca tek bir iş parçacığı veya süreç tarafından kullanılabilir. Diğer iş parçacıkları veya süreçler kaynağın serbest bırakılmasını bekler.
-
Kaynak Yaratma ve Tutma (Hold and Wait): Bir iş parçacığı veya süreç, sahip olduğu kaynağı serbest bırakmadan başka bir kaynağı talep eder ve bu talep karşılanana kadar bekler.
-
Kaynakların Önceden Talep Edilmesi (Preemption): Bir iş parçacığı veya süreç, elindeki kaynağı bırakmadan başka bir iş parçacığı veya süreç tarafından kaynağa müdahale edilemez.
-
Döngüsel Bekleme (Circular Wait): İş parçacıkları veya süreçler arasında döngüsel bir beklemeler zinciri oluşur, yani her biri diğerinin serbest bırakmasını bekler.
Örnek Deadlock Senaryosu:
#include <iostream>
#include <mutex>
#include <thread>
std::mutex mutex1;
std::mutex mutex2;
void Thread1() {
std::unique_lock<std::mutex> lock1(mutex1);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::unique_lock<std::mutex> lock2(mutex2);
// Bu noktaya asla ulaşılmaz
std::cout << "Thread 1 çalışıyor" << std::endl;
}
void Thread2() {
std::unique_lock<std::mutex> lock2(mutex2);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::unique_lock<std::mutex> lock1(mutex1);
// Bu noktaya asla ulaşılmaz
std::cout << "Thread 2 çalışıyor" << std::endl;
}
int main() {
std::thread t1(Thread1);
std::thread t2(Thread2);
t1.join();
t2.join();
return 0;
}
Bu örnekte, Thread1
ve Thread2
adlı iki iş parçacığı oluşturulur. Her bir iş parçacığı, önce bir mutex’i kilitleyip ardından diğer mutex’i bekler. İş parçacıkları birbirini beklerken, deadlock oluşur ve her iki iş parçacığı da ilerleyemez. Program donar ve hiçbir çıktı üretilmez.
8.4.2. Starvation
Starvation, bir iş parçacığının veya sürecin gereken kaynaklara erişim hakkını sürekli olarak diğer iş parçacıklarından veya süreçlerden daha düşük bir öncelikle veya hiç alamaması durumudur. Bir iş parçacığı veya süreç, diğerleri tarafından sürekli olarak engellenir ve kaynakları kullanma imkanı olmaz. Sonuç olarak, iş parçacığı veya süreç hiçbir zaman tamamlanmaz veya ilerlemez.
Örnek Starvation Senaryosu:
#include <iostream>
#include <mutex>
#include <thread>
std::mutex mutex;
void WorkerThread(int id) {
while (true) {
std::unique_lock<std::mutex> lock(mutex);
std::cout << "İş parçacığı " << id << " çalışıyor" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main() {
std::thread t1(WorkerThread, 1);
std::thread t2(WorkerThread, 2);
std::thread t3(WorkerThread, 3);
t1.join();
t2.join();
t3.join();
return 0;
}
Bu örnekte, WorkerThread
adlı üç iş parçacığı oluşturulur. Her bir iş parçacığı, bir mutex’i kilitleyip çalışır, ardından mutex’i serbest bırakır ve tekrar kilitleyerek çalışır. Ancak, iş parçacıkları arasında öncelik farkı olmadığından, her seferinde aynı iş parçacığı çalışırken diğerleri beklemek zorunda kalır. Bu durumda, örneğin İş parçacığı 1
diğer iş parçacıklarından daha önceki bir zamanda çalışırken, diğer iş parçacıkları sürekli olarak beklemek zorunda kalır ve starvation durumu oluşur.
Bir başka starvation (açlık) senaryosu örneği daha verilebilir:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mutex;
int sharedResource = 0;
void WorkerThread(int id) {
while (true) {
std::lock_guard<std::mutex> lock(mutex);
if (sharedResource < 10) {
sharedResource++;
std::cout << "İş parçacığı " << id << " paylaşılan kaynağı artırdı: " << sharedResource << std::endl;
}
else {
break;
}
}
}
int main() {
std::thread t1(WorkerThread, 1);
std::thread t2(WorkerThread, 2);
t1.join();
t2.join();
return 0;
}
Bu örnekte, WorkerThread
adlı iki iş parçacığı oluşturulur. Her bir iş parçacığı, bir mutex kullanarak sharedResource
adlı bir paylaşılan değişkeni artırır. Ancak, if
koşulu içerisinde kontrol edilen sharedResource
değeri 10’a eşit olduğunda, iş parçacığı döngüden çıkar ve sonlanır.
Eğer WorkerThread
iş parçacıklarından biri diğerinden daha hızlı çalışıyorsa ve sharedResource
değişkeni hızlı bir şekilde 10’a ulaşırsa, diğer iş parçacığı sürekli olarak döngüde beklemek zorunda kalır. Bu durumda, iş parçacığı 1 hızlı bir şekilde çalışırken, iş parçacığı 2 hiçbir zaman döngüden çıkamaz ve kaynak açlığı yaşar.
Starvation durumu, bir iş parçacığının diğer iş parçacıklarının veya süreçlerin önceliklerine göre sürekli olarak engellendiği bir durumdur. Bu durum, kaynakların adaletsiz bir şekilde tahsis edildiği veya önceliklerin yanlış şekilde ayarlandığı senaryolarda ortaya çıkabilir.
Yukarıdaki örnekteki starvation sorununu çözmek için bazı stratejiler şunlar olabilir:
-
Öncelik Sırası: İş parçacıklarına veya süreçlere öncelik sırası vermek, her birinin kaynaklara erişimi konusunda adil bir şekilde rekabet etmelerini sağlar. Öncelikli iş parçacıklar veya süreçler, diğerlerinden daha önce kaynakları talep edebilir ve böylece starvation durumu azaltılabilir.
-
Kaynakları Paylaşma: İş parçacıkları veya süreçler arasında kaynakları paylaşmak, tek bir iş parçacığının veya sürecin sürekli olarak tüm kaynakları kontrol etmesini önler. Böylece, kaynakların daha dengeli bir şekilde kullanılması sağlanır ve starvation durumu azaltılır.
-
Adil Sıra: Bir kuyruk veya sıra yapısı kullanarak iş parçacıklarının veya süreçlerin kaynaklara erişimini düzenlemek, starvation durumunu önlemeye yardımcı olabilir. Her bir iş parçacığı veya süreç, sıradaki en eski talebe göre kaynaklara erişim hakkı alır ve bu şekilde adaletli bir sıra oluşturulur.
-
Önceden Allokasyon: Kaynakları iş parçacıkları veya süreçler arasında önceden tahsis etmek, starvation durumunu azaltabilir. Her bir iş parçacığı veya süreç, başlangıçta belirli bir miktar kaynağa sahip olabilir ve bu kaynakları işlem süresi boyunca kullanabilir. Bu şekilde, kaynaklara olan talepler daha adil bir şekilde dağıtılabilir.
Örnek olarak, yukarıdaki örnekte starvation sorununu çözmek için öncelik sırası verilebilir veya bir kuyruk yapısı kullanılabilir. Öncelikli iş parçacıklar veya süreçler, kaynakları talep etme önceliğine göre sırayla erişim hakkı alır. Böylece, her bir iş parçacığı veya süreç adil bir şekilde kaynakları kullanır ve starvation durumu önlenir.
8.4.2.1. std::this_thread_yield()
Şimdi std::this_thread::yield()
işlevinden bahsettikten sonra starvation
durumunu önleyebilecek basit bir yaklaşım örneği vereceğim.
std::this_thread::yield
işlevi, çalışan bir iş parçacığının diğer iş parçacıklarına CPU zaman dilimi vermesini sağlar. Diğer bir deyişle, iş parçacığı geçici olarak CPU’yu serbest bırakır ve diğer iş parçacıklarının çalışmasına izin verir.
std::this_thread::yield
işlevi şu şekilde kullanılır:
#include <iostream>
#include <thread>
void WorkerThread() {
// Bazı işlemler yapılır
std::this_thread::yield();
// Diğer işlemler yapılır
}
int main() {
std::thread t(WorkerThread);
t.join();
return 0;
}
Yukarıdaki örnekte, WorkerThread
adlı bir iş parçacığı oluşturulur. İş parçacığı bazı işlemleri gerçekleştirir ve ardından std::this_thread::yield()
işlevini kullanarak CPU zaman dilimini serbest bırakır. Böylece, iş parçacığı diğer iş parçacıklarının çalışmasına fırsat verir.
std::this_thread::yield
işlevi, iş parçacığının CPU zaman dilimini serbest bırakmasını sağlasa da, işletim sistemi bu talebi tam olarak garanti etmez. Bu nedenle, std::this_thread::yield
işlevinin kullanımı iş parçacığı davranışını doğrudan kontrol etmek için değil, iş parçacıkları arasında işlemci kullanımını daha adil bir şekilde paylaşmak için tercih edilir.
std::this_thread::yield
işlevi, <thread>
başlığı altında tanımlanır ve iş parçacığı tarafından doğrudan çağrılabilir. Bu işlev, o anki iş parçacığının CPU zaman dilimini serbest bırakmasını sağlar ve işlemi diğer iş parçacıklarına geçer.
8.4.2.2. Starvation’ı Önlemek için Basit Bir Yaklaşım
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex mutex;
int sharedResource = 0;
void WorkerThread(int id, int priority) {
while (true) {
{
std::unique_lock<std::mutex> lock(mutex);
if (sharedResource < 10) {
sharedResource++;
std::cout << "İş parçacığı " << id << " paylaşılan kaynağı artırdı: " << sharedResource << std::endl;
}
else {
break;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// İş parçacığı önceliğine göre yield yapar
std::this_thread::yield();
}
}
int main() {
std::vector<std::thread> threads;
threads.emplace_back(WorkerThread, 1, 1);
threads.emplace_back(WorkerThread, 2, 2);
for (auto& thread : threads) {
thread.join();
}
return 0;
}
Bu örnekte, WorkerThread
adlı iki iş parçacığı oluşturulur ve her biri bir öncelik değeriyle çağrılır. İş parçacıkları paylaşılan kaynağı artırmak için mutex kullanır ve ardından std::this_thread::yield()
işlevini kullanarak önceliğine göre yield yapar. Böylece, öncelikli iş parçacığı bir süreliğine diğerine öncelik tanır ve starvation durumu önlenir.
Not: İş parçacığı önceliklerini doğrudan C++ standart kütüphanesi sağlamaz. İş parçacığı önceliklerini yönetmek için platforma özgü veya üçüncü taraf kütüphaneler kullanabilirsiniz. Yukarıdaki örnek, iş parçacığı önceliklerini göstermek için basit bir örnek olarak verilmiştir. Gerçek dünyada, iş parçacığı önceliklerini yönetirken dikkatli olmalı ve uygun bir yaklaşım kullanmalısınız.
8.4.3. Livelock
Livelock, iş parçacıklarının veya süreçlerin ilerlemek için çalışmalarına rağmen, sürekli olarak birbirlerine engel olması sonucu ortaya çıkan durumdur. İş parçacıkları veya süreçler birbirlerini beklerken sürekli olarak adım atarlar, ancak hiçbiri ilerleyemez. Bu durum, genellikle karşılıklı olarak birbirlerine yanıt vermeleriyle ilişkilidir.
Livelock durumu, deadlock durumuna benzese de, temel fark livelock’ta iş parçacıkları veya süreçler hareket halindedir ve çalışmaya devam ederken bir ilerleme sağlanamaz. İş parçacıkları veya süreçler, birbirlerine cevap vermek için adımlar atmaya devam ederler, ancak hiçbir işlem tamamlanmaz.
Livelock durumu genellikle iki veya daha fazla iş parçacığının veya sürecin birbirlerini beklemeleriyle ortaya çıkar. Örneğin, bir iş parçacığı veya süreç, başka bir iş parçacığı veya sürecin kaynakları serbest bırakmasını beklerken, diğer iş parçacığı veya süreç tam tersini yaparak aynı kaynakları serbest bırakmaya çalışır. Bu durumda, her ikisi de ilerlemek için adımlar atmaya devam ederken, birbirlerini engelleyerek livelock durumu oluşur.
Livelock durumunu çözmek zor olabilir. Genellikle, iş parçacıklarının veya süreçlerin davranışlarını kontrol etmek, karar verme mantığını değiştirmek veya senkronizasyon mekanizmalarını değiştirmek gerekebilir.
Aşağıda bir livelock örneği verilmiştir:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mutex1;
std::mutex mutex2;
void Thread1() {
while (true) {
std::unique_lock<std::mutex> lock1(mutex1);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::unique_lock<std::mutex> lock2(mutex2);
// İşlemler yapılır
lock2.unlock();
lock1.unlock();
}
}
void Thread2() {
while (true) {
std::unique_lock<std::mutex> lock2(mutex2);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::unique_lock<std::mutex> lock1(mutex1);
// İşlemler yapılır
lock1.unlock();
lock2.unlock();
}
}
int main() {
std::thread t1(Thread1);
std::thread t2(Thread2);
t1.join();
t2.join();
return 0;
}
Bu örnekte, Thread1
ve Thread2
adlı iki iş parçacığı oluşturulur. Her bir iş parçacığı sırasıyla mutex1
ve mutex2
mutex’lerini kilitlemeye çalışır. Ancak, her iki iş parçacığı da mutex’leri farklı sıralarla kilitlemeye çalışır. Bu durumda, her iki iş parçacığı da birbirlerini beklerken sürekli olarak mutex kilitleme işlemi gerçekleştirir, ancak hiçbiri ilerleme kaydedemez. İş parçacıkları birbirlerini beklerken sıkışıp kalır ve livelock durumu ortaya çıkar.
Livelock durumu, iş parçacıklarının veya süreçlerin birbirlerini engellemeleriyle ortaya çıkar. Önlemek için senkronizasyon mantığını ve mutex kilitleme sıralarını dikkatli bir şekilde düzenlemek gerekir.
Yukarıdaki livelock durumunu önlemek için düzenlenmiş bir örnek:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mutex1;
std::mutex mutex2;
void Thread1() {
while (true) {
std::unique_lock<std::mutex> lock1(mutex1, std::defer_lock);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::unique_lock<std::mutex> lock2(mutex2, std::defer_lock);
// Mutex'leri aynı anda kilitle
std::lock(lock1, lock2);
// İşlemler yapılır
lock2.unlock();
lock1.unlock();
}
}
void Thread2() {
while (true) {
std::unique_lock<std::mutex> lock1(mutex1, std::defer_lock);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::unique_lock<std::mutex> lock2(mutex2, std::defer_lock);
// Mutex'leri aynı anda kilitle
std::lock(lock1, lock2);
// İşlemler yapılır
lock2.unlock();
lock1.unlock();
}
}
int main() {
std::thread t1(Thread1);
std::thread t2(Thread2);
t1.join();
t2.join();
return 0;
}
Bu örnekte, std::lock
işlevi kullanılarak mutex’ler aynı anda kilitleme işlemi gerçekleştirilir. Her iki iş parçacığı da mutex’leri aynı sıralarla kilitleyerek livelock durumunu önler. std::lock
işlevi, tüm mutex’leri başarılı bir şekilde kilitleyene kadar bekler ve ardından iş parçacıklarına ilerlemelerine izin verir. Böylece, her iki iş parçacığı da mutex’leri aynı anda kilitleyerek birbirlerini beklemek yerine ilerleme kaydedebilir.
Bu düzenleme sayesinde, iş parçacıklarının mutex kilitleme sırasını düzenlemek yerine, aynı anda kilitlemeyi sağlayan std::lock
işlevini kullanarak livelock durumu önlenir.
8.4.3.1. std::defer_lock
std::defer_lock
, bir std::unique_lock
nesnesini, bir mutex’i hemen kilitlemek yerine erteleme (defer) etmek için kullanılan bir özelliktir. Kilitleme işlemi, std::unique_lock
nesnesi üzerindeki lock()
işlevi çağrıldığında gerçekleştirilir.
std::defer_lock
kullanıldığında, mutex hemen kilitlemez ve std::unique_lock
nesnesi, nesne oluşturulduğunda mutex üzerinde kilitleme işlemini gerçekleştirmez. Bu sayede, mutex kilitleme işlemi daha sonradan kontrol edilebilir ve istenildiği zaman gerçekleştirilebilir.
Aşağıda std::defer_lock
kullanımını gösteren bir örnek verilmiştir:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mutex;
void Thread1() {
std::unique_lock<std::mutex> lock(mutex, std::defer_lock);
// Kilitleme işlemini erteledik
// İşlemler yapılır
// Mutex'i kilitle
lock.lock();
// Mutex kilidini serbest bırakır
lock.unlock();
}
void Thread2() {
std::unique_lock<std::mutex> lock(mutex, std::defer_lock);
// Kilitleme işlemini erteledik
// İşlemler yapılır
// Mutex'i kilitle
lock.lock();
// Mutex kilidini serbest bırakır
lock.unlock();
}
int main() {
std::thread t1(Thread1);
std::thread t2(Thread2);
t1.join();
t2.join();
return 0;
}
Yukarıdaki örnekte, Thread1
ve Thread2
adlı iki iş parçacığı oluşturulur. Her bir iş parçacığı, std::unique_lock
nesnesini std::defer_lock
ile oluşturur, yani mutex’i hemen kilitleme işlemini ertelemiş olur. Daha sonra, işlemler yapılır ve mutex kilitleme işlemi istenildiği zaman gerçekleştirilir. lock()
işlevi çağrıldığında, mutex kilitleme işlemi gerçekleşir ve ilgili iş parçacığı mutex’i kilitleyebilir. Ardından, unlock()
işlevi çağrılarak mutex kilidi serbest bırakılır.
std::defer_lock
kullanımı, mutex kilitleme işlemini kontrol etmek ve kilitleme işlemini belli bir kod bloğuna odaklamak istediğimiz durumlarda faydalı olabilir. Bu sayede, kilitleme işlemi, diğer işlemler tamamlandıktan sonra ve istenilen zamanda gerçekleştirilebilir.
8.4.3.2. std::adopt_lock
Tabii! std::adopt_lock
bir argümandır ve std::unique_lock
veya std::lock_guard
nesneleriyle birlikte kullanılarak mutex kilidinin sahipliğini devralmayı ifade eder. Bu argüman, ilgili mutex’i kilitleyen iş parçacığı tarafından sahiplenilen bir mutex’i temsil eder.
std::adopt_lock
ifadesinin kullanımı, özellikle birden fazla mutexin kilidini aynı anda elde edilmesi gereken senaryolarda yararlıdır. Bir iş parçacığı önce bir mutexi kilitleyip ardından başka bir mutexi kilitleyorsa, std::adopt_lock
ifadesini kullanarak mutex kilidini devralabilir.
Bu ifade, iş parçacıkları arasında tutarlı bir şekilde mutex kilidi devralmayı sağlar ve senkronizasyonu düzgün bir şekilde yönetir. Ayrıca, başka bir iş parçacığı tarafından önceden kilitleme yapılmış bir mutexin kilidini almak için kullanılırken, deadlock veya yaşama kilitlenme gibi sorunlara neden olmaz.
Örneğin, aşağıdaki kod parçası std::unique_lock
kullanarak std::adopt_lock
ifadesini göstermektedir:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mutex1;
std::mutex mutex2;
void Thread1() {
std::unique_lock<std::mutex> lock1(mutex1, std::defer_lock);
std::unique_lock<std::mutex> lock2(mutex2, std::defer_lock);
// Mutex'leri aynı anda kilitle
std::lock(lock1, lock2);
// İşlemler yapılır
lock2.unlock();
lock1.unlock();
}
void Thread2() {
// Mutex'leri aynı anda kilitle
std::lock(mutex1, mutex2);
// Zaten kilitli olan her iki muteksin de kapsamın sonunda kilidinin açık olduğundan emin olun
std::lock_guard lock1{mutex1, std::adopt_lock};
std::lock_guard lock2{mutex2, std::adopt_lock};
// İşlemler yapılır
lock2.unlock();
lock1.unlock();
}
int main() {
std::thread t1(Thread1);
std::thread t2(Thread2);
t1.join();
t2.join();
return 0;
}
Yukarıdaki örnekte, Thread1
ve Thread2
adlı iki iş parçacığı oluşturulur. Her bir iş parçacığı std::unique_lock
nesnelerini std::defer_lock
ile oluşturur ve std::lock
işlevini kullanarak mutex’leri aynı anda kilitleyerek std::adopt_lock
ifadesiyle mutex kilidini devralır. Ardından, işlemler yapılır ve unlock()
işleviyle mutex kilidinin serbest bırakılması gerçekleştirilir.
std::adopt_lock
ifadesi, iş parçacığı tarafından mutex kilidi devralmak için kullanılır ve daha önce başka bir iş parçacığı tarafından kilitleme yapılmış bir mutexi güvenli bir şekilde kullanmayı sağlar.
Şimdi bir başka kod örneği verelim:
#include <mutex>
#include <thread>
#include <iostream>
struct bank_account {
explicit bank_account(int balance) : balance{balance} {}
int balance;
std::mutex m;
};
void transfer(bank_account &from, bank_account &to, int amount)
{
if(&from == &to) return; // avoid deadlock in case of self transfer
// lock both mutexes without deadlock
std::lock(from.m, to.m);
// make sure both already-locked mutexes are unlocked at the end of scope
std::lock_guard lock1{from.m, std::adopt_lock};
std::lock_guard lock2{to.m, std::adopt_lock};
// equivalent approach:
// std::unique_lock<std::mutex> lock1{from.m, std::defer_lock};
// std::unique_lock<std::mutex> lock2{to.m, std::defer_lock};
// std::lock(lock1, lock2);
from.balance -= amount;
to.balance += amount;
}
int main()
{
bank_account my_account{100};
bank_account your_account{50};
std::thread t1{transfer, std::ref(my_account), std::ref(your_account), 10};
std::thread t2{transfer, std::ref(your_account), std::ref(my_account), 5};
t1.join();
t2.join();
std::cout << "my_account.balance = " << my_account.balance << "\n"
"your_account.balance = " << your_account.balance << '\n';
}
Bu kod, iki banka hesabı arasında para transferi yapan bir senaryoyu simüle etmektedir. Her bir banka hesabı bank_account
yapısı ile temsil edilmekte ve balance
adında bir tamsayı değişkeni ile bakiyesini tutmaktadır. Ayrıca, her bir banka hesabı için bir mutex (m
) kullanılmaktadır.
transfer
fonksiyonu, from
hesabından to
hesabına belirli bir miktarı aktarır. Fonksiyonun işleyişi şu şekildedir:
-
İlk olarak,
from
veto
hesapları aynı hesap ise (kendine para aktarımı), fonksiyon direkt olarak sonlanır ve deadlock durumunu önlemek için işlem yapmadan geri döner. -
from
veto
hesapları arasında mutex kilitleme işlemi gerçekleştirilir.std::lock
işlevi, her iki mutex’i de aynı anda kilitleyerek deadlock durumunu önler. Mutex’lerin kilitleme sırası belirli bir algoritma tarafından belirlenir, bu durumdafrom
veto
hesaplarının adreslerinin karşılaştırması kullanılır. -
std::lock_guard
nesneleri kullanılarak mutex’lerin kilidi güvence altına alınır vestd::adopt_lock
argümanıyla mutex kilidinin devralındığı belirtilir. Bu sayede, mutex’lerin kilidi,std::lock_guard
nesnelerinin kapsamı sona erdiğinde otomatik olarak serbest bırakılır. Bu yaklaşım, güvenli ve temiz bir şekilde mutex kilitleme ve kilidi serbest bırakma işlemlerini yönetmeyi sağlar. -
Son olarak,
from
hesabındanamount
miktarı düşülür veto
hesabınaamount
miktarı eklenir. Böylece para transferi işlemi tamamlanmış olur.
main
fonksiyonunda ise, iki banka hesabı (my_account
ve your_account
) oluşturulur. Ardından, std::thread
kullanılarak iki ayrı iş parçacığı başlatılır ve her bir iş parçacığı üzerinde transfer
fonksiyonu çağrılır. Her bir iş parçacığı, farklı banka hesapları arasında para transferini gerçekleştirir.
Son olarak, iş parçacıklarının tamamlanması beklenir (t1.join()
ve t2.join()
) ve her bir banka hesabının bakiyesi std::cout
ile ekrana yazdırılır.
Bu kod örneği, paralel işlem yaparken mutex’lerin uygun bir şekilde kullanılmasıyla deadlock durumlarını önlemeyi ve güvenli bir şekilde senkronizasyon sağlamayı göstermektedir.
Kodun çıktısı aşağıdaki gibi olacaktır:
my_account.balance = 95
your_account.balance = 55
8.4.3.3. std::scoped_lock
std::scoped_lock
, C++17 standardıyla birlikte tanıtılan bir sınıftır ve birden fazla mutex’i atomik bir şekilde kilitlemek için kullanılır. Bu sınıf, std::unique_lock
veya std::lock_guard
yerine kullanılarak, daha güvenli ve kolay bir şekilde mutex’leri kilitleme işlemini gerçekleştirir.
std::scoped_lock
, std::mutex
veya std::recursive_mutex
türünden birden fazla mutex’i alabilir ve bu mutex’leri aynı anda kilitleyerek deadlock durumlarını önler. Mutex’lerin kilitleme sırası belirli bir algoritma tarafından belirlenir.
std::scoped_lock
sınıfı, std::lock()
işlevini kullanarak mutex’leri kilitleme işlemini gerçekleştirir. Bununla birlikte, std::lock_guard
veya std::unique_lock
nesnelerinden farklı olarak, std::scoped_lock
nesnesinin kendisi, mutex kilitleme ve kilidi serbest bırakma işlemlerini yönetir. Yani, std::scoped_lock
nesnesi, kapsamı sona erdiğinde otomatik olarak mutex’leri serbest bırakır.
Aşağıda std::scoped_lock
kullanımını gösteren bir örnek verilmiştir:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mutex1;
std::mutex mutex2;
void Thread1() {
std::scoped_lock lock(mutex1, mutex2);
// İşlemler yapılır
// Mutex kilidi otomatik olarak serbest bırakılır
}
void Thread2() {
std::scoped_lock lock(mutex1, mutex2);
// İşlemler yapılır
// Mutex kilidi otomatik olarak serbest bırakılır
}
int main() {
std::thread t1(Thread1);
std::thread t2(Thread2);
t1.join();
t2.join();
return 0;
}
Yukarıdaki örnekte, Thread1
ve Thread2
adlı iki iş parçacığı oluşturulur. Her bir iş parçacığı, std::scoped_lock
nesnesini kullanarak mutex1
ve mutex2
mutex’lerini aynı anda kilitleyerek işlemleri gerçekleştirir. std::scoped_lock
nesnesi, kapsamı sona erdiğinde (yani işlemler tamamlandığında) otomatik olarak mutex kilidini serbest bırakır. Bu, mutex kilitleme ve kilidi serbest bırakma işlemlerini güvenli ve kolay bir şekilde yönetir.
Bir diğer örnek olarak:
#include <chrono>
#include <functional>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
using namespace std::chrono_literals;
struct Employee
{
std::vector<std::string> lunch_partners;
std::string id;
std::mutex m;
Employee(std::string id) : id(id) {}
std::string partners() const
{
std::string ret = "Employee " + id + " has lunch partners: ";
for (const auto& partner : lunch_partners)
ret += partner + " ";
return ret;
}
};
void send_mail(Employee &, Employee &)
{
// simulate a time-consuming messaging operation
std::this_thread::sleep_for(1s);
}
void assign_lunch_partner(Employee &e1, Employee &e2)
{
static std::mutex io_mutex;
{
std::lock_guard<std::mutex> lk(io_mutex);
std::cout << e1.id << " and " << e2.id << " are waiting for locks" << std::endl;
}
{
// use std::scoped_lock to acquire two locks without worrying about
// other calls to assign_lunch_partner deadlocking us
// and it also provides a convenient RAII-style mechanism
std::scoped_lock lock(e1.m, e2.m);
// Equivalent code 1 (using std::lock and std::lock_guard)
// std::lock(e1.m, e2.m);
// std::lock_guard<std::mutex> lk1(e1.m, std::adopt_lock);
// std::lock_guard<std::mutex> lk2(e2.m, std::adopt_lock);
// Equivalent code 2 (if unique_locks are needed, e.g. for condition variables)
// std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
// std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
// std::lock(lk1, lk2);
{
std::lock_guard<std::mutex> lk(io_mutex);
std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
}
e1.lunch_partners.push_back(e2.id);
e2.lunch_partners.push_back(e1.id);
}
send_mail(e1, e2);
send_mail(e2, e1);
}
int main()
{
Employee alice("Alice"), bob("Bob"), christina("Christina"), dave("Dave");
// assign in parallel threads because mailing users about lunch assignments
// takes a long time
std::vector<std::thread> threads;
threads.emplace_back(assign_lunch_partner, std::ref(alice), std::ref(bob));
threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(bob));
threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(alice));
threads.emplace_back(assign_lunch_partner, std::ref(dave), std::ref(bob));
for (auto &thread : threads)
thread.join();
std::cout << alice.partners() << '\n' << bob.partners() << '\n'
<< christina.partners() << '\n' << dave.partners() << '\n';
}
Bu kod, bir şirketteki çalışanların öğle yemeği ortaklarını atamak için kullanılan bir senaryoyu simüle eder. Employee
adlı bir yapı, her bir çalışanın kimlik bilgilerini, öğle yemeği ortaklarını ve bir mutex’i içerir. send_mail
işlevi, zaman alıcı bir mesajlaşma işlemini temsil eder.
assign_lunch_partner
işlevi, iki çalışana öğle yemeği ortağı atamak için kullanılır. İşleyiş şu şekildedir:
-
İlk olarak, iki çalışanın mutex’lerini kilitleme işlemi gerçekleştirilir.
std::scoped_lock
kullanılarak iki mutex aynı anda kilitleyebilir. Bu, deadlock durumunu önlemek için uygun bir şekilde mutex’leri kilitleme işlemi yapar. Mutex kilitleme işlemi gerçekleşmeden önce, ilgili mutex’lerin kilidi bekletilmektedir. -
Ardından, iki mutex’in de başarılı bir şekilde kilidi alındığında,
io_mutex
adlı bir mutex kullanılarak bir konsol çıktısı yazdırılır. -
Daha sonra,
std::lock_guard
kullanılarak mutex’lerin kilidi otomatik olarak serbest bırakılır ve her iki çalışanınlunch_partners
vektörlerine birbirlerinin kimlik bilgileri eklenir. -
send_mail
işlevi, iki çalışana birbirlerine e-posta göndermeyi simüle eder.
main
işlevinde, Employee
yapısından dört çalışan oluşturulur. Ardından, std::thread
kullanılarak dört farklı iş parçacığı başlatılır ve her bir iş parçacığı üzerinde assign_lunch_partner
işlevi çağrılır. İş parçacıkları paralel olarak çalışır ve çalışanlara öğle yemeği ortakları atanır.
Son olarak, iş parçacıklarının tamamlanması beklenir (thread.join()
) ve her bir çalışanın öğle yemeği ortakları std::cout
ile ekrana yazdırılır.
Bu kod örneği, birden fazla mutex’i aynı anda kilitlemek için std::scoped_lock
sınıfını kullanmanın yanı sıra, mutex kilitleme ve serbest bırakma işlemlerini güvenli bir şekilde yönetmeyi ve deadlock durumlarını önlemeyi göstermektedir. Ayrıca, öğle yemeği atama işlemi gibi uzun süren işlemlerin paralel olarak çalışabilmesini sağlar.
Kodun çıktısı aşağıdaki gibi olacaktır:
Alice and Bob are waiting for locks
Alice and Bob got locks
Christina and Bob are waiting for locks
Christina and Alice are waiting for locks
Dave and Bob are waiting for locks
Dave and Bob got locks
Christina and Alice got locks
Christina and Bob got locks
Employee Alice has lunch partners: Bob Christina
Employee Bob has lunch partners: Alice Dave Christina
Employee Christina has lunch partners: Alice Bob
Employee Dave has lunch partners: Bob
8.7. Async Fonksiyon ve std::future
C++’ta async
fonksiyonları ve std::future
sınıfını kullanarak asenkron programlama yapabiliriz. Bu bize paralel veya beklemeli işlemleri daha verimli bir şekilde yönetme imkanı sunar.
std::async
fonksiyonu, bir işlevi asenkron olarak çağırmak için kullanılır ve std::future
sınıfı, bir işlemin sonucunu veya durumunu almak için kullanılır.
Öncelikle std::async
fonksiyonunu inceleyelim:
#include <iostream>
#include <future>
int foo(int x, int y) {
std::cout << "foo() started" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "foo() finished" << std::endl;
return x + y;
}
int main() {
std::future<int> result = std::async(foo, 2, 3);
std::cout << "Main thread continues its execution" << std::endl;
int sum = result.get();
std::cout << "Result: " << sum << std::endl;
return 0;
}
Yukarıdaki örnekte, foo
adlı işlevi std::async
ile asenkron olarak çağırıyoruz. İşlev, iki parametre alır ve bu parametrelerin toplamını hesaplar. İşlevin içinde 2 saniye uyku süresi simüle edilmiştir. std::async
fonksiyonu, işlevin asenkron olarak çağrılmasını sağlar ve bir std::future
nesnesi döndürür. Bu nesne, işlemin sonucunu tutar.
Ana iş parçacığı std::cout
ile “Main thread continues its execution” mesajını yazdırırken, result.get()
çağrısı sonucun tamamlanmasını bekler ve sonucu elde eder. Sonucu sum
değişkenine atayarak kullanabiliriz.
Çıktı şu şekilde olacaktır:
foo() started
Main thread continues its execution
foo() finished
Result: 5
Görüldüğü gibi, ana iş parçacığı foo
işlevinin tamamlanmasını beklemek yerine iş parçacığını başlatır ve ardından devam eder. Sonucu elde etmek için result.get()
çağrısı, sonucun tamamlanmasını bekler.
std::async
fonksiyonunun yanı sıra, std::future
sınıfı da önemlidir. Bu sınıf, asenkron işlemlerin sonucunu tutmak ve sonuçla ilgili operasyonlar yapmak için kullanılır.
#include <iostream>
#include <future>
int foo(int x, int y) {
std::cout << "foo() started" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "foo() finished" << std::endl;
return x + y;
}
int main() {
std::future<int> result = std::async(foo, 2, 3);
std::cout << "Main thread continues its execution" << std::endl;
if (result.valid()) {
std::cout << "Result is valid" << std::endl;
}
int sum = result.get();
std::cout << "Result: " << sum << std::endl;
if (!result.valid()) {
std::cout << "Result is no longer valid" << std::endl;
}
return 0;
}
Yukarıdaki örnekte, std::future
sınıfının valid()
üye işlevini kullanarak sonucun geçerli olup olmadığını kontrol ediyoruz. Sonuç başarılı bir şekilde alındıktan sonra valid()
yanıtı false
olur. Bu nedenle, sonuç alındıktan sonra result
nesnesi geçersiz hale gelir ve tekrar kullanılamaz.
Çıktı şu şekilde olacaktır:
foo() started
Main thread continues its execution
Result is valid
foo() finished
Result: 5
Result is no longer valid
Bu örnekte, result.valid()
kontrolü sayesinde sonucun geçerli olduğunu ve işlemin tamamlandığını doğrulayabiliyoruz.
std::async
ve std::future
, C++’ta asenkron programlama yapmak için kullanılan güçlü araçlardır. Bu araçlar, hesaplama yoğunluğu olan işlemleri paralel olarak yürütmek, beklemeli işlemleri verimli bir şekilde yönetmek ve paralel hesaplamaların sonuçlarını kullanmak için idealdir.
C++’ta std::async
fonksiyonu, farklı politikalarla kullanılabilmektedir. Bu politikalar, işlemlerin nasıl yürütüleceğini ve hangi iş parçacığı veya iş parçacığı havuzu kullanılacağını belirler. std::async
fonksiyonunun varsayılan politikası, sistem tarafından belirlenen bir politikadır. Ancak, C++17’den itibaren kullanıcılar, özelleştirilmiş politikaları da belirleyebilirler.
C++17’de sunulan politikalar aşağıdaki gibidir:
-
std::launch::async
: İşlemi hemen başlatır ve yeni bir iş parçacığında çalıştırır. Bu, işlemi paralel olarak yürütmek için uygun bir seçenektir.
#include <iostream>
#include <future>
int foo(int x, int y) {
std::cout << "foo() started" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "foo() finished" << std::endl;
return x + y;
}
int main() {
std::future<int> result = std::async(std::launch::async, foo, 2, 3);
std::cout << "Main thread continues its execution" << std::endl;
int sum = result.get();
std::cout << "Result: " << sum << std::endl;
return 0;
}
Yukarıdaki örnekte, std::launch::async
politikası kullanılarak std::async
fonksiyonu çağrılmaktadır. Bu, işlemi hemen başlatır ve yeni bir iş parçacığında çalıştırır.
-
std::launch::deferred
: İşlemi erteleyerek işlevi çağıran iş parçacığında çalıştırır. İşlev,get()
çağrısı yapıldığında yürütülür.
#include <iostream>
#include <future>
int foo(int x, int y) {
std::cout << "foo() started" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "foo() finished" << std::endl;
return x + y;
}
int main() {
std::future<int> result = std::async(std::launch::deferred, foo, 2, 3);
std::cout << "Main thread continues its execution" << std::endl;
int sum = result.get();
std::cout << "Result: " << sum << std::endl;
return 0;
}
Yukarıdaki örnekte, std::launch::deferred
politikası kullanılarak std::async
fonksiyonu çağrılmaktadır. İşlem ertelenir ve işlev, get()
çağrısı yapıldığında çağıran iş parçacığında çalıştırılır.
-
std::launch::async | std::launch::deferred
: Bu, sistem tarafından belirlenen politikayı kullanır. Bu, hem asenkron hem de ertelenmiş yürütme için uygun olan en iyi seçeneği sağlar.
#include <iostream>
#include <future>
int foo(int x, int y) {
std::cout << "foo() started" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "foo() finished" << std::endl;
return x + y;
}
int main() {
std::future<int> result = std::async(std::launch::async | std::launch::deferred, foo, 2, 3);
std::cout << "Main thread continues its execution" << std::endl;
int sum = result.get();
std::cout << "Result: " << sum << std::endl;
return 0;
}
Yukarıdaki örnekte, std::launch::async | std::launch::deferred
politikası kullanılarak std::async
fonksiyonu çağrılmaktadır. Bu, hem asenkron hem de ertelenmiş yürütme için en iyi seçeneği sistem tarafından belirler.
Bu politikalar, std::async
fonksiyonunu kullanırken işlemlerin nasıl yürütüleceğini belirlememizi sağlar. Bu sayede, paralel işlemler oluşturabilir, iş parçacığı havuzlarını kullanabilir ve beklemeli işlemleri yönetebiliriz.
8.6. İmplementasyonlar
Bu kısımda thread-pool
, consumer-producer
, ve Round Robin
algoritmasının implementasyonları gerçekleştirilecektir.
8.6.1. Thread Pool
Elbette! İşte C++20 özelliklerini kullanarak bir thread havuzu (thread pool) implementasyonu örneği:
#include <iostream>
#include <vector>
#include <queue>
#include <functional>
#include <thread>
#include <condition_variable>
class ThreadPool {
public:
ThreadPool(size_t numThreads) : stop(false) {
for (size_t i = 0; i < numThreads; ++i) {
threads.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queueMutex);
condition.wait(lock, [this] { return stop || !tasks.empty(); });
if (stop && tasks.empty()) {
return;
}
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queueMutex);
stop = true;
}
condition.notify_all();
for (std::thread& thread : threads) {
thread.join();
}
}
template <typename Func, typename... Args>
void enqueue(Func&& func, Args&&... args) {
auto task = std::bind(std::forward<Func>(func), std::forward<Args>(args)...);
{
std::unique_lock<std::mutex> lock(queueMutex);
tasks.emplace([task] { task(); });
}
condition.notify_one();
}
private:
std::vector<std::thread> threads;
std::queue<std::function<void()>> tasks;
std::mutex queueMutex;
std::condition_variable condition;
bool stop;
};
// Kullanım örneği
void printNumber(int number) {
std::cout << "Thread ID: " << std::this_thread::get_id() << ", Number: " << number << std::endl;
}
int main() {
ThreadPool pool(4);
// İş parçacıklarını havuza ekleyelim
for (int i = 0; i < 10; ++i) {
pool.enqueue(printNumber, i);
}
// Tüm iş parçacıklarının tamamlanmasını bekleyelim
std::this_thread::sleep_for(std::chrono::seconds(2));
return 0;
}
Bu örnek, std::function
, std::bind
, std::thread
, std::mutex
, std::condition_variable
ve std::unique_lock
gibi bileşenleri kullanarak basit bir thread havuzu (thread pool) implementasyonu sağlar. ThreadPool
sınıfı, belirli bir sayıda iş parçacığı oluşturur ve iş parçacığı havuzuna iş parçacıklarını eklemek ve çalıştırmak için enqueue
işlevini sağlar.
İşte kodun açıklaması:
-
ThreadPool
sınıfı, önceden belirlenmiş bir sayıda iş parçacığı oluşturur. Bu sayı, sınıfın yapıcı fonksiyonuna verilennumThreads
parametresiyle belirlenir. -
enqueue
işlevi, thread havuzuna bir iş parçacığı eklemek için kullanılır. Bu işlev, farklı parametre türleriyle çağrılabilen bir şablon işlevdir. -
Thread havuzunun oluşturulduğu yapıcı fonksiyonda, her iş parçacığı için bir lambda ifadesi oluşturulur. Bu lambda ifadesi, iş parçacığı tarafından sürekli olarak çalıştırılacak bir döngüye sahiptir.
-
İş parçacığı döngüsü, thread havuzunun durdurulmadığı sürece çalışır. Döngüde, önce bir
std::function<void()>
görevi tanımlanır. -
Ardından, görevi almak için bir
std::unique_lock<std::mutex>
oluşturulur vestd::condition_variable
üzerinde beklenir.condition.wait()
işlevi, bir lambda ifadesi kullanarak, havuzun durdurulması veya görevlerin boşaltılması durumunda beklemeyi sonlandırır. -
Döngü, havuz durdurulduğunda veya görevler bittiğinde sonlanır.
-
enqueue
işlevi, bir görevi thread havuzuna eklemek için kullanılır. İşlev, görevi birstd::function<void()>
nesnesine bağlar ve bu görevi bir kuyruğa ekler. -
Ardından,
std::condition_variable
üzerinde bircondition.notify_one()
işlemi gerçekleştirerek bekleyen bir iş parçacığın uyandırılmasını sağlar. -
~ThreadPool
yapıcısı, thread havuzu nesnesi sona erdiğinde çalışır. İlk olarak, havuzu durdurmak içinstop
bayrağı ayarlanır. -
Ardından,
std::condition_variable
üzerinde bircondition.notify_all()
işlemi gerçekleştirerek tüm bekleyen iş parçacıklarını uyandırır. -
Son olarak, tüm iş parçacıklarının tamamlanmasını beklemek için
std::thread::join()
işlevi kullanılır. -
printNumber
işlevi, her bir iş parçacığı tarafından çalıştırılan basit bir işlevdir. İş parçacığı kimliğini ve bir sayıyı ekrana yazdırır. -
main
fonksiyonunda, birThreadPool
nesnesi oluşturulur veenqueue
işlevi kullanılarak iş parçacıkları havuza eklenir. -
Son olarak, tüm iş parçacıklarının tamamlanmasını beklemek için
std::this_thread::sleep_for()
işlevi kullanılır ve program sonlanır.
Bu örnek, basit bir thread havuzu implementasyonunu göstermektedir. Thread havuzları, aynı anda çok sayıda görevi paralel olarak çalıştırmak için kullanılabilir ve iş parçacıklarının yönetimi ve senkronizasyonu gibi karmaşık detayları gizler. Bu sayede, çoklu iş parçacığı uygulamalarında performansı artırabilir ve kaynakları daha verimli bir şekilde kullanabilirsiniz.
8.6.2. Consumer-Producer
8.6.2.1. Basit İmplementasyon
Basit bir Consumer-Producer kodu:
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
std::queue<int> buffer;
const int BUFFER_SIZE = 10;
std::mutex mutex;
std::condition_variable cv;
void Producer()
{
for (int i = 1; i <= 20; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(500)); // Üretim süresi
std::unique_lock<std::mutex> lock(mutex);
// Buffer dolu ise bekle
cv.wait(lock, [] { return buffer.size() < BUFFER_SIZE; });
buffer.push(i); // Üretim
std::cout << "Producer produced: " << i << std::endl;
lock.unlock(); // Mutex kilidini aç
cv.notify_all(); // Tüketicilere haber ver
}
}
void Consumer()
{
while (true) {
std::this_thread::sleep_for(std::chrono::milliseconds(800)); // Tüketim süresi
std::unique_lock<std::mutex> lock(mutex);
// Buffer boş ise bekle
cv.wait(lock, [] { return !buffer.empty(); });
int value = buffer.front(); // Tüketim
buffer.pop();
std::cout << "Consumer consumed: " << value << std::endl;
lock.unlock(); // Mutex kilidini aç
cv.notify_all(); // Üreticiye haber ver
if (value == 20) {
break; // Tüketim tamamlandı, döngüyü sonlandır
}
}
}
int main()
{
std::thread producerThread(Producer);
std::thread consumerThread(Consumer);
producerThread.join();
consumerThread.join();
return 0;
}
Bu kodda, basit bir Consumer-Producer senaryosu gerçekleştirilir. buffer
adlı bir kuyruk yapısı kullanarak üretici işlem tarafından üretilen değerlerin tüketici işlem tarafından tüketilmesi sağlanır.
Producer
işlevi, 1’den 20’ye kadar olan sayıları üretir ve buffer
kuyruğuna ekler. Her bir üretim arasında bir süre beklenir. Eğer buffer
kuyruğu dolu ise, üretici işlem cv.wait()
ile beklemeye alınır. Üretim gerçekleştikten sonra mutex kilidi açılır, tüketici işlemlere haber verilir ve işlem tekrar beklemeye alınır.
Consumer
işlevi, buffer
kuyruğundan değerleri tüketir. Eğer buffer
kuyruğu boş ise, tüketici işlem cv.wait()
ile beklemeye alınır. Değer tüketildikten sonra mutex kilidi açılır, üretici işleme haber verilir ve işlem tekrar beklemeye alınır. Son olarak, tüketim tamamlandığında döngüden çıkılır ve işlem sonlandırılır.
Ana işlevde, üretici ve tüketici işlemler oluşturulur, beklenir ve ardından program sonlandırılır.
Bu örnek kod, basit bir Consumer-Producer senaryosunu semafor olmadan gerçekleştirir. İki işlem arasında veri paylaşımını kontrol etmek için mutex ve condition variable kullanılır.
8.6.2.2. Semaphore Kullanımı ile
#include <iostream>
#include <thread>
#include <vector>
#include <condition_variable>
class Semaphore {
public:
explicit Semaphore(int count = 0) : count_(count) {}
void Notify() {
std::unique_lock<std::mutex> lock(mutex_);
++count_;
cv_.notify_one();
}
void Wait() {
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this] { return count_ > 0; });
--count_;
}
private:
std::mutex mutex_;
std::condition_variable cv_;
int count_;
};
const int BUFFER_SIZE = 5;
std::vector<int> buffer(BUFFER_SIZE);
Semaphore emptySlots(BUFFER_SIZE);
Semaphore fullSlots;
std::mutex mutex;
void Producer(int producerId)
{
for (int i = 1; i <= 10; ++i) {
emptySlots.Wait(); // Boş yuva sayısını kontrol et
std::lock_guard<std::mutex> lock(mutex); // Buffer'ı kilitle
buffer[i % BUFFER_SIZE] = i; // Üretilen veriyi buffer'a ekle
std::cout << "Producer " << producerId << " produced: " << i << std::endl;
fullSlots.Notify(); // Dolu yuva sayısını artır
std::this_thread::sleep_for(std::chrono::milliseconds(500)); // Biraz bekle
}
}
void Consumer(int consumerId)
{
for (int i = 1; i <= 10; ++i) {
fullSlots.Wait(); // Dolu yuva sayısını kontrol et
std::lock_guard<std::mutex> lock(mutex); // Buffer'ı kilitle
int value = buffer[i % BUFFER_SIZE]; // Buffer'dan veriyi al
std::cout << "Consumer " << consumerId << " consumed: " << value << std::endl;
emptySlots.Notify(); // Boş yuva sayısını artır
std::this_thread::sleep_for(std::chrono::milliseconds(800)); // Biraz bekle
}
}
int main()
{
std::thread producerThread(Producer, 1);
std::thread consumerThread(Consumer, 1);
producerThread.join();
consumerThread.join();
return 0;
}
Bu kod, Consumer-Producer senaryosunu semaforlar kullanarak gerçekleştirir.
Semaphore
adlı bir sınıf tanımlanır. Bu sınıf, mutex ve condition variable kullanarak semafor işlevselliğini sağlar. Notify()
işlevi semaforu artırır ve bekleyen iş parçacıklarını uyarır. Wait()
işlevi ise semaforu kontrol eder ve uygun koşulu bekleyerek azaltır.
Ana fonksiyon olan main()
işlevi, bir üretici ve bir tüketici iş parçacığı oluşturur. Bu iş parçacıkları producerThread
ve consumerThread
olarak adlandırılır.
Producer
işlevi, 1’den 10’a kadar olan sayıları üretir ve buffer’a ekler. İlk olarak, emptySlots.Wait()
işlevi kullanılarak boş yuva sayısı kontrol edilir. Eğer boş yuva mevcut değilse, üretici beklemek zorunda kalır. Ardından, std::lock_guard
kullanılarak mutex
kilidi alınır ve buffer’a veri eklenir. Üretilen değer ekrana yazdırılır. Son olarak, fullSlots.Notify()
işlevi kullanılarak dolu yuva sayısı artırılır ve bir süre beklenir.
Consumer
işlevi, 1’den 10’a kadar olan sayıları tüketir. İlk olarak, fullSlots.Wait()
işlevi kullanılarak dolu yuva sayısı kontrol edilir. Eğer dolu yuva mevcut değilse, tüketici beklemek zorunda kalır. Ardından, std::lock_guard
kullanılarak mutex
kilidi alınır ve buffer’dan veri alınır. Tüketilen değer ekrana yazdırılır. Son olarak, emptySlots.Notify()
işlevi kullanılarak boş yuva sayısı artırılır ve bir süre beklenir.
Son olarak, producerThread
ve consumerThread
iş parçacıkları join()
işlevi ile beklenir ve program sonlanır.
Bu kod, semaforları kullanarak üretici ve tüketici iş parçacıkları arasında senkronizasyon sağlar. Semaforlar, boş yuva ve dolu yuva sayılarını kontrol ederek üretici ve tüketici işlemlerin senkronize bir şekilde çalışmasını sağlar. Mutex, buffer’a eşzamanlı erişimi kontrol eder ve veri bütünlüğünü sağlar.
8.6.3 Round Robin Algoritması
Round Robin (RR), bir işlemci zaman paylaşımı (scheduling) algoritmasıdır. Bu algoritma, bir dizi işlem arasında zamanı eşit bir şekilde böler ve işlemcilere sırayla paylaştırır. Her bir işlem belirli bir zamana kadar çalışır ve ardından işlemci diğer işleme geçer. Bu şekilde, işlemciler arasında adil bir dağılım sağlanır ve her bir işlem belirli bir zaman diliminde çalışma fırsatı bulur.
Round Robin algoritmasının temel özellikleri şunlardır:
- İşlemciler arasında zaman dilimleri eşit olarak paylaştırılır. Her bir işlem, belirli bir zaman diliminde çalışır (sabit bir zaman dilimi veya kuantum olarak adlandırılır).
- İşlemciler, sırayla işlemleri çalıştırır. İlk işlem tamamlanmadan diğer işleme geçilmez, kuantum süresi dolduğunda işlemci diğer işleme geçer.
- İşlemciler arasında adil bir zaman paylaşımı sağlar. Her işlem, aynı miktarda işlemci zamanı alır ve öncelikli bir işlemi diğerlerinden ayırt etmez.
Round Robin algoritması, çoklu kullanıcılı işletim sistemlerinde ve zaman paylaşımı gerektiren ortamlarda yaygın olarak kullanılır. Özellikle interaktif uygulamalar ve çoklu iş parçacıklı programlar için uygundur. Bu algoritma, işlemciler arasında iş yükünün dengeli bir şekilde dağıtılmasını sağlar ve cevap süresini adil bir şekilde dağıtır.
Örneğin, bir Round Robin algoritmasıyla çalışan bir işletim sistemi, her bir kullanıcıya sırayla zaman paylaştırabilir. Her kullanıcı, belirli bir kuantum süresi boyunca işlemciyi kullanabilir ve ardından diğer kullanıcıya geçiş yapılır. Bu şekilde, kullanıcılar arasında eşit ve adil bir işlemci kullanımı sağlanır.
Round Robin algoritması, basit ve adil bir işlemci zaman paylaşımı yöntemi sağlar. Ancak, tüm işlemlerin aynı öneme sahip olduğunu varsayar ve işlem sürelerinin sabit olduğunu varsayarak çalışır. Gerçek dünya senaryolarında, bazı işlemler diğerlerinden daha fazla işlemci zamanı talep edebilir veya işlem süreleri değişkenlik gösterebilir. Bu durumda, Round Robin algoritması optimize edilmiş diğer zaman paylaşımı algoritmalarıyla değiştirilebilir.
8.6.3.1. Basit İmplementasyon
Bu kod, Round Robin algoritmasının basit bir örneğini göstermektedir. Kod, kullanıcı tarafından tanımlanan görevleri temsil eden Task
yapısını kullanır ve bu görevleri Round Robin algoritmasına göre zaman paylaşımı ile sırayla çalıştırır.
#include <iostream>
#include <vector>
#include <algorithm>
struct Task {
int id;
int remainingTime;
int priority;
void (*executeFunction)();
};
void taskFunction1() {
std::cout << "Executing Task 1 function" << std::endl;
}
void taskFunction2() {
std::cout << "Executing Task 2 function" << std::endl;
}
void taskFunction3() {
std::cout << "Executing Task 3 function" << std::endl;
}
void roundRobin(std::vector<Task>& tasks, int quantum) {
int numTasks = tasks.size();
int currentTime = 0;
bool isCompleted = false;
while (!isCompleted) {
isCompleted = true;
// Sort tasks based on priority
std::sort(tasks.begin(), tasks.end(), [](const Task &t1, const Task &t2){
return t1.priority < t2.priority;
});
for (auto& task : tasks) {
if (task.remainingTime > 0) {
isCompleted = false;
int executeTime = std::min(quantum, task.remainingTime);
task.remainingTime -= executeTime;
currentTime += executeTime;
std::cout << "Task " << task.id << " executed for " << executeTime << " units. Current time: " << currentTime << std::endl;
task.executeFunction();
}
}
}
}
int main() {
std::vector<Task> tasks = {
{1, 10, 2, taskFunction1},
{2, 4, 1, taskFunction2},
{3, 5, 3, taskFunction3}
};
int quantum = 3;
roundRobin(tasks, quantum);
return 0;
}
Kodun açıklaması:
- İlk olarak,
Task
yapısı tanımlanır. Bu yapı, görevin kimliği (id
), kalan süresi (remainingTime
), önceliği (priority
) ve yürütme fonksiyonunu (executeFunction
) içerir. - Ardından, her bir görevin yürütülmesini sağlayan üç adet örnek yürütme fonksiyonu (
taskFunction1
,taskFunction2
,taskFunction3
) tanımlanır. Bu fonksiyonlar sadece ilgili görevin yürütüldüğünü ekrana yazdırır. -
roundRobin
işlevi tanımlanır. Bu işlev, görevleri Round Robin algoritması kullanarak zaman paylaşımı ile sırayla çalıştırır. İşlev, görevlerin listesini (tasks
) ve kuantum süresini (quantum
) parametre olarak alır. -
roundRobin
işlevi içinde, görevlerin sayısı (numTasks
), geçerli zamanı (currentTime
) ve tüm görevlerin tamamlandığını belirten bir bayrak (isCompleted
) tanımlanır. - Döngü içinde, görevlerin sırasını önceliklerine göre sıralamak için
std::sort
kullanılır. Bu şekilde, her turda öncelikli görevlerin önce çalıştırılması sağlanır. - Döngü içinde, her bir görevin süresi (
remainingTime
) kontrol edilir. Eğer süre sıfırdan büyük ise görev henüz tamamlanmamış demektir. Bu durumda, en fazla kuantum süresi kadar çalışma yapılır. Görevin kalan süresi ve geçerli zaman güncellenir. Ayrıca, çalıştırma süresi ve geçerli zaman ekrana yazdırılır. - Son olarak, görevin ilgili yürütme fonksiyonu çağrılır.
- Döngü, tüm görevlerin tamamlandığı (
isCompleted
bayrağıtrue
olduğu) duruma kadar devam eder. -
main
işlevinde, örnek görevler (tasks
) ve kuantum süresi (quantum
) tanımlanır. -
roundRobin
işlevi çağrılır ve görevler Round Robin algoritmasıyla sırayla çalıştırılır. - Program sonlanır.
Bu kod, basit bir şekilde Round Robin algoritmasını uygulamaktadır. Önceliklere göre sıralama ve kuantum süresine göre işlem sürelerini belirleme gibi algoritmanın temel prensiplerini içerir. Daha gerçekçi senaryolar için farklı optimizasyonlar ve senkronizasyon mekanizmaları gerekebilir.
8.6.3.2. Consumer-Producer Implemantasyonu
Bu kod, Consumer-Producer modelini kullanarak Round Robin algoritmasını simüle eder.
#include <iostream>
#include <vector>
#include <algorithm>
#include <chrono>
#include <thread>
#include <mutex>
#include <condition_variable>
struct Task {
int id;
int remainingTime;
int priority;
void (*executeFunction)();
};
void taskFunction1() {
std::cout << "Executing Task 1 function" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1)); // Simulating task execution time
}
void taskFunction2() {
std::cout << "Executing Task 2 function" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2)); // Simulating task execution time
}
void taskFunction3() {
std::cout << "Executing Task 3 function" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3)); // Simulating task execution time
}
bool comparePriority(const Task& task1, const Task& task2) {
return task1.priority < task2.priority;
}
std::mutex mtx;
std::condition_variable cv;
std::vector<Task> taskQueue;
bool isCompleted = false;
void producer() {
std::this_thread::sleep_for(std::chrono::seconds(1)); // Simulating task arrival time
std::lock_guard<std::mutex> lock(mtx);
taskQueue.push_back({1, 10, 2, taskFunction1});
taskQueue.push_back({2, 4, 1, taskFunction2});
taskQueue.push_back({3, 5, 3, taskFunction3});
std::cout << "Tasks produced." << std::endl;
cv.notify_one();
}
void consumer(int quantum) {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return !taskQueue.empty() || isCompleted; });
if (isCompleted && taskQueue.empty()) {
break;
}
Task task = taskQueue.front();
taskQueue.erase(taskQueue.begin());
lock.unlock();
int executeTime = std::min(quantum, task.remainingTime);
task.remainingTime -= executeTime;
std::cout << "Task " << task.id << " executed for " << executeTime << " units." << std::endl;
task.executeFunction();
if (task.remainingTime > 0) {
lock.lock();
taskQueue.push_back(task);
lock.unlock();
}
else {
std::cout << "Task " << task.id << " completed." << std::endl;
}
}
}
int main() {
int quantum = 3;
std::thread producerThread(producer);
std::thread consumerThread(consumer, quantum);
producerThread.join();
isCompleted = true;
cv.notify_all();
consumerThread.join();
std::cout << "All tasks completed." << std::endl;
return 0;
}
Kodun açıklaması:
-
Task
yapısı, görevin kimliği (id
), kalan süresi (remainingTime
), önceliği (priority
) ve yürütme fonksiyonunu (executeFunction
) içerir. -
taskFunction1
,taskFunction2
vetaskFunction3
fonksiyonları, her bir görevin yürütme işlevini temsil eder ve sadece görevin yürütüldüğünü ekrana yazdırır. Aynı zamanda,std::this_thread::sleep_for
işlevini kullanarak görevin yürütme süresini simüle eder. -
comparePriority
işlevi, görevleri önceliğe göre sıralamak için kullanılan karşılaştırma fonksiyonudur. -
mtx
(mutex) vecv
(condition_variable) değişkenleri, senkronizasyon ve senkronizasyon olaylarını yönetmek için kullanılır. -
taskQueue
vektörü, üretilen görevlerin depolandığı kuyruğu temsil eder. -
isCompleted
bayrağı, tüm görevlerin tamamlandığını belirtmek için kullanılır. -
producer
işlevi, üretici iş parçacığı tarafından çağrılır ve belirli bir süre bekledikten sonra görevleri üretir. Üretilen görevlertaskQueue
kuyruğuna eklenir vecv.notify_one()
çağrısı ile tüketici iş parçacığına haber verilir. -
consumer
işlevi, tüketici iş parçacığı tarafından çağrılır ve görevleri tüketir.cv.wait()
işlevi ile görevlerin üretilmesini bekler ve uyandırıldığındataskQueue
kuyruğunda görevlerin olup olmadığını kontrol eder. Görev varsa, en öncelikli görevi alır ve yürütür. Görevin kalan süresi hesaplanır vestd::this_thread::sleep_for
işleviyle yürütme süresi simüle edilir. Eğer görevin kalan süresi sıfırdan büyükse, kuyruğa geri eklenir, aksi halde tamamlandığı ekrana yazdırılır. Daha sonra, işlemci kilitlenmesi çözülerek diğer görevlere erişim sağlanır. -
main
işlevi, kuantum süresini belirler ve üretici ve tüketici iş parçacıklarını başlatır. İşlemler tamamlandığında bayrak vecv.notify_all()
çağrısı kullanılarak tüketici iş parçacığına haber verilir. Son olarak, program tamamlanmış görevleri ekrana yazdırır.
Bu kod, bir görev kuyruğu üzerinde Round Robin algoritmasını simüle etmektedir. Üretici iş parçacığı görevleri üretirken, tüketici iş parçacığı da görevleri alarak belirli bir süreyle çalıştırır. Her görevin kuantum süresi kullanılarak adil bir şekilde işlem paylaşımı sağlanır.