Nesne Tabanlı Programlama 2

12 - Eş Zamanlılık, Paralellik ve Asenkron Programlama

Emre Can Yılmaz

Ondokuz Mayıs Üniversitesi

2026

Ana Soru

Bir program aynı anda birden fazla işle ilgilenmek zorunda kalabilir.

Örneğin:

  • Kullanıcıdan giriş beklerken dosya kaydetmek
  • Birden fazla dosya üzerinde işlem yapmak
  • Web’den veri beklerken programı durdurmamak
  • Büyük bir hesaplamayı daha kısa sürede bitirmek

Bu derste bu tür durumlar için kullanılan üç temel yaklaşımı göreceğiz.

Üç Temel Yaklaşım

Yaklaşım Temel fikir Python’daki araç
Eş zamanlılık Birden fazla işi aynı zaman aralığında yönetmek threading
Paralellik Birden fazla işi gerçekten aynı anda çalıştırmak multiprocessing
Asenkron programlama Bekleme sırasında başka görevlere geçebilmek asyncio

Bu üç yaklaşım aynı şey değildir.

En önemli soru şudur:

Program daha çok bekliyor mu, yoksa daha çok hesaplama mı yapıyor?

Bekleyen İş ve Hesaplayan İş

Bazı işler çoğunlukla bekleme içerir.

Örnek:

  • Dosya okuma
  • Ağ isteği
  • Veritabanı sorgusu
  • Kullanıcıdan giriş bekleme

Bazı işler ise işlemciyi yoğun kullanır.

Örnek:

  • Büyük matematiksel hesaplamalar
  • Görüntü işleme
  • Şifreleme
  • Büyük veri üzerinde analiz

Hangi Yaklaşım Ne Zaman?

Durum Uygun yaklaşım
Program çoğunlukla dosya, ağ veya kullanıcı bekliyorsa threading veya asyncio
Program yoğun hesaplama yapıyorsa multiprocessing
Çok sayıda ağ isteği veya bekleyen görev varsa asyncio
Aynı veriye birden fazla thread erişiyorsa threading + dikkatli veri koruma

Python’da threading her zaman gerçek paralellik sağlamaz.

CPU-bound işler için çoğu zaman multiprocessing daha uygundur.

Threading Nedir?

Thread, aynı program içinde çalışan ayrı bir yürütme akışı gibi düşünülebilir.

Bir program birden fazla thread başlatabilir.

Bu özellikle bekleme içeren işlerde yararlı olabilir.

Örnek:

  • Bir thread dosya okuyabilir.
  • Başka bir thread kullanıcı arayüzünü açık tutabilir.
  • Başka bir thread arka planda kayıt işlemi yapabilir.

Basit Threading Örneği

import threading
import time

def yaz():
    for i in range(5):
        print("Yazma işlemi")
        time.sleep(0.2)

def oku():
    for i in range(5):
        print("Okuma işlemi")
        time.sleep(0.2)

thread1 = threading.Thread(target=yaz)
thread2 = threading.Thread(target=oku)

thread1.start()
thread2.start()

thread1.join()
thread2.join()

Bu örnekte iki farklı thread başlatılır.

Çıktıların sırası her çalıştırmada aynı olmak zorunda değildir.

start() ve join()

start() thread’i başlatır.

thread1.start()

join() ise thread’in tamamlanmasını bekler.

thread1.join()

join() kullanılmazsa ana program thread’i başlattıktan sonra sonraki satırlara devam eder.

Thread’in bitmesini beklemek istiyorsak join() kullanırız.

Kısa Alıştırma

Önceki örneği değiştirin.

Üç thread oluşturun:

  • Birinci thread: "Dosya okunuyor"
  • İkinci thread: "Veri işleniyor"
  • Üçüncü thread: "Sonuç kaydediliyor"

Her thread kendi mesajını 5 kez yazdırsın.

Çıktı sırasını gözlemleyin.

Paylaşılan Veri Problemi

Race Condition Nedir?

Birden fazla thread aynı veriyi aynı anda değiştirmeye çalışırsa beklenmeyen sonuçlar ortaya çıkabilir.

