Concurrency (Eşzamanlılık): Lock, Race Condition ve Deadlock

Modern yazılım sistemleri, aynı anda birden fazla işlemi gerçekleştirmek zorundadır. Kullanıcılar aynı anda sipariş verir, aynı bilet için rezervasyon yapar veya aynı dokümanı düzenler. İşte tam bu noktada Concurrency (Eşzamanlılık) devreye girer.

Bu yazıda, eşzamanlı programlamanın temel kavramlarını, karşılaşılan sorunları ve çözüm tekniklerini detaylıca inceleyeceğiz.


1. Concurrency Nedir?

Concurrency, birden fazla işlemin aynı zaman dilimi içinde yürütülmesidir. Bu işlemler:

  • Paralel çalışabilir (farklı CPU çekirdeklerinde aynı anda)
  • Paralel olmayan şekilde çalışabilir (tek CPU'da zaman dilimleriyle)

Gerçek Hayat Örneği: Bilet Satış Sistemi

Bir konser için 100 bilet satılıyor. 200 kullanıcı aynı anda satın almaya çalışıyor. Eğer concurrency yönetimi yoksa, aynı bileti iki kişi satın alabilir!


2. Race Condition (Yarış Koşulu)

Mantık

Race condition, birden fazla işlemin paylaşılan bir kaynağa aynı anda erişmeye çalışması ve işlemlerin zamanlamasına bağlı olarak sonucun değişmesi durumudur.

Klasik Örnek: Bakiye Güncelleme

# İki thread'in aynı anda çalıştığını düşünelim
bakiye = 100

# Thread 1
def para_cek_100():
    if bakiye >= 100:        # 1. okuma
        bakiye = bakiye - 100 # 2. yazma

# Thread 2
def para_cek_50():
    if bakiye >= 50:          # 1. okuma
        bakiye = bakiye - 50  # 2. yazma

Race Condition Senaryosu

Başlangıç: bakiye = 100

Zaman:
T1: Thread 1 okur -> bakiye = 100
T2: Thread 2 okur -> bakiye = 100
T3: Thread 1 yazar -> bakiye = 0 (100 - 100)
T4: Thread 2 yazar -> bakiye = -50 (100 - 50 ???)

Sonuç: bakiye = -50 (Bu imkansız!)

Çözüm: Atomic Operasyonlar

import threading

lock = threading.Lock()
bakiye = 100

def guvenli_para_cek_100():
    global bakiye
    with lock:  # Thread-safe blok
        if bakiye >= 100:
            bakiye -= 100

def guvenli_para_cek_50():
    global bakiye
    with lock:
        if bakiye >= 50:
            bakiye -= 50

3. Lock (Kilit) Mekanizmaları

Lock'lar, paylaşılan kaynaklara aynı anda sadece bir thread'in erişmesini sağlayan mekanizmalardır.

3.1 Mutex (Mutual Exclusion - Karşılıklı Dışlama)

En temel lock türüdür. Bir thread lock'u alır, işini bitirir, lock'u bırakır.

import threading

mutex = threading.Lock()
shared_counter = 0

def increment_counter():
    global shared_counter
    for _ in range(1000000):
        with mutex:  # Lock al
            shared_counter += 1  # Kritik bölge
        # Lock otomatik bırakılır

# 10 thread oluştur
threads = [threading.Thread(target=increment_counter) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()

print(shared_counter)  # 10,000,000 (her zaman doğru!)

3.2 Semaphore (Semafor)

Belirli sayıda thread'in aynı anda kaynağa erişmesine izin verir.

import threading
import time

# Aynı anda sadece 3 thread veritabanı bağlantısı kullanabilsin
connection_pool = threading.Semaphore(3)

def database_query(query):
    with connection_pool:
        print(f"{threading.current_thread().name} bağlantı aldı")
        time.sleep(2)  # SQL sorgusu çalışıyor
        print(f"{threading.current_thread().name} bağlantı bıraktı")

# 10 thread çalıştır
for i in range(10):
    t = threading.Thread(target=database_query, args=(f"SELECT {i}",))
    t.start()

3.3 Read-Write Lock (Okuyucu-Yazıcı Kilidi)

Okuma işlemleri eşzamanlı yapılabilirken, yazma işlemleri özel erişim gerektirir.

import threading

class ReadWriteLock:
    def __init__(self):
        self.readers = 0
        self.read_lock = threading.Lock()
        self.write_lock = threading.Lock()

    def acquire_read(self):
        with self.read_lock:
            self.readers += 1
            if self.readers == 1:
                self.write_lock.acquire()  # İlk okuyucu yazıcıyı engeller

    def release_read(self):
        with self.read_lock:
            self.readers -= 1
            if self.readers == 0:
                self.write_lock.release()  # Son okuyucu yazıcıya izin verir

    def acquire_write(self):
        self.write_lock.acquire()  # Yazmak için bekle

    def release_write(self):
        self.write_lock.release()

# Kullanım
rw_lock = ReadWriteLock()
cache = {}

def read_data(key):
    rw_lock.acquire_read()
    try:
        return cache.get(key)
    finally:
        rw_lock.release_read()

def write_data(key, value):
    rw_lock.acquire_write()
    try:
        cache[key] = value
    finally:
        rw_lock.release_write()

3.4 Spinlock (Dönel Kilit)

Thread, lock serbest kalana kadar boşta beklemez, sürekli kontrol eder (döner).

import threading
import time

class Spinlock:
    def __init__(self):
        self.locked = False
        self.lock = threading.Lock()

    def acquire(self):
        while True:
            with self.lock:
                if not self.locked:
                    self.locked = True
                    return
            # Kısa bir bekleme (CPU yakmamak için)
            time.sleep(0.000001)

    def release(self):
        with self.lock:
            self.locked = False

# Spinlock, çok kısa süreli kritik bölgeler için idealdir

4. Deadlock (Kilitlenme)

Mantık

Deadlock, iki veya daha fazla işlemin birbirlerinin tuttuğu kaynakları beklerken sonsuza kadar bloke olması durumudur.

Deadlock için 4 Koşul (Coffman Şartları)

  1. Mutual Exclusion (Karşılıklı Dışlama): Kaynaklar paylaşılamaz
  2. Hold and Wait (Tut ve Bekle): Bir kaynağı tutarken başka kaynak bekleme
  3. No Preemption (Zorla Alma Yok): Kaynaklar zorla alınamaz
  4. Circular Wait (Dairesel Bekleme): Her thread bir sonrakini bekler

Klasik Deadlock Örneği

import threading
import time

lock1 = threading.Lock()
lock2 = threading.Lock()

def thread1():
    with lock1:
        print("Thread 1: lock1 aldı")
        time.sleep(0.1)  # Thread2'nin lock2'yi almasına izin ver
        print("Thread 1: lock2 bekliyor...")
        with lock2:  # Deadlock burada!
            print("Thread 1: lock2 aldı")

def thread2():
    with lock2:
        print("Thread 2: lock2 aldı")
        time.sleep(0.1)
        print("Thread 2: lock1 bekliyor...")
        with lock1:  # Deadlock burada!
            print("Thread 2: lock1 aldı")

# Thread'leri başlat
t1 = threading.Thread(target=thread1)
t2 = threading.Thread(target=thread2)
t1.start()
t2.start()
t1.join()
t2.join()

Deadlock Çözümleri

Çözüm 1: Lock Sıralaması (Lock Ordering)

# Her zaman lock'ları aynı sırada al!
def thread1_safe():
    with lock1:
        with lock2:
            print("Thread 1: her iki lock da alındı")

def thread2_safe():
    with lock1:  # Aynı sıra!
        with lock2:
            print("Thread 2: her iki lock da alındı")

Çözüm 2: Zaman Aşımı (Timeout)

def thread_with_timeout():
    acquired_lock1 = lock1.acquire(timeout=1)
    if not acquired_lock1:
        print("lock1 alınamadı, iptal")
        return

    try:
        acquired_lock2 = lock2.acquire(timeout=1)
        if not acquired_lock2:
            print("lock2 alınamadı, lock1 bırakılıyor")
            return
        try:
            print("Kritik işlem yapılıyor...")
        finally:
            lock2.release()
    finally:
        lock1.release()

Çözüm 3: Banker's Algorithm (Teorik)

# Kaynak tahsisini merkezi olarak yönet
class ResourceManager:
    def __init__(self, total_resources):
        self.available = total_resources.copy()
        self.allocation = {}
        self.lock = threading.Lock()

    def request_resources(self, process_id, request):
        with self.lock:
            # Deadlock oluşmayacağını kontrol et
            if all(request[r] <= self.available[r] for r in request):
                # Geçici olarak tahsis et
                for r in request:
                    self.available[r] -= request[r]
                self.allocation[process_id] = request
                return True
            return False

5. Veritabanı Seviyesinde Concurrency

5.1 Optimistic Locking (İyimser Kilit)

-- Tabloya version sütunu ekle
ALTER TABLE products ADD COLUMN version INTEGER DEFAULT 0;

-- Güncelleme yaparken version kontrolü
UPDATE products 
SET stock = stock - 1, version = version + 1
WHERE id = 123 AND version = 5;

-- Etkilenen satır sayısı 0 ise, başkası güncellemiş demektir

5.2 Pessimistic Locking (Kötümser Kilit)

-- Satırı kilitle (başkası okuyamaz/yazamaz)
BEGIN;
SELECT * FROM products WHERE id = 123 FOR UPDATE;
-- İşlemler...
UPDATE products SET stock = stock - 1 WHERE id = 123;
COMMIT;

6. Modern Concurrency Araçları

6.1 Python Asyncio (Tek Thread'li Concurrency)

import asyncio

async def fetch_data(url):
    print(f"İndiriliyor: {url}")
    await asyncio.sleep(1)  # I/O işlemi simülasyonu
    return f"Veri: {url}"

async def main():
    urls = ['url1', 'url2', 'url3']
    tasks = [fetch_data(url) for url in urls]
    results = await asyncio.gather(*tasks)
    print(results)

# Çalıştır
asyncio.run(main())

6.2 Go Routines (Goroutines)

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var counter int
    var mutex sync.Mutex
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            mutex.Lock()
            counter++
            mutex.Unlock()
        }()
    }

    wg.Wait()
    fmt.Println("Counter:", counter)
}

