---
title: 'İş Parçacıkları ve Kilitler'
source: 'https://academia.sh/tr/kurslar/cpp-sablonlar/is-parcaciklari-ve-kilitler'
course: 'Şablonlar ve Başarım'
language: tr
updated: '2026-10-21T05:02:46+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'std::thread ve std::jthread ile iş parçacığı açar, std::mutex ve std::lock_guard ile paylaşılan bir değişkeni veri yarışından korur.'
    - 'std::scoped_lock ile birden çok kilidi kilitlenmeden alır.'
    - 'std::unique_lock ve std::condition_variable ile bir üretici/tüketici kuyruğu kurar.'
    - 'Bir kilidin bedelini, kaç kez alındığını sayarak ölçer ve düşürür.'
---

# İş Parçacıkları ve Kilitler

std::thread ve join, std::jthread, std::mutex ile std::lock_guard, std::scoped_lock, std::unique_lock ile std::condition_variable, üretici/tüketici kuyruğu ve kilidin kaç eşgüdüm noktası eklediğinin sayılması.

Önceki ders, tip kümesi derlemede kapalıysa çok biçimliliğin bedelinin derlemeye
taşındığını gösterdi: işlev çağrısı koşarken hiçbir şey ödemez, ama somutlaştırılan her
tip için ayrı bir gövde üretilir. Bu derste ödemenin başka bir yere gittiği durumu
ölçüyoruz: iş birden çok iş parçacığına bölününce ortak veriyi korumak için eklenen
kilitler. Kursun ölçtüğü soru burada da aynı kalıyor: bir düzenek bedeli nereye taşıyor?
Cevabın sınıfı **bekleme**dir. Bu ölçü gerçek beklemeyi değil, bir iş parçacığının
**beklemek zorunda kalabileceği nokta** sayısını sayar; süre basılmaz, çünkü süre
makineye ve o anki yüke bağlıdır, nokta sayısı ise koddan okunur.

Bir ayrımı baştan söyleyelim. M10/K03'te "kaç kez koştu" sorusunda sayı ölçünün kendisiydi.
Burada sayı yalnızca kanıt aracıdır: ders "kilit kaç kez alındı" sayısına değil, o
sayının gösterdiği sınıfa, yani **bekleme**ye cevap verir. Ayrıca M08/K04'ün sorusu olan
"adımı hangi iş parçacığı koşturuyor" bu derste sorulmaz; hangi iş parçacığının ne
yaptığı değil, kaç eşgüdüm noktası olduğu ölçülür.

## İş Parçacığı Açmak ve Beklemek

`std::thread`, kurulduğu anda verilen işlevi ayrı bir iş parçacığında koşturmaya başlar.
Nesne yıkılmadan önce ya `join()` ile beklenmeli ya da `detach()` ile bırakılmalıdır;
ikisinden hiçbiri yapılmadan yıkılan nesne programı sonlandırır. Bu yüzden her örnekte
`join()` çağrılır ve sonuç yalnızca `join()` döndükten sonra okunur: `join()` iş
parçacığının yazdığı her şeyin çağırana görünür olduğunu garanti eder.

Ortak veriye hiçbir koruma olmadan iki iş parçacığı yazarsa, programın davranışı
**tanımsız**dır: buna veri yarışı denir. Dört iş parçacığının kilitsiz bir sayacı onar
bin kez artırması, sonucun kırk bin olacağını garanti etmez; hiçbir sonucu garanti etmez.
Tanımsız davranışı koşturmuyoruz; yalnızca tanımlı karşılığını koşturup düzyazıda
neden gerektiğini anlatıyoruz.

## jthread: Kendiliğinden Katılan İş Parçacığı

`std::jthread` C++20'yle gelir ve `std::thread`'in en sık yapılan hatasını kaldırır.
Yıkıcısı, iş parçacığı henüz katılmamışsa önce bir durdurma isteği gönderir, sonra
`join()` çağırır; araya bir `return` ya da bir
istisna girse bile iş parçacığı kapsamdan çıkmadan bitirilir. Katılmadan yıkılan bir
`std::thread` ise programı sonlandırır. Fark yazım kolaylığından çok **güvenliktir**:
unutulan bir `join()` artık bir çökme olmaz.

İkinci fark, `jthread`'in bir durdurma isteği taşımasıdır. İşlev bir `std::stop_token`
parametresi alırsa, dışarıdan `request_stop()` çağrıldığında döngüsünden çıkabilir.
İsteğin ne zaman görüleceği zamanlamaya bağlı olduğundan bunu koşturmuyoruz; yalnızca
kapsamın sonunda katılma davranışını kullanıyoruz. Ölçümün konusu iş parçacığının nasıl
açıldığı değil, açıldıktan sonra ortak veriyi nasıl koruduğumuzdur.

