yoklateknik mülakat

Backend Eşzamanlılık Mülakat Soruları

Eşzamanlılık mülakatlarının çekirdeği: paylaşılan ve değişebilen state, concurrency-parallelism ayrımı, kilit maliyetleri, deadlock/livelock/starvation, ölçekte çıkan tuzaklar ve bellek görünürlüğü.

Gerçek simülasyonu dene →

Konu özeti

Bu konu neyi ölçer

Eşzamanlılık soruları "hangi API'yi biliyorsun"u değil, paylaşılan ve değişebilen state'i doğru yönetip yönetemediğini ölçer. Mülakatta en sık dönülen nokta şudur: senkronizasyon yalnızca state hem paylaşılan hem mutable olduğunda gerekir. Her çağrıya ait yerel değişkenler ve açılıştan sonra hiç değişmeyen veri, istenildiği kadar thread'den serbestçe okunabilir — immutability, güvenli eşzamanlılığın en ucuz stratejisidir.

Karıştırılan temel ayrımlar

  • Concurrency ve parallelism aynı şey değildir. Concurrency yapıyla ilgilidir: yaşam süreleri örtüşen işleri çevirmek. Parallelism yürütmeyle ilgilidir: işlerin aynı anda gerçekten çalışması, ki bu birden çok core ister. Tek core'lu bir makine concurrent olabilir, ama asla gerçekten parallel olamaz.
  • Process ve thread. Bir process kendi adres alanına sahiptir; thread'leri o belleği paylaşır. Bu paylaşım iletişimi ucuzlatan şeydir — ve shared-state bug'larını mümkün kılan da aynı şeydir.
  • Critical section, paylaşılan state'e dokunan bölgedir; adındaki "critical" performansla ilgili değildir. O bölgenin çalışması karşılıklı dışlamalı hale getirilmelidir.
  • Race condition bir exception türü değildir. Tanımlayıcı özelliği zamanlamaya bağımlılıktır: aynı girdiler, işlemlerin iç içe geçişine göre farklı sonuçlar üretir. Sonuçları genellikle sessizce bozar; tehlikeli olmasının sebebi budur.

Klasik örnek counter = counter + 1. Kaynak kodda tek satır olması bir şey ifade etmez: bu bir read-modify-write dizisidir, iki thread okuma ile yazma arasında iç içe geçtiğinde bir artış diğerini ezer ve kaybolur.

Araçlar ve gerçek maliyetleri

Mutex yalnızca tek şey vaat eder: aynı anda tek sahip. Preemption'ı engellemez — kilidi tutan thread bölümün ortasında duraklatılabilir; diğerleri sadece o bırakana kadar giremez. Geri alma semantiği lock'ların değil transaction'ların işidir.

Tek global lock doğruluğu contention pahasına satın alır: yük altında thread'ler kilitte kuyruğa girer, core'lar boş durur. Kilidin maliyeti arkasındaki veri miktarından değil, onu isteyen thread sayısından gelir. Çare daha ince taneli kilitler ya da daha az paylaşılan mutable state'tir.

Read-write lock okuma-ağırlıklı ve okuma bölümleri uzun olan yüklerde kazandırır. İki bilinen maliyeti vardır: writer starvation (okuyucular sürekli gelirken yazar süresiz bekleyebilir; birçok implementasyon writer preference ekler) ve ekstra defter tutma — çok kısa bölümlerde düz mutex genellikle daha ucuzdur.

Counting semaphore "aynı anda en çok N" için doğru primitive'dir. Mutex bunun için aşırı kısıtlar; sleep ile bekletmek istek hızını şekillendirir, eşzamanlılığı değil.

Pessimistic ve optimistic locking. Pessimistic çakışmayı olmadan dışlar (SELECT ... FOR UPDATE), eşzamanlı yazarlar bekler. Optimistic herkesi ilerletir, çakışmayı yazma anında yakalar (tipik olarak WHERE version = ?), kaybeden retry eder. Düşük çekişmede optimistic, sıcak ve yüksek çekişmeli kayıtlarda pessimistic avantajlıdır — retry fırtınasını önler.

İlerlemenin durduğu üç biçim

  • Deadlock bir döngü gerektirir: her thread bir kilidi tutarken diğerini ister. Herkes kilitleri aynı sırayla (önce A sonra B) alırsa döngü kurulamaz.
  • Livelock'ta thread'ler bloklanmış değil meşguldür: koridorda birbirine yol vermeye çalışan iki insan gibi retry edip dururlar.
  • Starvation bir adalet sorunudur: aç kalan thread sırası gelse ilerleyebilir, ama CPU'yu ya da kilidi hep başkalarına kaptırır.

Deadlock ve livelock güvenilir şekilde kendiliğinden çözülmez; randomized backoff ya da tutarlı kilit sıralaması gibi bir tasarım düzeltmesi gerekir.