6.3 Actor Model (Akka, Erlang)

// Actor modelinde her aktör kendi durumuna sahiptir ve mesajlarla iletişir
public class BankAccount extends AbstractActor {
    private int balance = 0;

    @Override
    public Receive createReceive() {
        return receiveBuilder()
            .match(Deposit.class, deposit -> {
                balance += deposit.amount;
                sender().tell(new DepositResult(true), self());
            })
            .match(Withdraw.class, withdraw -> {
                if (balance >= withdraw.amount) {
                    balance -= withdraw.amount;
                    sender().tell(new WithdrawResult(true), self());
                } else {
                    sender().tell(new WithdrawResult(false), self());
                }
            })
            .build();
    }
}

7. Pratik İpuçları ve En İyi Pratikler

Lock Kullanırken Dikkat Edilecekler

  1. Lock'ları mümkün olduğunca kısa tutun

    # KÖTÜ
    with lock:
       veri_oku()
       hesaplama_yap()
       dosya_yaz()
       api_cagir()  # 2 saniye bekler!
    
    # İYİ
    with lock:
       veri_oku()
       temp = hesaplama_yap()
    dosya_yaz(temp)  # lock dışında
    api_cagir(temp)  # lock dışında
  2. İç içe lock'lardan kaçının

    # KÖTÜ - deadlock riski
    with lock_a:
       with lock_b:
           islem_yap()
    
    # İYİ - lock'ları aynı anda al
    with lock_a, lock_b:
       islem_yap()
  3. Thread-safe veri yapıları kullanın

    from queue import Queue
    from collections import deque
    
    # Thread-safe kuyruk
    safe_queue = Queue()
    
    # Thread-safe değil!
    unsafe_list = []