## Kilit ve lock_guard

Tanımlı karşılık, ortak veriyi bir dışlama kilidinin arkasına almaktır. `std::mutex`
kilitlendiğinde, kilidi bırakana kadar başka hiçbir iş parçacığı onu alamaz; alamayan
iş parçacığı bekler. `std::lock_guard` kilidi kurulduğunda alır, yıkıldığında bırakır;
bırakma işini kapsamın sonuna bağladığı için bir `return` ya da bir istisna kilidi
açık bırakamaz.

Aşağıdaki başlık dosyası ortak sayacı ve onu artırmanın iki yolunu tanımlar. `alma`,
kilidin kaç kez alındığını sayan ikinci alandır; ona yalnızca kilit tutulurken
dokunulur. İlk yol her artışta kilit alır. İkincisini birazdan açıklayacağız.

```cpp
// kilit_olcum.hpp — ortak sayac ve iki artirma bicimi
#pragma once
#include <mutex>

struct KilitliSayac {
    std::mutex kilit;
    long deger = 0;
    long alma = 0;
};

inline void artir_her_seferde(KilitliSayac& s, int kez) {
    for (int i = 0; i < kez; ++i) {
        std::lock_guard<std::mutex> koruma(s.kilit);
        ++s.deger;
        ++s.alma;
    }
}

inline void artir_yerel(KilitliSayac& s, int kez) {
    long yerel = 0;
    for (int i = 0; i < kez; ++i) ++yerel;
    std::lock_guard<std::mutex> koruma(s.kilit);
    s.deger += yerel;
    ++s.alma;
}
```

## Birden Çok Kilit ve scoped_lock

Tek kilit tek değişkeni korur. İki nesneyi birden değiştiren bir işlem, ikisinin de
kilidini almak zorundadır. İki iş parçacığı bu kilitleri ters sırayla alırsa, her biri
öbürünün tuttuğu kilidi bekler ve hiçbiri ilerleyemez: buna kilitlenme denir.
Kilitlenmenin ortak kökü, kilitlerin **tutarsız sırayla** alınmasıdır.

İki çıkış yolu vardır. Birincisi bütün kodda tek bir sıra belirlemektir; örneğin kilitleri
her zaman hesap numarasına göre küçükten büyüğe almak. Bu yol çalışır ama disiplin ister:
sıra yalnızca bir kuraldır ve yeni yazılan her işlev ona uymak zorundadır. İkincisi
`std::scoped_lock`'tur: birden çok kilidi tek seferde, kilitlenmeyi önleyen bir sırayla
alır; standart bunun için kilitlenmeden kaçınan bir algoritma kullanılmasını ister. Kilitleri
hangi sırayla yazdığınız artık önemli olmaz.

```cpp
// hesap.hpp — iki ve uc kilidi birden alan islemler
#pragma once
#include <mutex>

struct Hesap {
    std::mutex kilit;
    long bakiye = 0;
};

inline void aktar(Hesap& kaynak, Hesap& hedef, long miktar) {
    std::scoped_lock ikisi(kaynak.kilit, hedef.kilit);
    kaynak.bakiye -= miktar;
    hedef.bakiye += miktar;
}

inline long toplam_bakiye(Hesap& a, Hesap& b, Hesap& c) {
    std::scoped_lock uclu(a.kilit, b.kilit, c.kilit);
    return a.bakiye + b.bakiye + c.bakiye;
}
```

`aktar(a, b, 1)` ile `aktar(b, a, 1)` aynı anda çağrılsa bile kilitlenme olmaz. Elle
`a.kilit.lock()` ardından `b.kilit.lock()` yazılsaydı, iki çağrı ters sırada alırsa
kilitlenme mümkün olurdu; bunu koşturmuyoruz, çünkü sonucun programı asılı bırakması
mümkündür. `toplam_bakiye` üç kilidi birden alır: aktarmalar yalnızca iki kilit tutar,
bu yüzden toplamı okuyan, hiçbir aktarmanın ortasına denk gelmez.

## Koşul Değişkeni ve unique_lock

