İleri Multithreading: condition_variable, atomic, async/future
Temel threading dersinde std::thread, std::mutex ve std::lock_guard ile tanıştık. Ama gerçek dünyada bunlar yetmez. Bir thread'in başka bir thread'i "beklemesi", verilerin kilitsiz (lock-free) paylaşılması, asenkron görev yönetimi ve thread havuzları gibi konular modern C++'ın vazgeçilmezleridir.
Bu derste C++11'den C++20'ye kadar gelen ileri threading araçlarını öğreneceksin. Her birini somut örneklerle ve analojilerle ele alacağız.
1. std::condition_variable — Koşul Değişkeni
Ne İşe Yarar?
Bir thread'in belirli bir koşul gerçekleşene kadar uyumasını ve koşul gerçekleştiğinde uyandırılmasını sağlar. Bunu şöyle düşün: bir fırında ekmek bekliyorsun. Sürekli "ekmek var mı?" diye sormak yerine, fırıncı hazır olduğunda sana sesleniyor (notify ediyor).
std::condition_variable her zaman bir std::mutex ile birlikte kullanılır. Mutex veriyi korur, condition_variable ise "hazır" sinyalini verir.
Producer-Consumer Pattern (Üretici-Tüketici)
Bu pattern'in en klasik kullanım alanı kuyruk (queue) yapısıdır. Bir thread kuyruğa veri ekler (producer), diğeri kuyruktan veri alır (consumer). Aralarında condition_variable ile haberleşirler.
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::queue<int> buffer;
std::mutex mtx;
std::condition_variable cv;
bool finished = false;
void producer() {
for (int i = 1; i <= 5; ++i) {
std::lock_guard<std::mutex> lock(mtx);
buffer.push(i);
std::cout << "Produced: " << i << "\n";
cv.notify_one();
}
std::lock_guard<std::mutex> lock(mtx);
finished = true;
cv.notify_one();
}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return !buffer.empty() || finished; });
while (!buffer.empty()) {
std::cout << "Consumed: " << buffer.front() << "\n";
buffer.pop();
}
if (finished) break;
}
}Dikkat: cv.wait() çağrısı mutlaka std::unique_lock ile yapılır, lock_guard ile değil. Çünkü wait kilit üzerinde unlock/lock işlemi yapar.
2. notify_one vs notify_all
notify_one() bekleyen thread'lerden yalnızca birini uyandırır. notify_all() ise bekleyen tüm thread'leri uyandırır. Hangisini kullanacağın senaryoya bağlı.
Analoji: Bir restoranda tek kişilik masa boşaldığında sadece bir kişiye haber verirsin (notify_one). Ama restoran kapanıyorsa herkese haber verirsin (notify_all).
// Tek consumer varsa — notify_one yeterli
cv.notify_one();
// Birden fazla consumer varsa veya tüm thread'leri
// uyandırmak istiyorsan — notify_all
cv.notify_all();Genel kural: Eğer sadece bir thread'in işi varsa notify_one, birden fazla thread aynı koşulu bekliyorsa notify_all kullan. Yanlış tercih performans kaybına yol açar ama doğruluğu bozmaz — notify_all her zaman güvenlidir.
void shutdown(std::vector<std::thread>& workers) {
{
std::lock_guard<std::mutex> lock(mtx);
stop_flag = true;
}
cv.notify_all(); // tüm worker'ları uyandır
for (auto& t : workers) t.join();
}3. Spurious Wakeup Problemi
Nedir?
İşletim sistemi bazen bir thread'i hiçbir notify çağrısı olmadan uyandırabilir. Buna spurious wakeup (sahte uyanma) denir. Bu, OS seviyesindeki optimizasyonlardan kaynaklanır ve standart tarafından "olabilir" olarak tanımlanmıştır.
While Loop Çözümü
Bu yüzden cv.wait() çağrısını her zaman bir koşul ile kullanmalısın. Predicate (koşul fonksiyonu) vermeyen ham wait() kullanırsan, sahte uyanmalara karşı savunmasız kalırsın.
// ❌ YANLIŞ — spurious wakeup'a açık
cv.wait(lock);
if (buffer.empty()) continue;
// ✅ DOĞRU — predicate ile otomatik kontrol
cv.wait(lock, [] { return !buffer.empty(); });Predicate versiyonu aslında şununla eşdeğer:
while (!predicate()) {
cv.wait(lock);
}Yani her uyanmada koşulu tekrar kontrol eder. Sahte uyanma olsa bile koşul sağlanmadıysa tekrar uyur.
⚠️ Dikkat: condition_variable::wait() çağrısını asla predicate olmadan kullanma. Spurious wakeup her platform'da olabilir ve debug etmesi çok zordur.
4. std::atomic — Lock-Free Programlama Temelleri
Nedir?
std::atomic, bir değişken üzerindeki okuma ve yazma işlemlerini mutex olmadan thread-safe yapar. Donanım seviyesinde atomik (bölünemez) işlemler kullanır.
Analoji: Bir sayacı düşün. Mutex ile korumak, sayaca dokunmadan önce kapıyı kilitlemek gibi. atomic ise sayacın kendisini "dokunulduğu anda güvenli" yapmak gibi — kapıya gerek yok.
#include <atomic>
#include <thread>
#include <iostream>
std::atomic<int> counter{0};
void increment(int times) {
for (int i = 0; i < times; ++i) {
counter.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
std::thread t1(increment, 100000);
std::thread t2(increment, 100000);
t1.join();
t2.join();
std::cout << "Counter: " << counter.load() << "\n"; // 200000
}fetch_add, fetch_sub, exchange, compare_exchange_strong gibi atomik operasyonlar mevcuttur. Basit sayaçlar ve flag'ler için mutex yerine atomic kullanmak çok daha performanslı.
5. std::atomic\<int\> ile Thread-Safe Sayaç
En yaygın kullanım: birden fazla thread'in aynı anda güncellediği sayaçlar. ++counter yerine counter.fetch_add(1) veya kısaca ++counter (atomic tipler için operator overload var) kullanılır.
#include <atomic>
#include <thread>
#include <vector>
#include <iostream>
std::atomic<int> active_connections{0};
void simulate_connection() {
++active_connections; // atomik increment
// ... bağlantı işlemleri ...
--active_connections; // atomik decrement
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i)
threads.emplace_back(simulate_connection);
for (auto& t : threads) t.join();
std::cout << "Remaining: " << active_connections << "\n"; // 0
}std::atomic<bool> da çok kullanılır — özellikle thread'lere "dur" sinyali göndermek için:
std::atomic<bool> running{true};
void worker() {
while (running.load()) {
// çalışmaya devam et
}
}
// Ana thread'den durdurmak için:
// running.store(false);💡 İpucu: Basit flag ve sayaçlar için std::atomic kullan. Karmaşık veri yapıları (map, vector vs.) için hâlâ mutex gerekir.
6. memory_order — Kısa Tanıtım
Atomik işlemler varsayılan olarak memory_order_seq_cst (sequentially consistent) kullanır. Bu en güvenli ama en yavaş sıralama garantisidir. Performans kritik durumlarda daha gevşek (relaxed) sıralama kullanılabilir.
Temel Sıralama Seçenekleri
| Sıralama | Anlamı |
|---|---|
memory_order_seq_cst | Tam sıralama garantisi — varsayılan, en güvenli |
memory_order_relaxed | Sadece atomiklik — sıralama garantisi yok |
memory_order_acquire | Bu okumadan sonraki işlemler öne alınamaz |
memory_order_release | Bu yazmadan önceki işlemler geriye atılamaz |
std::atomic<int> data{0};
std::atomic<bool> ready{false};
// Thread 1 — producer
data.store(42, std::memory_order_relaxed);
ready.store(true, std::memory_order_release);
// Thread 2 — consumer
while (!ready.load(std::memory_order_acquire)) {}
int value = data.load(std::memory_order_relaxed);
// value kesinlikle 42 olurBu örnekte release-acquire çifti sayesinde data yazımının ready flag'inden önce tamamlanması garanti edilir. relaxed tek başına bu garantiyi vermez.
⚠️ Dikkat: memory_order ileri seviye bir konudur. Emin değilsen varsayılan seq_cst kullan — performans farkı çoğu uygulamada ihmal edilebilir düzeydedir.
7. std::async ve std::future — Asenkron Görevler
Ne İşe Yarar?
std::async bir fonksiyonu asenkron olarak çalıştırır ve sonucunu std::future ile alırsın. Thread oluşturma, join etme gibi detaylarla uğraşmazsın.
Analoji: Online yemek siparişi gibi. Siparişi verirsin (async), bir referans numarası alırsın (future). İstediğin zaman "siparişim hazır mı?" diye sorarsın (future.get()).
#include <future>
#include <iostream>
int heavy_computation(int x) {
// uzun süren bir hesaplama
return x * x;
}
int main() {
std::future<int> result = std::async(std::launch::async, heavy_computation, 42);
// başka işler yap...
std::cout << "Calculating...\n";
// sonucu al (gerekirse bekler)
std::cout << "Result: " << result.get() << "\n"; // 1764
}Launch Policy
std::async iki çalışma politikasına sahip:
// Kesinlikle yeni thread'de çalıştır
auto f1 = std::async(std::launch::async, func);
// Tembel çalıştır — get() çağrılınca aynı thread'de
auto f2 = std::async(std::launch::deferred, func);
// Derleyiciye bırak (varsayılan)
auto f3 = std::async(func);std::launch::async politikası gerçekten ayrı thread'de çalıştırır. deferred ise get() çağrılana kadar hiç çalıştırmaz — lazy evaluation gibi düşünebilirsin.
#include <future>
#include <vector>
#include <numeric>
#include <iostream>
double parallel_sum(const std::vector<int>& v) {
auto mid = v.begin() + v.size() / 2;
auto left = std::async(std::launch::async, [&]() {
return std::accumulate(v.begin(), mid, 0.0);
});
double right = std::accumulate(mid, v.end(), 0.0);
return left.get() + right;
}8. std::promise ve std::future Çifti
std::async otomatik olarak future oluşturur. Ama bazen bir thread'in sonucunu "elle" ayarlamak istersin. İşte burada std::promise devreye girer.
Analoji: promise bir "söz mektubu". Bir thread söz verir ki bir değer üretecek. Diğer thread ise future ile o sözün yerine getirilmesini bekler.
#include <future>
#include <thread>
#include <iostream>
void compute(std::promise<int> prom) {
int result = 7 * 6;
prom.set_value(result); // sözü yerine getir
}
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(compute, std::move(prom));
std::cout << "Answer: " << fut.get() << "\n"; // 42
t.join();
}Hata durumunda set_exception kullanabilirsin:
void risky_task(std::promise<int> prom) {
try {
// riskli işlem
throw std::runtime_error("something failed");
prom.set_value(42);
} catch (...) {
prom.set_exception(std::current_exception());
}
}
// Alıcı tarafta:
// try { fut.get(); } catch (const std::exception& e) { ... }promise move-only'dir — kopyalanamaz, sadece std::move ile taşınır. Her promise-future çifti tek kullanımlıktır: bir kere set_value çağrıldıktan sonra tekrar çağrılamaz.
9. std::packaged_task
std::packaged_task, bir callable'ı (fonksiyon, lambda, functor) bir future ile paketler. async gibi çalışır ama ne zaman çalıştırılacağına sen karar verirsin.
Analoji: async bir kurye servisi gibi — siparişi verir vermez yola çıkar. packaged_task ise bir hediye paketi gibi — paketlersin ama ne zaman vereceğine sen karar verirsin.
#include <future>
#include <iostream>
int multiply(int a, int b) { return a * b; }
int main() {
std::packaged_task<int(int, int)> task(multiply);
std::future<int> result = task.get_future();
// task'ı istediğin zaman çalıştır
task(6, 7);
std::cout << "Result: " << result.get() << "\n"; // 42
}packaged_task genellikle thread pool implementasyonlarında kullanılır. Görevleri kuyruğa koyar, worker thread'ler sırayla çalıştırır:
#include <future>
#include <thread>
#include <iostream>
#include <functional>
int main() {
std::packaged_task<int()> task([] { return 100 + 200; });
auto fut = task.get_future();
// task'ı başka bir thread'e gönder
std::thread t(std::move(task));
t.join();
std::cout << "Result: " << fut.get() << "\n"; // 300
}10. Thread Pool Pattern
Neden Thread Pool?
Her görev için yeni thread oluşturmak pahalıdır. Thread Pool, sabit sayıda thread oluşturur ve görevleri bir kuyruktan alarak çalıştırır. Thread yaratma maliyetini bir kere öder, sonra tekrar tekrar kullanırsın.
Analoji: Bir çağrı merkezi düşün. Her müşteri aradığında yeni bir operatör işe almak yerine, sabit sayıda operatör (worker thread) var ve gelen çağrılar (görevler) kuyrukta bekliyor.
Basit Thread Pool Implementasyonu
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <functional>
#include <vector>
#include <future>
class ThreadPool {
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex mtx;
std::condition_variable cv;
bool stop = false;
public:
ThreadPool(size_t num_threads) {
for (size_t i = 0; i < num_threads; ++i) {
workers.emplace_back([this] { worker_loop(); });
}
}
void worker_loop() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this] {
return stop || !tasks.empty();
});
if (stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
}
template<typename F>
auto enqueue(F&& f) -> std::future<decltype(f())> {
using RetType = decltype(f());
auto task_ptr = std::make_shared<std::packaged_task<RetType()>>(
std::forward<F>(f)
);
std::future<RetType> result = task_ptr->get_future();
{
std::lock_guard<std::mutex> lock(mtx);
tasks.push([task_ptr]() { (*task_ptr)(); });
}
cv.notify_one();
return result;
}
~ThreadPool() {
{
std::lock_guard<std::mutex> lock(mtx);
stop = true;
}
cv.notify_all();
for (auto& w : workers) w.join();
}
};Kullanımı
int main() {
ThreadPool pool(4);
auto f1 = pool.enqueue([] { return 10 + 20; });
auto f2 = pool.enqueue([] { return 30 * 40; });
std::cout << f1.get() << "\n"; // 30
std::cout << f2.get() << "\n"; // 1200
}Bu implementasyonda enqueue, herhangi bir callable kabul eder ve bir future döner. Worker thread'ler condition_variable ile uyandırılır. Destructor'da stop flag'i set edilir ve tüm thread'ler join edilir.
💡 İpucu: Production kodunda kendi thread pool'unu yazmak yerine std::async, Intel TBB veya BS::thread_pool gibi kütüphaneleri değerlendir. Ama mekanizmayı anlamak için kendi implementasyonunu yapmak çok öğreticidir.
11. Producer-Consumer Tam Örneği
Şimdi condition_variable, mutex ve queue üçlüsünü birleştiren tam bir producer-consumer örneği yazalım. Birden fazla producer ve consumer olacak.
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <vector>
#include <atomic>
class MessageQueue {
std::queue<std::string> queue_;
std::mutex mtx_;
std::condition_variable cv_;
std::atomic<bool> shutdown_{false};
public:
void push(const std::string& msg) {
{
std::lock_guard<std::mutex> lock(mtx_);
queue_.push(msg);
}
cv_.notify_one();
}
bool pop(std::string& msg) {
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait(lock, [this] {
return !queue_.empty() || shutdown_;
});
if (queue_.empty()) return false;
msg = std::move(queue_.front());
queue_.pop();
return true;
}
void close() {
shutdown_ = true;
cv_.notify_all();
}
};Kullanımı
int main() {
MessageQueue mq;
// 2 producer
auto produce = [&](int id) {
for (int i = 0; i < 3; ++i) {
mq.push("P" + std::to_string(id) + "-" + std::to_string(i));
}
};
// 2 consumer
auto consume = [&](int id) {
std::string msg;
while (mq.pop(msg)) {
std::cout << "C" << id << " got: " << msg << "\n";
}
};
std::thread p1(produce, 1), p2(produce, 2);
std::thread c1(consume, 1), c2(consume, 2);
p1.join(); p2.join();
mq.close();
c1.join(); c2.join();
}Bu pattern'de close() çağrısı tüm consumer'ları uyandırır. Consumer pop() false döndüğünde döngüden çıkar. Bu şekilde temiz bir shutdown sağlanır.
12. std::scoped_lock (C++17) — Deadlock-Free Çoklu Kilitleme
Deadlock Problemi
İki thread, iki mutex'i farklı sırada kilitlemeye çalışırsa deadlock oluşur. Thread A önce mtx1 sonra mtx2 kilitler, Thread B tam tersi — ikisi de birbirini sonsuza kadar bekler.
Analoji: İki kişi dar bir koridorda karşılaşıyor. Biri sağa, diğeri sola çekilirse geçerler. Ama ikisi de aynı tarafa çekilirse — deadlock.
Çözüm: std::scoped_lock
std::scoped_lock (C++17) birden fazla mutex'i aynı anda, deadlock olmadan kilitler. İç implementasyonda std::lock algoritmasını kullanır.
#include <mutex>
#include <thread>
#include <iostream>
std::mutex mtx1, mtx2;
int account1 = 1000, account2 = 1000;
void transfer(int& from, int& to, int amount,
std::mutex& m1, std::mutex& m2) {
std::scoped_lock lock(m1, m2); // deadlock-free!
from -= amount;
to += amount;
}
int main() {
std::thread t1(transfer, std::ref(account1), std::ref(account2),
100, std::ref(mtx1), std::ref(mtx2));
std::thread t2(transfer, std::ref(account2), std::ref(account1),
50, std::ref(mtx2), std::ref(mtx1));
t1.join(); t2.join();
std::cout << account1 << " " << account2 << "\n"; // 950 1050
}scoped_lock scope bittiğinde otomatik unlock yapar. Tek mutex için de lock_guard yerine kullanılabilir — daha modern ve tutarlı.
// C++11 yolu — iki adım
std::lock(mtx1, mtx2);
std::lock_guard<std::mutex> lk1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lk2(mtx2, std::adopt_lock);
// C++17 yolu — tek satır
std::scoped_lock lock(mtx1, mtx2);13. std::shared_mutex ve std::shared_lock — Reader-Writer Lock
Problem
Bir veri yapısını birçok thread okuyor ama nadiren yazıyor. Normal mutex kullanırsan okuyucular bile birbirini bekler — gereksiz performans kaybı.
Analoji: Bir kütüphane düşün. Birden fazla kişi aynı anda aynı kitabı okuyabilir. Ama biri kitaba not yazacaksa, herkesin bitmesini beklemesi ve tek başına kalması gerekir.
std::shared_mutex (C++17)
#include <shared_mutex>
#include <thread>
#include <iostream>
#include <map>
#include <string>
class ThreadSafeCache {
std::map<std::string, int> cache_;
mutable std::shared_mutex mtx_;
public:
int read(const std::string& key) const {
std::shared_lock lock(mtx_); // birden fazla reader aynı anda
auto it = cache_.find(key);
return (it != cache_.end()) ? it->second : -1;
}
void write(const std::string& key, int value) {
std::unique_lock lock(mtx_); // exclusive — tek writer
cache_[key] = value;
}
};std::shared_lock okuma kilidi alır — birden fazla thread aynı anda shared lock alabilir. std::unique_lock yazma kilidi alır — tüm reader ve writer'ların bitmesini bekler.
int main() {
ThreadSafeCache cache;
cache.write("score", 100);
std::vector<std::thread> readers;
for (int i = 0; i < 5; ++i) {
readers.emplace_back([&] {
std::cout << cache.read("score") << "\n";
});
}
for (auto& t : readers) t.join();
}Okuma ağırlıklı (read-heavy) senaryolarda shared_mutex normal mutex'e göre çok daha iyi performans verir. Ama yazma ağırlıklı senaryolarda fark minimumdur, hatta overhead nedeniyle daha yavaş olabilir.
14. std::jthread (C++20) — Otomatik Join ve Cooperative Cancellation
Problem
std::thread destructor çağrıldığında eğer join veya detach yapılmadıysa std::terminate çağrılır — program çöker. Bu, özellikle exception durumlarında tehlikelidir.
std::jthread Çözümü
std::jthread (joining thread) destructor'da otomatik join() yapar. Ayrıca cooperative cancellation mekanizması sunar.
Analoji: std::thread bir çalışan gibi — işten çıkarmadan önce "ayrıl" demen lazım, yoksa sorun çıkar. std::jthread ise sözleşmeli çalışan — sözleşme bittiğinde otomatik ayrılır, ayrıca "lütfen bitir" diyebilirsin.
#include <thread>
#include <iostream>
#include <chrono>
void worker(std::stop_token stoken) {
while (!stoken.stop_requested()) {
std::cout << "Working...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
std::cout << "Stopped gracefully.\n";
}
int main() {
std::jthread t(worker); // stop_token otomatik geçilir
std::this_thread::sleep_for(std::chrono::seconds(1));
t.request_stop(); // nazikçe dur de
// destructor'da otomatik join — açıkça join() gerekmez
}Stop Callback
Durdurma isteğinde otomatik çalışacak bir callback de tanımlayabilirsin:
void worker(std::stop_token stoken) {
std::stop_callback cb(stoken, [] {
std::cout << "Cleanup triggered!\n";
});
while (!stoken.stop_requested()) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}request_stop() çağrıldığında callback hemen tetiklenir. Bu, kaynak temizliği veya loglama için çok kullanışlıdır.
💡 İpucu: C++20 kullanabiliyorsan std::thread yerine her zaman std::jthread tercih et. Otomatik join sayesinde resource leak riski sıfıra iner.
15. std::latch ve std::barrier (C++20)
std::latch — Tek Kullanımlık Bariyer
std::latch bir sayaç ile başlatılır. Thread'ler count_down() çağırır. Sayaç sıfıra düştüğünde bekleyen thread'ler uyandırılır. Tek kullanımlıktır — sıfıra indikten sonra tekrar kullanılamaz.
Analoji: Bir yarış başlangıcı. Koşucular (thread'ler) hazır olduklarında "hazırım" der (count_down). Herkes hazır olunca hakem başlatır (wait geçer).
#include <latch>
#include <thread>
#include <iostream>
#include <vector>
int main() {
constexpr int num_threads = 4;
std::latch start_signal(1); // ana thread tetikler
std::latch ready_signal(num_threads); // worker'lar hazır olduğunda
std::vector<std::jthread> threads;
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back([&, i] {
std::cout << "Thread " << i << " ready\n";
ready_signal.count_down(); // hazırım
start_signal.wait(); // başlama sinyalini bekle
std::cout << "Thread " << i << " running\n";
});
}
ready_signal.wait(); // tüm thread'ler hazır olana kadar bekle
std::cout << "All ready — GO!\n";
start_signal.count_down(); // başlat
}std::barrier — Tekrar Kullanılabilir Senkronizasyon Noktası
std::barrier, latch'e benzer ama her "faz" tamamlandığında sayaç sıfırlanır ve tekrar kullanılabilir. Opsiyonel olarak her fazın sonunda bir completion fonksiyonu çalıştırılabilir.
#include <barrier>
#include <thread>
#include <iostream>
#include <vector>
int main() {
constexpr int num_threads = 3;
int phase = 0;
std::barrier sync_point(num_threads, [&]() noexcept {
++phase;
std::cout << "=== Phase " << phase << " complete ===\n";
});
auto work = [&](int id) {
for (int i = 0; i < 3; ++i) {
std::cout << "T" << id << " phase work\n";
sync_point.arrive_and_wait();
}
};
std::vector<std::jthread> threads;
for (int i = 0; i < num_threads; ++i)
threads.emplace_back(work, i);
}Bu örnekte 3 thread, 3 faz boyunca çalışır. Her fazda tüm thread'ler arrive_and_wait() çağırır. Son thread geldiğinde completion fonksiyonu çalışır ve yeni faz başlar.
Latch vs Barrier
| Özellik | std::latch | std::barrier |
|---|---|---|
| Tekrar kullanılabilir | ❌ Hayır | ✅ Evet |
| Completion fonksiyonu | ❌ Yok | ✅ Var |
| Kullanım | Tek seferlik senkronizasyon | Tekrarlı fazlı hesaplama |
16. std::counting_semaphore ve std::binary_semaphore (C++20)
Semaphore (semafor), belirli bir kaynağa aynı anda kaç thread'in erişebileceğini kontrol eden senkronizasyon aracıdır. Trafik ışığı analojisi mükemmel çalışır: yeşil yanan ışık sayısı kadar araç geçebilir, diğerleri bekler.
C++20 ile <semaphore> header'ında iki tür semaphore geldi:
| Tür | Açıklama |
|---|---|
std::counting_semaphore<N> | Sayacı N'e kadar çıkabilen genel semaphore |
std::binary_semaphore | counting_semaphore<1>'in kısaltması — mutex benzeri |
counting_semaphore Temel Kullanım
#include <semaphore>
#include <thread>
#include <iostream>
#include <vector>
// Aynı anda en fazla 3 thread çalışabilir
std::counting_semaphore<3> sem(3);
void worker(int id) {
sem.acquire(); // Sayacı 1 azalt — 0'sa bekle
std::cout << "Thread " << id << " calisiyor...\n";
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::cout << "Thread " << id << " bitti.\n";
sem.release(); // Sayacı 1 artır — bekleyen thread'i uyandır
}
int main() {
std::vector<std::jthread> threads;
for (int i = 0; i < 8; ++i) {
threads.emplace_back(worker, i);
}
// 8 thread var ama aynı anda sadece 3'ü çalışır
}🎯 Analoji — Restoran Masası: 3 masalı bir restoran düşün. 8 müşteri geldi ama aynı anda sadece 3'ü oturabilir. Biri kalktığında (release), sıradaki oturur (acquire).
binary_semaphore — Sinyal Mekanizması
binary_semaphore mutex'e benzer ama farklı bir amaca hizmet eder: thread'ler arası sinyal gönderme. Mutex'ten farkı, bir thread lock'lar diğer thread unlock'lar — mutex'te bunu yapamazsın.
#include <semaphore>
#include <thread>
#include <iostream>
std::binary_semaphore ready(0); // Başlangıçta "hazır değil"
std::binary_semaphore done(0);
std::string data;
void producer() {
data = "Onemli veri";
std::cout << "Producer: veri hazir\n";
ready.release(); // Consumer'a "hazır" sinyali gönder
done.acquire(); // Consumer'ın bitirmesini bekle
std::cout << "Producer: consumer tamamladi\n";
}
void consumer() {
ready.acquire(); // Producer'dan sinyal bekle
std::cout << "Consumer: veri alindi -> " << data << "\n";
done.release(); // Producer'a "bitti" sinyali gönder
}
int main() {
std::jthread t1(producer);
std::jthread t2(consumer);
}Semaphore vs Mutex vs condition_variable
| Özellik | mutex | condition_variable | semaphore |
|---|---|---|---|
| Lock eden unlock etmeli | ✅ Evet | — | ❌ Hayır (farklı thread) |
| Sayaç | 1 (binary) | — | N'e kadar |
| Sinyal gönderme | ❌ | ✅ (ama mutex gerekli) | ✅ (tek başına yeter) |
| Kullanım kolaylığı | Orta | Zor | Kolay |
| Tipik kullanım | Kritik bölge koruma | Bekleme/bildirim | Kaynak havuzu, sinyal |
try_acquire ve Zamanlı Bekleme
#include <semaphore>
#include <iostream>
#include <chrono>
std::counting_semaphore<2> pool(2);
void try_access(int id) {
// Bloklamadan deneme
if (pool.try_acquire()) {
std::cout << "Thread " << id << ": eristim (try_acquire)\n";
pool.release();
} else {
std::cout << "Thread " << id << ": mesgul, bekliyorum...\n";
// Zamanlı bekleme
if (pool.try_acquire_for(std::chrono::milliseconds(100))) {
std::cout << "Thread " << id << ": bekledikten sonra eristim\n";
pool.release();
} else {
std::cout << "Thread " << id << ": zaman asimi!\n";
}
}
}Pratik: Bağlantı Havuzu (Connection Pool)
Semaphore'un en klasik gerçek dünya kullanımı — veritabanı bağlantı havuzu:
#include <semaphore>
#include <mutex>
#include <queue>
#include <thread>
#include <iostream>
#include <vector>
#include <string>
class ConnectionPool {
std::counting_semaphore<10> sem_; // Max bağlantı sayısı
std::mutex mtx_;
std::queue<std::string> connections_;
public:
ConnectionPool(int size) : sem_(size) {
for (int i = 0; i < size; ++i) {
connections_.push("conn_" + std::to_string(i));
}
}
std::string acquire() {
sem_.acquire(); // Boş bağlantı yoksa bekle
std::lock_guard<std::mutex> lock(mtx_);
auto conn = connections_.front();
connections_.pop();
return conn;
}
void release(const std::string& conn) {
{
std::lock_guard<std::mutex> lock(mtx_);
connections_.push(conn);
}
sem_.release(); // Bekleyenleri uyandır
}
};
int main() {
ConnectionPool pool(3); // 3 bağlantılı havuz
auto task = [&](int id) {
auto conn = pool.acquire();
std::cout << "Thread " << id << " kulaniyor: " << conn << "\n";
std::this_thread::sleep_for(std::chrono::milliseconds(200));
pool.release(conn);
std::cout << "Thread " << id << " birakti: " << conn << "\n";
};
std::vector<std::jthread> threads;
for (int i = 0; i < 8; ++i) {
threads.emplace_back(task, i);
}
}⚠️ Dikkat: Semaphore sayacı negatife düşemez.
acquire()sayaç 0'sa bloklar. Eğerrelease()ile sayacı maksimum değerin üzerine çıkarmaya çalışırsan undefined behavior oluşur — heracquireiçin tam birreleaseolduğundan emin ol.
17. Pratik: Thread-Safe Bounded Buffer
Şimdi tüm öğrendiklerimizi birleştiren bir proje yapalım: kapasitesi sınırlı (bounded), thread-safe bir buffer sınıfı. Producer buffer doluysa bekleyecek, consumer buffer boşsa bekleyecek.
Bu sınıf aslında bir blocking queue implementasyonudur ve gerçek dünyada mesaj kuyruğu, görev kuyruğu gibi pek çok yerde kullanılır.
#include <queue>
#include <mutex>
#include <condition_variable>
#include <optional>
#include <stdexcept>
template<typename T>
class BoundedBuffer {
std::queue<T> buffer_;
size_t capacity_;
std::mutex mtx_;
std::condition_variable not_full_;
std::condition_variable not_empty_;
bool closed_ = false;
public:
explicit BoundedBuffer(size_t cap) : capacity_(cap) {
if (cap == 0) throw std::invalid_argument("capacity must be > 0");
}
// Kuyruğa eleman ekle — doluysa bekle
bool push(T item) {
std::unique_lock<std::mutex> lock(mtx_);
not_full_.wait(lock, [this] {
return buffer_.size() < capacity_ || closed_;
});
if (closed_) return false;
buffer_.push(std::move(item));
not_empty_.notify_one();
return true;
}
// Kuyruktan eleman al — boşsa bekle
std::optional<T> pop() {
std::unique_lock<std::mutex> lock(mtx_);
not_empty_.wait(lock, [this] {
return !buffer_.empty() || closed_;
});
if (buffer_.empty()) return std::nullopt;
T item = std::move(buffer_.front());
buffer_.pop();
not_full_.notify_one();
return item;
}
void close() {
std::lock_guard<std::mutex> lock(mtx_);
closed_ = true;
not_full_.notify_all();
not_empty_.notify_all();
}
size_t size() const {
std::lock_guard<std::mutex> lock(mtx_);
return buffer_.size();
}
};Kullanım Örneği
#include <thread>
#include <iostream>
#include <vector>
int main() {
BoundedBuffer<int> buf(5); // max 5 eleman
// 2 producer — her biri 10 eleman üretir
auto produce = [&](int id) {
for (int i = 0; i < 10; ++i) {
int val = id * 100 + i;
if (!buf.push(val)) break;
std::cout << "P" << id << " pushed " << val << "\n";
}
};
// 2 consumer
auto consume = [&](int id) {
while (auto val = buf.pop()) {
std::cout << "C" << id << " popped " << *val << "\n";
}
};
std::jthread p1(produce, 1), p2(produce, 2);
std::jthread c1(consume, 1), c2(consume, 2);
p1.join(); p2.join();
buf.close();
// c1, c2 jthread olduğu için otomatik join
}Tasarım Kararları
Bu implementasyonda dikkat edilmesi gereken noktalar:
İki condition_variable kullanıyoruz:
not_full_(producer bekler) venot_empty_(consumer bekler). Tek condition_variable ile de yapılabilir ama iki tane kullanmak daha verimli — gereksiz uyanmaları azaltır.`std::optional` dönüş tipi sayesinde "kuyruk kapalı ve boş" durumunu
nulloptile temiz bir şekilde ifade ediyoruz.`close()` metodu
notify_allyapıyor. Böylece bekleyen tüm producer ve consumer'lar uyandırılır ve temiz bir şekilde kapanır.Move semantics kullanıyoruz (
std::move). Bu, büyük objeler için kopyalama maliyetini ortadan kaldırır.
⚠️ Dikkat: size() metodunda mutable mutex kullanılması gerekir çünkü const metod içinden lock alıyoruz. Bunu mutable std::mutex mtx_; olarak tanımlamayı unutma — yukarıdaki örnekte bilerek basit tuttuk.
Özet
Bu derste C++11'den C++20'ye kadar ileri threading araçlarını öğrendik. İşte anahtar noktalar:
`std::condition_variable` thread'ler arasında "hazır" sinyali göndermek için kullanılır. Her zaman bir mutex ile birlikte ve predicate (koşul) ile kullanılmalıdır — spurious wakeup'a karşı savunma bu şekilde sağlanır.
`std::atomic` basit veri tipleri (int, bool, pointer) üzerinde lock-free thread-safe işlemler sağlar. Sayaçlar ve flag'ler için mutex'ten çok daha performanslıdır.
memory_orderile ince ayar yapılabilir ama varsayılanseq_cstçoğu durumda yeterlidir.`std::async/future/promise/packaged_task` asenkron programlamanın yapı taşlarıdır.
asyncen kolay yol,promiseen esnek,packaged_taskise thread pool gibi yapılarda görev paketleme için idealdir.Thread Pool sabit sayıda thread ile görev kuyruğu yönetir.
condition_variable+mutex+queueüçlüsü ile inşa edilir. Her görev için thread oluşturma maliyetini ortadan kaldırır.`std::scoped_lock` (C++17) birden fazla mutex'i deadlock-free kilitler. `std::shared_mutex` reader-writer senaryoları için birden fazla eşzamanlı okuyucu destekler.
`std::jthread` (C++20) otomatik join ve cooperative cancellation sağlar. `std::latch` tek seferlik, `std::barrier` tekrarlı senkronizasyon noktaları oluşturur.
`std::counting_semaphore` belirli sayıda thread'in aynı anda kaynağa erişimini kontrol eder. `std::binary_semaphore` thread'ler arası sinyal mekanizması olarak mutex'ten daha esnek çalışır (farklı thread lock/unlock yapabilir). Bağlantı havuzu, rate limiting gibi senaryolarda idealdir.
AI Asistan
Sorularını yanıtlamaya hazır