Ölçekte ortaya çıkan tuzaklar

  • Hot row. Satır kilidi transaction commit edene kadar tutulur; her istek tek bir sayaç satırının arkasına kuyruğa girer ve sistem seri bir boru hattına döner. Çare sayacı shard'lamak (N satır, rastgele artır, okurken topla) ya da artırımı asenkron toplamaya taşımaktır.
  • Thread pool deadlock. Parent task'lar tüm thread'leri kaplayıp aynı pool'da koşacak subtask'ları beklerse, ortada tek bir mutex olmadan deadlock oluşur. Çözüm: subtask'ları ayrı pool'da koşturmak, bloklamamak ya da uçuştaki parent sayısını pool boyutunun altında tutmak.
  • Event loop'u CPU işiyle tıkamak. Event loop işler arasında ancak kontrolü bıraktıkları noktada geçiş yapar. Saf CPU işi kontrolü hiç bırakmaz; bitene kadar tüm bağlantılar durur. Standart çözüm o işi worker'lara taşımaktır.
  • Thread saymadan ölçeklemek. Sekiz core en fazla sekiz CPU-bound thread çalıştırır; 800 thread arasında geçiş yapmak CPU'yu gerçek iş yerine context switch'e harcar.
  • False sharing. Aynı cache line'ı paylaşan bağımsız değişkenler, coherence protokolü altında line'ı core'lar arasında ping-pong eder: mantıksal olarak bağımsız, fiziksel olarak çekişmeli. Çare padding ya da thread-local biriktiriciler.

Görünürlük: kilit her şey değil

Doğruluk yalnızca karşılıklı dışlamadan ibaret değildir; yazmaların görünürlüğü ayrı bir konudur. happens-before senkronizasyon kenarlarıyla kurulur: aynı mutex'in sonraki kilidinden önce sıralanan bir unlock, acquire-load tarafından okunan bir release-store, ya da thread start/join. Yalnızca aynı adresi paylaşmak sıralama değil, data race yaratır; wall-clock sırası ise bir senkronizasyon kenarı olmadan anlamsızdır.

Bunun pratik iki sonucu:

  • Güvenli publication. Yarı kurulmuş bir nesnenin referansı diğer thread'lere sırasız görünebilir. Referansı release-acquire semantiği taşıyan bir atomic üzerinden kaydet ya da dilin garantili tek-seferlik initialization olanağını kullan.
  • Condition variable'da while. Uyanma bir kanıt değil ipucudur: spurious uyanmalar mümkündür ve gerçek bir sinyalden sonra bile uyanan thread mutex'i yeniden almak zorundadır — o boşlukta başka bir consumer öğeyi alabilir. Koşulu döngüde yeniden kontrol etmek evrensel sözleşmedir.

Mükerrerlik ve idempotency

Eşzamanlılık sorularının sık bir dalı "aynı iş iki kez yapıldı" vakasıdır. Client tarafındaki devre dışı buton, retry'ları ve ikinci sekmeyi baypas ettiği için hiçbir şey garanti etmez; pencereyi küçültmek de yarışı ortadan kaldırmaz. Garanti sunucuda yaşamalıdır: her deneme için bir token, unique constraint altında saklanır ve tekrarlara orijinal sonuç döndürülür. Durum geçişlerinde aynı işi koşullu update yapar: UPDATE ... SET status = 'paid' WHERE id = ? AND status = 'pending' ve yalnızca gerçekten satır değiştiyse devam et.