Race Condition Tespiti için Araçlar

# Python'da race condition tespiti
pip install pytest-xdist
pytest -n 10 test_file.py

# Thread sanitizer (C/C++)
gcc -fsanitize=thread -g program.c -o program
./program

8. Gerçek Hayat Senaryosu: Stok Yönetimi

Tam bir e-ticaret stok yönetimi örneği:

import threading
import time
import random
from dataclasses import dataclass
from typing import Dict

@dataclass
class Product:
    id: int
    name: str
    stock: int
    version: int = 0

class InventoryService:
    def __init__(self):
        self.products: Dict[int, Product] = {}
        self.locks: Dict[int, threading.Lock] = {}
        self.global_lock = threading.Lock()

    def _get_lock(self, product_id: int) -> threading.Lock:
        # Her ürün için ayrı lock (fine-grained locking)
        with self.global_lock:
            if product_id not in self.locks:
                self.locks[product_id] = threading.Lock()
            return self.locks[product_id]

    def add_product(self, product: Product):
        self.products[product.id] = product

    def purchase_product(self, product_id: int, quantity: int) -> bool:
        lock = self._get_lock(product_id)

        with lock:
            product = self.products.get(product_id)
            if not product:
                return False

            # Stok kontrolü
            if product.stock < quantity:
                return False

            # Stok güncelleme (atomic)
            product.stock -= quantity
            product.version += 1

            # Satın alma log'u
            print(f"[{threading.current_thread().name}] "
                  f"{product.name} - {quantity} adet satıldı. "
                  f"Kalan stok: {product.stock}")

            return True

    def get_stock(self, product_id: int) -> int:
        lock = self._get_lock(product_id)
        with lock:
            product = self.products.get(product_id)
            return product.stock if product else -1