Bazen bir iş parçacığının ortak veriyi korumasının ötesinde, **bir şeyin olmasını
beklemesi** gerekir. `std::condition_variable` bunun içindir: bekleyen iş parçacığı
kilidi bırakıp uyur, başka bir iş parçacığı koşulu sağlayıp haber verince uyanır. Bekleyen
tarafın kilidi elinde tutup bırakabilmesi için `std::lock_guard` yetmez; bırakılıp yeniden
alınabilen `std::unique_lock` gerekir. `unique_lock`, `lock_guard`'ın yaptığı her şeyi
yapar ve üstüne `unlock()` ile `lock()` çağrılabilir; bedeli küçük bir ek durum bilgisidir.

```cpp
// teslim.hpp — kosul degiskeniyle bir kez uyanma
#pragma once
#include <condition_variable>
#include <mutex>

struct Teslim {
    std::mutex kilit;
    std::condition_variable uyandir;
    bool hazir = false;
    int deger = 0;
};

inline void hazirla(Teslim& t, int v) {
    {
        std::lock_guard<std::mutex> koruma(t.kilit);
        t.deger = v;
        t.hazir = true;
    }
    t.uyandir.notify_one();
}

inline int bekle(Teslim& t) {
    std::unique_lock<std::mutex> kilitli(t.kilit);
    t.uyandir.wait(kilitli, [&t] { return t.hazir; });
    return t.deger;
}
```

`wait` koşulu bir koşul ifadesiyle alır ve kilit elde iken denetler; uyanma haberi
boşuna gelmiş ya da gelmeden önce de koşul sağlanmış olabilir, bu yüzden koşul her
uyanışta yeniden sınanır. Bayrak ve değer yalnızca kilit altında yazılıp okunur.

## Üretici ve Tüketici

Tek bir değer yerine bir değer akışı taşındığında aynı düzenek bir kuyruğa dönüşür.
Üretici kuyruğa değer koyar ve haber verir; tüketici kuyruk doluyken değerleri alır,
boşken uyur. İş bittiğinde üretici bir `bitti` bayrağı kaldırır, çünkü tüketicinin
"kuyruk boş, ama daha değer gelecek mi" sorusuna başka cevabı yoktur.

Tüketici değeri kuyruktan aldıktan sonra kilidi **bırakır**, işi kilitsiz yapar ve kilidi
yeniden alır. Bu, `unique_lock`'un `lock_guard`'dan ayrıldığı yerdir: kilit yalnızca
ortak veriye dokunulan süre boyunca tutulur, değerin işlenmesi kuyruğu başkalarına
kapatmaz.

```cpp
// kuyruk.hpp — uretici/tuketici kuyrugu
#pragma once
#include <condition_variable>
#include <mutex>
#include <queue>

struct Kuyruk {
    std::mutex kilit;
    std::condition_variable uyandir;
    std::queue<int> ogeler;
    bool bitti = false;
};

inline void uret(Kuyruk& k, int adet) {
    for (int i = 0; i < adet; ++i) {
        {
            std::lock_guard<std::mutex> koruma(k.kilit);
            k.ogeler.push(i);
        }
        k.uyandir.notify_one();
    }
    {
        std::lock_guard<std::mutex> koruma(k.kilit);
        k.bitti = true;
    }
    k.uyandir.notify_one();
}

inline long tuket(Kuyruk& k, int& alinan) {
    long toplam = 0;
    std::unique_lock<std::mutex> kilitli(k.kilit);
    for (;;) {
        k.uyandir.wait(kilitli, [&k] { return !k.ogeler.empty() || k.bitti; });
        if (k.ogeler.empty()) break;
        int v = k.ogeler.front();
        k.ogeler.pop();
        kilitli.unlock();
        toplam += v;
        ++alinan;
        kilitli.lock();
    }
    return toplam;
}
```

Tüketicinin kaç kez uyanıp kaç kez beklediği çizelgeye bağlıdır ve basılmaz. Basılan,
çizelgeden bağımsız iki sayıdır: kaç değerin alındığı ve değerlerin toplamı.

## Ölçüm: Kilit Kaç Kez Alındı

Şimdi iki artırma biçimini dört iş parçacığıyla, her biri on bin artış yapacak biçimde
koşturuyoruz. Aynı programda `aktar`, `Teslim` ve `Kuyruk` da çalışır; her sonuç yalnızca
`join()` döndükten sonra basılır, böylece çıktı iş parçacıklarının sırasından bağımsızdır.