Bu duruma race condition denir.

Örnek olarak ortak bir sayaç düşünelim.

Sayaç değerini 10 thread aynı anda artırmaya çalışırsa bazı artırma işlemleri kaybolabilir.

Race Condition Örneği

import threading
import time

class Counter:
    def __init__(self):
        self.counter = 0

    def increment(self):
        current_value = self.counter
        time.sleep(0.00001)
        self.counter = current_value + 1

counter = Counter()

def worker():
    for _ in range(10000):
        counter.increment()

Burada increment() metodu ortak sayaç değerini artırır.

Fakat bu işlem aynı anda birden fazla thread tarafından çağrılabilir.

Race Condition Örneği: Devamı

threads = []

for _ in range(10):
    thread = threading.Thread(target=worker)
    threads.append(thread)

for thread in threads:
    thread.start()

for thread in threads:
    thread.join()

expected_value = len(threads) * 10000

print(f"Son sayaç değeri: {counter.counter}")
print(f"Beklenen sayaç değeri: {expected_value}")

Beklenen değer:

100000

Fakat program her zaman bu değeri üretmeyebilir.

Sorun Nerede?

Sayaç artırma işlemi tek adım gibi görünür.

Fakat aslında birkaç adımdan oluşur:

  1. Sayaç değerini oku.
  2. Yeni değeri hesapla.
  3. Yeni değeri sayaç değişkenine yaz.

İki thread aynı eski değeri okursa, artırmalardan biri kaybolabilir.

Lock ile Çözüm

import threading
import time

class Counter:
    def __init__(self):
        self.counter = 0
        self.lock = threading.Lock()

    def increment(self):
        with self.lock:
            current_value = self.counter
            time.sleep(0.00001)
            self.counter = current_value + 1

with self.lock: bloğuna aynı anda yalnızca bir thread girebilir.

Böylece sayaç değeri okunurken ve güncellenirken başka bir thread araya giremez.

Lock Kullanırken

Lock, paylaşılan veriyi korumak için kullanılır.

Ancak her yere lock koymak doğru bir yaklaşım değildir.

Dikkat edilmesi gerekenler:

  • Sadece paylaşılan veriyi koruyun.
  • Lock içinde mümkün olduğunca az işlem yapın.
  • with lock: kullanımını tercih edin.
  • Gereksiz lock kullanımı programı yavaşlatabilir.

Thread-Safe Kavramı

Bir kod parçası aynı anda birden fazla thread tarafından kullanıldığında veri tutarlılığını koruyabiliyorsa thread-safe kabul edilir.

Örneğin:

  • Paylaşılan sayaç korunuyorsa thread-safe olabilir.
  • Paylaşılan liste kontrolsüz değiştiriliyorsa sorun çıkabilir.
  • Sınıf içinde ortak durum varsa bu durum dikkatli yönetilmelidir.

Bu derste temel çözüm olarak Lock kullanımını gördük.

Multiprocessing Nedir?

Multiprocessing, birden fazla process kullanarak çalışır.

Thread’lerden farklı olarak process’ler ayrı bellek alanlarına sahiptir.

Bu yapı özellikle CPU-bound işler için uygundur.

Örnek:

  • Büyük sayısal hesaplamalar
  • Görüntü işleme
  • Büyük veri parçalarının ayrı ayrı işlenmesi

Neden Process?

Her process ayrı çalıştığı için birden fazla CPU çekirdeğinden yararlanmak mümkün olabilir.

Multiprocessing Örneği

Aşağıdaki örnekte 1’den 100’e kadar sayıların kareleri hesaplanır.

Her process 10 sayının karesini hesaplar.

from multiprocessing import Process

def calculate_squares(start, end):
    result = []

    for i in range(start, end + 1):
        result.append(i * i)

    print(f"{start}-{end} arası kareler: {result}")

if __name__ == "__main__":
    processes = []

    for i in range(1, 101, 10):
        p = Process(target=calculate_squares, args=(i, i + 9))
        processes.append(p)
        p.start()

    for p in processes:
        p.join()

    print("Tüm işlemler bitti!")

if __name__ == "__main__" Notu

