Multithreading Temelleri
Modern bilgisayarlarda birden fazla işlemci çekirdeği var. Ama senin programın tek bir thread'de çalışıyorsa, bu çekirdeklerden sadece birini kullanıyor demektir. Multithreading, programının birden fazla işi aynı anda yapmasını sağlar.
Multithreading'i bir restoran mutfağı gibi düşün. Tek aşçı varsa yemekler sırayla pişer. Ama beş aşçı varsa beş yemek aynı anda hazırlanabilir. Tabii aşçılar aynı tencereye aynı anda karıştırırsa kaos çıkar — işte bu da "race condition" denen şey. Multithreading güçlüdür ama disiplin gerektirir.
std::thread — Thread Oluşturma
C++11 ile birlikte gelen <thread> başlık dosyası, platform bağımsız thread oluşturmayı sağlar.
#include <iostream>
#include <thread>
void selamVer(const std::string& isim) {
std::cout << "Merhaba, " << isim << "! (Thread ID: "
<< std::this_thread::get_id() << ")\n";
}
int main() {
std::thread t1(selamVer, "Ali");
std::thread t2(selamVer, "Veli");
t1.join(); // t1 bitene kadar bekle
t2.join(); // t2 bitene kadar bekle
std::cout << "Her iki thread de bitti.\n";
return 0;
}std::thread constructor'ına bir fonksiyon ve argümanlarını verirsin. Thread hemen çalışmaya başlar. join() çağırmak, o thread bitene kadar mevcut thread'i bekletir.
join() vs detach()
Her thread için iki seçeneğin var:
`join()`: Thread'in bitmesini bekle. Sonucu veya tamamlanmasını önemsiyorsan kullan.
`detach()`: Thread'i bağımsız çalışmaya bırak. "Arka plan görevi" gibi — onunla bağın kopuyor.
#include <iostream>
#include <thread>
#include <chrono>
void arkaplanGorev() {
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "Arka plan gorevi tamamlandi.\n";
}
int main() {
std::thread t(arkaplanGorev);
t.detach(); // thread serbest birakildi
std::cout << "Main devam ediyor...\n";
// DIKKAT: main biterse detach'li thread de olur
std::this_thread::sleep_for(std::chrono::seconds(3));
return 0;
}⚠️ Thread nesnesini join/detach etmeden yok etme!
std::threaddestructor'ı, nejoinne dedetachedilmemiş bir thread içinstd::terminate()çağırır. Program çöker. Her thread yajoinya dadetachedilmelidir.
Lambda ile Thread
Thread'lere fonksiyon yerine lambda da verebilirsin — genellikle daha pratiktir:
#include <iostream>
#include <thread>
#include <vector>
int main() {
std::vector<std::thread> threadler;
for (int i = 0; i < 5; i++) {
threadler.emplace_back([i]() {
std::cout << "Thread " << i << " calisiyor\n";
});
}
for (auto& t : threadler) {
t.join();
}
return 0;
}Race Condition — Yarış Durumu
İki thread aynı veriye aynı anda erişip değiştirmeye çalışırsa ne olur? Kaos. Buna "race condition" denir.
#include <iostream>
#include <thread>
int sayac = 0;
void artir() {
for (int i = 0; i < 100000; i++) {
sayac++; // RACE CONDITION!
}
}
int main() {
std::thread t1(artir);
std::thread t2(artir);
t1.join();
t2.join();
// Beklenen: 200000
// Gercek: Her seferinde farkli, 200000'den kucuk
std::cout << "Sayac: " << sayac << "\n";
return 0;
}sayac++ tek bir işlem gibi görünse de aslında üç adımdır: oku → artır → yaz. İki thread bu adımları aynı anda yapınca birbirlerinin değerlerini ezer. Sonuç: 200000 yerine 150000, 180000 gibi rastgele sayılar.
Bu problemi çözmek için "senkronizasyon" mekanizmaları gerekir. En temeli mutex'tir.
std::mutex ve lock_guard
Mutex (mutual exclusion), bir anda sadece bir thread'in kritik bölgeye erişmesini sağlar. Tuvalet kapısındaki kilit gibi — biri içerideyse diğerleri bekler.
#include <iostream>
#include <thread>
#include <mutex>
int sayac = 0;
std::mutex mtx;
void guvenliArtir() {
for (int i = 0; i < 100000; i++) {
mtx.lock();
sayac++;
mtx.unlock();
}
}
int main() {
std::thread t1(guvenliArtir);
std::thread t2(guvenliArtir);
t1.join();
t2.join();
std::cout << "Sayac: " << sayac << "\n"; // Her zaman 200000
return 0;
}Artık sonuç her zaman 200000. Ama lock() / unlock() kullanmak tehlikelidir — unlock'u unutursan veya exception fırlatılırsa, diğer thread'ler sonsuza kadar bekler (deadlock).
lock_guard — RAII ile Kilitleme
std::lock_guard, mutex'i RAII prensibiyle yönetir. Constructor'da kilitler, destructor'da açar. Scope'tan çıkınca otomatik unlock.
#include <iostream>
#include <thread>
#include <mutex>
int sayac = 0;
std::mutex mtx;
void guvenliArtir() {
for (int i = 0; i < 100000; i++) {
std::lock_guard<std::mutex> kilit(mtx);
sayac++;
// scope sonunda kilit otomatik acilir
}
}
int main() {
std::thread t1(guvenliArtir);
std::thread t2(guvenliArtir);
t1.join();
t2.join();
std::cout << "Sayac: " << sayac << "\n"; // 200000
return 0;
}💡 Her zaman `lock_guard` veya `unique_lock` kullan. Manuel
lock()/unlock()çağrısı yapma. Exception safety için RAII şart.
std::unique_lock — Esnek Kilitleme
lock_guard basit ve verimlidir ama esnekliği sınırlıdır. std::unique_lock daha fazla kontrol sunar:
Kilitlemeyi erteleyebilirsin
Kilitleyip açıp tekrar kilitleyebilirsin
Condition variable ile kullanabilirsin
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void islem() {
// Olusturulunca kilitlenmez
std::unique_lock<std::mutex> kilit(mtx, std::defer_lock);
// Bazi hazirlik islemleri (kilit gerekmez)
std::cout << "Hazirlik yapiliyor...\n";
// Simdi kilitle
kilit.lock();
std::cout << "Kritik bolge (kilitli)\n";
// Manuel unlock
kilit.unlock();
std::cout << "Kilit acildi, baska isler yapiliyor\n";
// Tekrar kilitle
kilit.lock();
std::cout << "Tekrar kilitli\n";
// scope sonunda otomatik acilir
}
int main() {
std::thread t1(islem);
std::thread t2(islem);
t1.join();
t2.join();
return 0;
}lock_guard vs unique_lock:
lock_guard: Basit, hafif, sadece scope-based kilitleme. Genellikle bu yeterli.unique_lock: Esnek, condition variable'lar ile kullanılır, ertelenmiş kilitleme desteği var.
std::async ve std::future
Thread oluşturup join etmek iyi ama bazen daha yüksek seviyeli bir araç istersin. std::async, bir fonksiyonu asenkron olarak çalıştırır ve sonucunu std::future ile alırsın.
#include <iostream>
#include <future>
#include <chrono>
int uzunHesaplama(int n) {
std::this_thread::sleep_for(std::chrono::seconds(2));
int toplam = 0;
for (int i = 1; i <= n; i++) {
toplam += i;
}
return toplam;
}
int main() {
// Asenkron baslat
auto gelecek = std::async(std::launch::async, uzunHesaplama, 1000);
std::cout << "Hesaplama arkaplanda calisiyor...\n";
std::cout << "Bu arada baska isler yapabilirsin.\n";
// Sonucu al (hazir degilse bekle)
int sonuc = gelecek.get();
std::cout << "Sonuc: " << sonuc << "\n";
return 0;
}std::async ile thread yönetimi derleyiciye/kütüphaneye bırakılır. future.get() çağrıldığında:
Sonuç hazırsa hemen döner
Hazır değilse bekler
Exception fırlatılmışsa burada yeniden fırlatılır
Launch Policy
std::async'in iki başlatma politikası var:
`std::launch::async`: Yeni thread'de çalıştır (garanti)
`std::launch::deferred`:
get()çağrılana kadar çalıştırma (tembel değerlendirme)
#include <iostream>
#include <future>
int hesapla() {
std::cout << "Hesaplama basladi\n";
return 42;
}
int main() {
// Hemen baslar
auto f1 = std::async(std::launch::async, hesapla);
// get() cagirilinca baslar
auto f2 = std::async(std::launch::deferred, hesapla);
std::cout << "Henuz get cagirilmadi\n";
std::cout << "f1: " << f1.get() << "\n";
std::cout << "f2: " << f2.get() << "\n"; // simdi baslar ve biter
return 0;
}💡 `std::async` basit asenkron işler için idealdir. Thread oluşturma, join etme, exception taşıma gibi detayları senin için halleder. Karmaşık thread havuzu (thread pool) gereken durumlar için farklı kütüphaneler gerekir.
Basit Producer-Consumer Örneği
Producer-consumer, multithreading'in klasik bir kalıbıdır. Bir thread veri üretir (producer), başka bir thread bu veriyi tüketir (consumer). Aralarında paylaşımlı bir kuyruk (queue) bulunur.
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::queue<int> kuyruk;
std::mutex mtx;
std::condition_variable cv;
bool bitti = false;
void uretici() {
for (int i = 1; i <= 10; i++) {
{
std::lock_guard<std::mutex> kilit(mtx);
kuyruk.push(i);
std::cout << "Uretildi: " << i << "\n";
}
cv.notify_one(); // tuketiciyi uyandır
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
{
std::lock_guard<std::mutex> kilit(mtx);
bitti = true;
}
cv.notify_one();
}
void tuketici() {
while (true) {
std::unique_lock<std::mutex> kilit(mtx);
cv.wait(kilit, []{ return !kuyruk.empty() || bitti; });
while (!kuyruk.empty()) {
int deger = kuyruk.front();
kuyruk.pop();
std::cout << "Tuketildi: " << deger << "\n";
}
if (bitti && kuyruk.empty()) break;
}
}
int main() {
std::thread t1(uretici);
std::thread t2(tuketici);
t1.join();
t2.join();
std::cout << "Tamamlandi.\n";
return 0;
}Bu örnekte birkaç önemli kavram bir arada:
mutex: Kuyruğa güvenli erişim
condition_variable: Tüketici, yeni veri gelene kadar bekler (busy-wait yapmadan)
unique_lock: condition_variable ile kullanım zorunlu (lock_guard olmaz)
cv.wait(kilit, koşul) şöyle çalışır: koşul false ise kilidi açar ve uyur. Uyandırılınca kilidi tekrar alır ve koşulu kontrol eder. Koşul true ise devam eder.
Thread Safety Temel Kuralları
Multithreading'de hata yapmak kolaydır ve hatalar genellikle aralıklı (intermittent) olur — bazen çalışır, bazen çalışmaz. Bu yüzden kuralları bilmek kritiktir.
1. Paylaşılan Veriye Senkronize Erişim
Birden fazla thread aynı veriye erişiyorsa ve en az biri yazıyorsa, mutlaka senkronizasyon kullan (mutex, atomic, vs.).
#include <atomic>
#include <thread>
#include <iostream>
std::atomic<int> sayac{0}; // atomic — mutex'e gerek yok
void artir() {
for (int i = 0; i < 100000; i++) {
sayac++; // atomik islem, thread-safe
}
}
int main() {
std::thread t1(artir);
std::thread t2(artir);
t1.join();
t2.join();
std::cout << "Sayac: " << sayac << "\n"; // 200000
return 0;
}std::atomic, basit türler (int, bool, pointer) için mutex'ten daha verimli bir alternatiftir.
2. Kilit Sırasını Tutarlı Tut
Birden fazla mutex kilitlemen gerekiyorsa, her zaman aynı sırada kilitle. Aksi halde deadlock oluşur.
#include <mutex>
std::mutex mtxA, mtxB;
// HATALI — deadlock riski
void thread1() {
std::lock_guard<std::mutex> kilitA(mtxA);
std::lock_guard<std::mutex> kilitB(mtxB);
}
void thread2() {
std::lock_guard<std::mutex> kilitB(mtxB); // ters sira!
std::lock_guard<std::mutex> kilitA(mtxA);
}
// DOGRU — std::scoped_lock (C++17)
void guvenli1() {
std::scoped_lock kilit(mtxA, mtxB); // ikisini birden kilitler
// ...
}std::scoped_lock (C++17), birden fazla mutex'i deadlock olmadan aynı anda kilitler.
3. Kilitleri Kısa Tut
Mutex'i sadece gerçekten gerekli olan kod bölümünde kilitli tut. Uzun süren işlemleri kilit dışında yap.
4. Thread'ler Arası Veri Paylaşımını Minimize Et
En güvenli thread, başka thread'lerle hiçbir veri paylaşmayan thread'dir. Mümkünse her thread kendi verisini kullansın.
5. const Nesneler Doğal Thread-Safe
Eğer bir nesne oluşturulduktan sonra hiç değiştirilmiyorsa (immutable), mutex'e gerek yok. Birden fazla thread güvenle okuyabilir.
⚠️ cout thread-safe değildir. Birden fazla thread aynı anda
std::cout'a yazarsa çıktılar karışır. Üretim kodunda her thread'in kendi log buffer'ını kullanması veyacouterişiminin mutex ile korunması gerekir.
Özet
std::thread ile yeni thread oluşturulur. Her thread ya
join()(bekle) ya dadetach()(serbest bırak) edilmelidir, yoksa program çöker.Race condition, birden fazla thread'in aynı veriye senkronizasyonsuz erişmesidir. Mutex veya atomic ile çözülür.
std::mutex ile
lock_guard(basit) veyaunique_lock(esnek) kullanarak kritik bölgeleri koru. Manuellock()/unlock()yapma.std::async ve std::future, yüksek seviyeli asenkron programlama araçlarıdır. Thread yönetimini senin yerine halleder ve sonucu
future.get()ile alırsın.Producer-consumer kalıbında
condition_variablekullanarak tüketici thread'i gereksiz yere bekletmezsin (busy-wait yerine uyut-uyandır).Thread safety kuralları: paylaşılan veriye senkronize eriş, kilit sırasını tutarlı tut, kilitleri kısa tut, veri paylaşımını minimize et.
AI Asistan
Sorularını yanıtlamaya hazır