```cpp
// kilit_olcumu.cpp — kilit alma sayisi ve yardimci sinamalar
#include <functional>
#include <iostream>
#include <thread>
#include <vector>

#include "hesap.hpp"
#include "kilit_olcum.hpp"
#include "kuyruk.hpp"
#include "teslim.hpp"

namespace {

void dort_is_parcacigi(KilitliSayac& s, void (*islev)(KilitliSayac&, int)) {
    std::vector<std::thread> isler;
    for (int i = 0; i < 4; ++i) isler.emplace_back(islev, std::ref(s), 10000);
    for (auto& is : isler) is.join();
}

}  // namespace

int main() {
    KilitliSayac her_seferde;
    dort_is_parcacigi(her_seferde, artir_her_seferde);
    std::cout << "kilitli sayac bekleme alma=" << her_seferde.alma
              << " deger=" << her_seferde.deger << "\n";

    KilitliSayac yerel;
    dort_is_parcacigi(yerel, artir_yerel);
    std::cout << "yerel toplam bekleme alma=" << yerel.alma
              << " deger=" << yerel.deger << "\n";

    Hesap a, b;
    a.bakiye = 1000;
    b.bakiye = 1000;
    {
        std::thread ileri([&] { for (int i = 0; i < 500; ++i) aktar(a, b, 1); });
        std::thread geri([&] { for (int i = 0; i < 500; ++i) aktar(b, a, 1); });
        ileri.join();
        geri.join();
    }
    std::cout << "iki yonlu aktarma bekleme toplam=" << a.bakiye + b.bakiye << "\n";

    Hesap x, y, z;
    x.bakiye = y.bakiye = z.bakiye = 1000;
    int ihlal = 0;
    {
        std::thread t1([&] { for (int i = 0; i < 300; ++i) aktar(x, y, 1); });
        std::thread t2([&] { for (int i = 0; i < 300; ++i) aktar(y, z, 1); });
        std::thread t3([&] { for (int i = 0; i < 300; ++i) aktar(z, x, 1); });
        std::thread denetci([&] {
            for (int i = 0; i < 200; ++i)
                if (toplam_bakiye(x, y, z) != 3000) ++ihlal;
        });
        t1.join();
        t2.join();
        t3.join();
        denetci.join();
    }
    std::cout << "uc hesap halkasi bekleme toplam=" << toplam_bakiye(x, y, z)
              << " ihlal=" << ihlal << "\n";

    Teslim teslim;
    int alinan = 0;
    {
        std::jthread tuketici([&] { alinan = bekle(teslim); });
        hazirla(teslim, 42);
    }
    std::cout << "kosul degiskeni bekleme alinan=" << alinan << "\n";

    Kuyruk kuyruk;
    int alinan_oge = 0;
    long oge_toplami = 0;
    {
        std::jthread tuketici([&] { oge_toplami = tuket(kuyruk, alinan_oge); });
        uret(kuyruk, 1000);
    }
    std::cout << "uretici tuketici bekleme alinan=" << alinan_oge
              << " toplam=" << oge_toplami << "\n";
    return 0;
}
```

```text
kilitli sayac bekleme alma=40000 deger=40000
yerel toplam bekleme alma=4 deger=40000
iki yonlu aktarma bekleme toplam=2000
uc hesap halkasi bekleme toplam=3000 ihlal=0
kosul degiskeni bekleme alinan=42
uretici tuketici bekleme alinan=1000 toplam=499500
```

**EB1.** İlk satır dört iş parçacığının kırk bin artışının her birinde kilit aldığını
gösterir: `alma=40000`, `deger=40000`. Her kilit alma, başka bir iş parçacığının
tuttuğu kilidi beklemek zorunda kalabileceği bir noktadır; ders bu noktaların
sayısını ölçüyor, gerçekte kaç kez beklendiğini değil. Sonuç `deger=40000` tanımlıdır
çünkü her artış kilit altında yapıldı; bu doğruluk kilidin verdiği garantidir.

**EB2.** Üçüncü satırdaki `toplam=2000`, iki yönde ters sırayla yapılan aktarmaların
`std::scoped_lock` sayesinde ne kilitlenme ne de kayıp bıraktığını gösterir: iki
bakiyenin toplamı değişmedi. Burada kilitleri hangi sırayla yazdığımızın bir önemi
yoktu; `aktar(a, b, …)` ve `aktar(b, a, …)` aynı iki kilidi zıt sırayla istedi.

**EB3.** Dördüncü satır üç kilidin bir halkasını sınar: `x`'ten `y`'ye, `y`'den `z`'ye ve
`z`'den `x`'e aktarma yapan üç iş parçacığı çalışırken, dördüncü iş parçacığı iki yüz
kez üç kilidi birden alıp toplamı okudu. Toplam her seferinde `3000` göründü ve
`ihlal=0` çıktı. Okuyan hiçbir aktarmanın ortasına denk gelmedi, çünkü hiçbir aktarma
iki kilidin yalnızca birini tutarken bakiyeyi değiştirmedi.