multiprocessing kullanırken ana program bloğu şu şekilde korunmalıdır:

if __name__ == "__main__":
    ...

Bu kullanım özellikle Windows ve macOS ortamlarında önemlidir.

Yeni process oluşturulurken Python dosyası yeniden yüklenebilir.

Ana kod bu blok içine alınmazsa program beklenmeyen şekilde tekrar çalışabilir.

Process Çıktıları

Birden fazla process aynı anda çalıştığında çıktıların sırası garanti edilmez.

Örneğin:

21-30 arası kareler: ...
1-10 arası kareler: ...
41-50 arası kareler: ...

Bu durum hata olmak zorunda değildir.

Process’lerin çalışma sırası işletim sistemi tarafından belirlenir.

Asyncio Nedir?

asyncio, bekleme içeren işleri asenkron olarak yönetmek için kullanılır.

Özellikle şu tür işler için uygundur:

  • Ağ istekleri
  • API çağrıları
  • WebSocket bağlantıları
  • Çok sayıda bekleyen görev

Burada amaç CPU’yu daha fazla kullanmak değildir.

Amaç, bekleme sırasında programın başka görevleri ilerletebilmesidir.

Event Loop

asyncio programlarında görevleri event loop yönetir.

Event loop basitçe şunu yapar:

  • Çalışabilecek görevi çalıştırır.
  • Bir görev beklemeye geçerse başka göreve geçer.
  • Bekleyen görev hazır olduğunda onu tekrar devam ettirir.

Bu derste asenkron programları çalıştırmak için şu yapıyı kullanacağız:

asyncio.run(main())

async ve await

Asenkron fonksiyonlar async def ile tanımlanır.

async def selamla():
    print("Merhaba")

Bekleme gerektiren asenkron işlemler await ile beklenir.

await asyncio.sleep(1)

await sırasında event loop başka görevleri çalıştırabilir.

time.sleep() ve asyncio.sleep() Farkı

Normal bekleme:

import time

time.sleep(1)

Bu kullanım mevcut akışı durdurur.

Asenkron bekleme:

import asyncio

await asyncio.sleep(1)

Bu kullanım event loop’a başka görevleri çalıştırma fırsatı verir.

Asyncio Örneği

import asyncio

async def veri_cek(kaynak, sure):
    print(f"{kaynak} için istek gönderildi")
    await asyncio.sleep(sure)
    print(f"{kaynak} yanıt verdi")
    return f"{kaynak} sonucu"

async def main():
    sonuclar = await asyncio.gather(
        veri_cek("API-1", 2),
        veri_cek("API-2", 1),
        veri_cek("API-3", 3)
    )

    print(sonuclar)

asyncio.run(main())

Bu örnekte üç görev birlikte başlatılır.

Hangisinin cevabı önce gelirse o görev önce tamamlanır.

asyncio.gather()

asyncio.gather() birden fazla asenkron görevi birlikte çalıştırmak için kullanılır.

await asyncio.gather(
    gorev1(),
    gorev2(),
    gorev3()
)

Bu yapı, verilen görevlerin tamamlanmasını bekler.

Bekleme sırasında görevler birbirini gereksiz yere bekletmez.

Asyncio Ne Zaman Uygun Değildir?

asyncio, CPU-bound işler için tek başına uygun değildir.

Örneğin:

  • Büyük matematiksel hesaplama
  • Görüntü işleme
  • Uzun süren döngüler
  • İşlemciyi yoğun kullanan algoritmalar

Bu tür işler event loop’u bloklayabilir.

Bu durumda multiprocessing daha uygun olabilir.

Hangi Durumda Hangisi?

Senaryo Uygun yaklaşım Neden
Dosya okurken programın beklememesi threading I/O-bound iş
Büyük bir hesaplamayı parçalara bölmek multiprocessing CPU-bound iş
100 farklı API adresinden veri çekmek asyncio Çok sayıda bekleyen ağ isteği
Ortak sayacı birden fazla thread ile artırmak threading + Lock Paylaşılan veri korunmalı
Arayüz açıkken arka planda indirme yapmak threading veya asyncio Bekleme içeren görev