Mülakatta anlatabilmen gerekenler

  1. Bu state gerçekten paylaşılan ve mutable mı; değilse senkronizasyona hiç gerek yok.
  2. Seçtiğin primitive neyi vaat ediyor, neyi vaat etmiyor (mutex preemption'ı engellemez).
  3. Kilit granülerliğinin contention'a etkisi ve tek global kilidin faturası.
  4. Deadlock'u nasıl imkânsız kıldığın (tutarlı sıralama, tek kilit, sınırlı uçuş sayısı).
  5. Yazmaların diğer thread'lerde ne zaman görünür olduğu.
  6. Aynı isteğin iki kez gelmesi hâlinde sistemin ne yapacağı.

Bu özet, aşağıdaki soruların doğrulanmış açıklamalarından çıkarıldı.

Örnek sorular

EşzamanlılıkZorluk 1
Process ile thread arasındaki temel bellek farkı nedir?
  • aAynı process içindeki thread'ler process'in belleğini paylaşır; ayrı process'lerin normalde kendi address space'leri vardır
  • bProcess'ler belleği varsayılan olarak paylaşır; her thread izole bir address space alır
  • cHem thread'ler hem process'ler her zaman tamamen izole bellek alır
  • dThread'ler yalnızca aynı CPU core üzerinde çalıştıklarında bellek paylaşır
Açıklama:Bir process kendi address space'ine sahiptir; thread'leri onun içinde yaşar ve o belleği paylaşır. Bu paylaşım thread'ler arası iletişimi ucuz yapar — shared-state bug'larını mümkün kılan şey de budur. Paylaşım CPU core'una bağlı değildir (d): thread'ler nerede schedule edilirse edilsin belleği paylaşır.
EşzamanlılıkZorluk 1
Concurrency ile parallelism arasındaki fark nedir?
  • aTamamen aynı anlama gelirler — iki kelime tek kavramın birbirinin yerine geçen adlarıdır
  • bConcurrency, yaşam süreleri örtüşen birden çok işi yönetmektir; parallelism, aynı fiziksel anda birden fazlasını çalıştırmaktır
  • cParallelism tek core'da bile olabilir; concurrency ise her zaman birden çok core gerektirir
  • dConcurrency sadece I/O-bound işlere, parallelism sadece CPU-bound işlere uygulanır
Açıklama:Concurrency yapıyla ilgilidir — tek core'da bile yaşam süreleri örtüşen işleri çevirmek. Parallelism yürütmeyle ilgilidir — işlerin aynı anda gerçekten çalışması, ki bu birden çok core gerektirir. Tek core'lu bir makine concurrent olabilir ama asla gerçekten parallel olamaz; (c) tam tersini söylüyor.
EşzamanlılıkZorluk 2
Bir sunucu bellekte paylaşılan bir sayaç tutuyor.
counter = 0

function handle_request():
    counter = counter + 1

İki thread, handle_request() fonksiyonunu 1000'er kez çağırıyor. counter'ın son değeri ne olur?
  • aHer zaman tam 2000 — artırma tek satır olduğu için kesintiye uğrayamaz
  • bHer zaman tam 1000 — ikinci thread'in yazmaları ilkinin yazmalarını ezer
  • cEn fazla 2000, ama daha az olabilir — eşzamanlı artırmalar race condition'a kurban gidebilir
  • dProgram çöker, çünkü iki thread aynı değişkene yazamaz
Açıklama:counter = counter + 1 bir read-modify-write dizisidir. İki thread okuma ile yazma arasında iç içe geçtiğinde artışlardan biri diğerini ezer ve kaybolur; sonuç 2000'in altına düşebilir. Kaynak kodda tek satır olması (a) bir şey ifade etmez — birden çok adıma derlenir — ve eşzamanlı yazma tek başına programı çökertmez (d).
EşzamanlılıkZorluk 1
Concurrent programlamada critical section nedir?
  • aProgramın performans açısından en kritik, her şeyden önce optimize edilmesi gereken sıcak yolu
  • bPaylaşılan bir kaynağa erişen ve aynı anda yalnızca tek thread'de çalışması gereken kod
  • cUygulama açılışında, henüz hiçbir thread yokken çalışan kod
  • dYakalanmayan exception'ları tüm process'i düşüren blok
Açıklama:Critical section, paylaşılan state'e dokunan bölgedir; bu yüzden çalışması karşılıklı dışlamalı (mutually exclusive) hale getirilmelidir — tipik olarak bir mutex ile. İsimdeki 'critical' performansla ilgili değildir (a); karışıklık kelimeyi 'performance-critical' okumaktan gelir.
EşzamanlılıkZorluk 2
Bir mutex'i tutmak gerçekte neyi garanti eder?
  • aAynı anda en fazla bir thread onu tutar; lock ile unlock arasındaki kod asla eşzamanlı çalışmaz
  • bKoruduğu kod tek bir CPU core'a sabitlenir ve hızlanır
  • cThread lock'u tuttuğu sürece OS scheduler onu preempt etmez
  • dÇakışan yazmalar otomatik tespit edilip geri alınır
Açıklama:Mutex yalnızca mutual exclusion sağlar: aynı anda tek sahip. Preemption'ı engellemez (c) — lock'u tutan thread bölümün ortasında duraklatılabilir; diğerleri sadece o bırakana kadar giremez. Geri alma semantiği (d) lock'ların değil transaction'ların işidir.
EşzamanlılıkZorluk 2
thread 1:        thread 2:
lock(A)          lock(B)
lock(B)          lock(A)
# ... iş ...     # ... iş ...
unlock(B)        unlock(A)
unlock(A)        unlock(B)

Bu iki thread aynı anda çalıştığında ne olabilir?
  • aHer zaman tamamlanırlar, çünkü lock istekleri ilk gelen alır sırasıyla verilir
  • bRuntime hatası oluşur: aynı mutex iki farklı thread'den istenemez
  • cLivelock: iki thread iş yapmadan sonsuza dek lock alıp bırakır
  • dDeadlock mümkündür: her thread bir lock'u tutarken diğerini sonsuza dek bekleyebilir
Açıklama:Thread 1 A'yı, thread 2 B'yi aldıktan sonra ikisi de ikinci lock'una uzanırsa, her biri diğerinin tuttuğu lock'u bekler — sonsuza dek. FIFO dağıtım (a) genel bir garanti değildir ve işe de yaramazdı; thread'ler farklı lock'ları bekliyor. Livelock (c) aktif deneme-vazgeçme içerir; düz bloklayan lock'lar bunu yapmaz.

3750 soruluk Backend bankasında kendini sına.

Mülakata başla