# Simülasyon
def simulate_purchases(inventory: InventoryService, product_id: int):
    for _ in range(100):
        quantity = random.randint(1, 3)
        success = inventory.purchase_product(product_id, quantity)
        if not success:
            # Stok bitmiş
            break
        time.sleep(random.uniform(0.01, 0.05))

# Test
inventory = InventoryService()
inventory.add_product(Product(id=1, name="Akıllı Telefon", stock=50))

threads = []
for i in range(10):  # 10 kullanıcı aynı anda satın almaya çalışıyor
    t = threading.Thread(
        target=simulate_purchases,
        args=(inventory, 1),
        name=f"Kullanıcı-{i}"
    )
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print(f"\nSon stok: {inventory.get_stock(1)}")
print(f"Toplam satılan: {50 - inventory.get_stock(1)}")

Özet: Concurrency Kontrol Listesi

  • [ ] Race Condition riski var mı? Paylaşılan kaynakları kontrol edin
  • [ ] Lock sıralaması tutarlı mı? Deadlock'tan kaçınmak için aynı sırayı kullanın
  • [ ] Kritik bölge ne kadar uzun? Mümkün olduğunca kısa tutun
  • [ ] Hangi lock tipi uygun? Mutex, semaphore, read-write?
  • [ ] Veritabanı işlemleri için hangi izolasyon seviyesi? Optimistic vs Pessimistic
  • [ ] Timeout mekanizması var mı? Sonsuz bekleme olmasın
  • [ ] Test edildi mi? Yüksek eşzamanlılıkta test yapın
  • [ ] Monitoring var mı? Deadlock ve contention metriklerini izleyin

Unutmayın: Concurrency sorunlarını test etmek zordur çünkü rastgele ortaya çıkarlar. "Test geçti, production'da patladı" senaryosu en çok burada görülür! Bu yüzden tasarım aşamasında doğru stratejiyi belirlemek kritik önem taşır.