Mini Karar Alıştırması

Aşağıdaki durumlar için uygun yaklaşımı seçin:

  1. Bir klasördeki 500 dosyanın okunması
  2. Büyük bir görüntü üzerinde filtreleme işlemi yapılması
  3. 100 farklı API adresinden veri çekilmesi
  4. Bir sayaç değişkeninin 10 thread tarafından artırılması
  5. Kullanıcı arayüzü açıkken arka planda dosya indirilmesi

Seçenekler:

  • threading
  • multiprocessing
  • asyncio
  • threading + Lock

Mini Karar Alıştırması: Olası Cevaplar

Durum Olası cevap Gerekçe
500 dosyanın okunması threading Dosya okuma bekleme içerebilir
Büyük görüntü filtreleme multiprocessing İşlemci yoğun işlem olabilir
100 API adresinden veri çekme asyncio Çok sayıda ağ beklemesi vardır
Ortak sayacı artırma threading + Lock Paylaşılan veri korunmalıdır
Arka planda dosya indirme threading veya asyncio Bekleme içeren görevdir

Bazı durumlarda birden fazla doğru cevap olabilir.

Önemli olan seçimin gerekçesini açıklayabilmektir.

Quiz 1

Hangisi gerçek paralellik için daha uygundur?

  1. threading
  2. asyncio
  3. multiprocessing
  4. time.sleep
  5. input

Quiz 2

Aşağıdaki kodda thread’in başlaması için hangi satır eksiktir?

import threading

def islem():
    print("Merhaba")

thread = threading.Thread(target=islem)
...
  1. thread.run()
  2. thread.join()
  3. thread.start()
  4. thread.call()
  5. thread.activate()

Quiz 3

Asenkron bir fonksiyon hangi anahtar kelime ile tanımlanır?

  1. async
  2. await
  3. yield
  4. lambda
  5. defasync

Ödev

Aşağıdaki üç küçük uygulamayı yazın.

  1. Threading uygulaması

    Bir klasördeki birden fazla metin dosyasındaki kelime sayılarını thread kullanarak hesaplayın.

  2. Multiprocessing uygulaması

    Büyük bir metni parçalara bölerek her parçadaki kelime sayısını farklı process ile hesaplayın.

  3. Asyncio uygulaması

    Farklı kaynaklardan veri çekmeyi temsil eden üç asenkron fonksiyon yazın. Her fonksiyon farklı süre beklesin ve sonucunu döndürsün.

Ödev İçin Örnek Dosya Hazırlama

Threading uygulamasında kullanmak için birkaç örnek metin dosyası oluşturabilirsiniz.

from pathlib import Path

klasor = Path("metinler")
klasor.mkdir(exist_ok=True)

icerikler = [
    "Python thread kullanımı giriş seviyesi bir konudur.",
    "Eş zamanlılık bekleme içeren işlerde faydalı olabilir.",
    "Dosya okuma işlemleri I/O-bound örneği olarak düşünülebilir."
]

for index, icerik in enumerate(icerikler, start=1):
    dosya = klasor / f"metin_{index}.txt"
    dosya.write_text(icerik, encoding="utf-8")

Ödev İçin Beklenenler

Her uygulama için:

  • Kullanılan yöntemi kısaca açıklayın.
  • Kodunuzu çalıştırın.
  • Çıktıyı yorumlayın.
  • Hangi yöntemin hangi problem türüne daha uygun olduğunu belirtin.

Ek soru:

Aynı problemi üç yöntemle çözmeye çalışmak her zaman mantıklı mıdır? Neden?

Kapanış

Bu derste üç temel yaklaşımı gördük:

  • threading: Bekleme içeren işleri aynı program içinde yönetmek
  • multiprocessing: CPU-bound işleri farklı process’lerle çalıştırmak
  • asyncio: Çok sayıda bekleyen görevi event loop ile yönetmek

Temel karar noktası:

Program daha çok bekliyor mu, yoksa daha çok hesaplama mı yapıyor?

Bu ayrım, kullanılacak yöntemi belirlemede en önemli ölçüttür.

Ek Kaynaklar