**EB4.** Beşinci satırda bekleyen iş parçacığı `hazirla` çağrılınca uyandı ve `42`
değerini aldı. `std::jthread` kapsamın sonunda `join()` yaptı, bu yüzden `alinan`
okunmadan önce yazılmış oldu. Uyanmanın hangi anda olduğu basılmadı; yalnızca ne alındığı
basıldı.

**EB5.** Son satır, tüketicinin bin değerin hepsini aldığını ve toplamın `499500`
olduğunu gösterir; bu, sıfırdan dokuz yüz doksan dokuza kadar sayıların toplamıdır.
Hiçbir değer kaybolmadı ve hiçbiri iki kez alınmadı. `bitti` bayrağı, kuyruk boşalsa
bile üretici henüz bitmemişken tüketicinin çıkmamasını, üretici bitip kuyruk boşalınca da
beklemekte takılı kalmamasını sağladı.

**EB6.** Beşinci ve altıncı satırdaki iki `std::jthread` de elle `join()` yazılmadan
bitirildi: kapsam kapanırken yıkıcı katıldı. Bu, sonucun okunabilir olmasının tek
dayanağıdır; `join()` ya da onu çağıran yıkıcı olmasaydı, `alinan` ve `oge_toplami`
okunurken iş parçacığı hâlâ yazıyor olabilirdi.

## Sınırlayıcı Ölçüm

Bu dersin ölçtüğü düzenek, ortak veriyi kilitle korumaktır; ve ikinci satır bu
düzeneğin bedelini düşürür. `artir_yerel` her iş parçacığında on bin artışı kendi yerel
toplamında yapar, kilidi yalnızca sonunda bir kez alıp toplamı ortak sayaca ekler.
Sonuç aynıdır, `deger=40000`; ama eşgüdüm noktası kırk binden **dört**e düştü:
`alma=4`. Eşgüdüm noktası on bin kat azaldı, doğruluk korundu.

Bundan çıkan ders, bedelin kilidin kendisinden değil **kaç kez alındığından** geldiğidir.
Aynı `std::mutex` iki ayrı yazımda kırk bin ya da dört kez alındı. Bunun sınırı da
şudur: yerel toplam yalnızca işin parçalara ayrılıp sonda birleştirilebildiği durumda
çalışır. Her adım ortak durumu görmek zorundaysa, yani bir iş parçacığının sonucu
öbürünün bir sonraki adımına girdiyse, kilit her adımda alınır ve sayı düşmez.
Bu durumda bedeli düşürmenin yolu kilidi bırakmak değil, ortak durumu bölmektir; bunu
sonraki iki derste bellek sıralaması ve veri düzeni olarak göreceğiz.

## Özet

- Bu dersin ölçtüğü düzenek bedeli **bekleme** sınıfına taşır: kilit, bir iş parçacığının
  beklemek zorunda kalabileceği nokta sayısını artırır; ölçü gerçek bekleme süresi değil,
  bu noktaların sayısıdır.
- `std::thread` kurulduktan sonra `join()` ile beklenir; iş parçacığının yazdığı her şey
  yalnızca `join()` döndükten sonra okunur. `std::jthread` aynı işi yıkıcısında yapar.
- Ortak veriye korumasız yazmak veri yarışıdır ve davranışı tanımsızdır;
  `std::mutex` ile `std::lock_guard` bu yarışı bitirir, kilidi kapsamın sonunda bırakır.
- `std::scoped_lock` birden çok kilidi kilitlenmeye yol açmayan bir sırayla alır; sıra
  disiplini gerekmez.
- `std::condition_variable` bir koşulu beklemek içindir ve `std::unique_lock` ister;
  koşul her uyanışta yeniden sınanır. `unique_lock` kilidi bırakıp yeniden alabildiği
  için üretici/tüketici kuyruğunda işin kilitsiz yapılmasına izin verir.
- `kilitli sayac` kırk bin kilit alması, `yerel toplam` dört kilit alması ile aynı sonucu
  verdi: bedeli kilit değil, kaç kez alındığı belirler.

## Sonraki Adım

Kilit, bir verinin kimin elinde olduğunu söyler ve bir iş parçacığının kilit altında
yazdığını kilidi sonra alana görünür kılar; ama görünürlük için hangi sıralamanın gerçekten
gerektiğini ve bunun kaç eşgüdüm noktasına mal olduğunu söylemez. Bir sonraki ders bellek modelini açıyor: bir
bellek sıralaması kaç eşgüdüm noktası istiyor sorusuyla `std::atomic`, release ve acquire
bağıntısını ölçeceğiz.
