PythonProgramlamaPython Geliştirici

Neden tek bir **Python** işlemindeki birden fazla **thread** mevcut çoklu çekirdekli bir sistemde bile tek bir CPU çekirdeğinde sıralı bir şekilde çalışır?

Hintsage yapay zeka asistanı ile mülakatları geçin

Sorunun cevabı

Python'un Global Interpreter Lock (GIL), Python nesnelerine erişimi koruyan bir mutex'dir ve o anda yalnızca bir thread'in Python bytecode'unu yürütmesini sağlar. Bu tasarım kararı, CPython'da bellek yönetimini basitleştirmek ve nesne referans sayısındaki yarış durumlarını önlemek için yapılmıştır. Sonuç olarak, çok çekirdekli işlemcilerde bile thread'ler Python kodunu paralel olarak çalıştıramaz; bunun yerine, tek bir çekirdek üzerinde hızlı bir şekilde yürütme değiştirir, bu da CPU ile yoğun olan çoklu thread'lerin verimsiz olmasına neden olur.

import threading import multiprocessing import time def cpu_intensive_task(n): """Saf Python CPU yoğun işlem""" count = 0 for i in range(n): count += i ** 2 return count # Thread sınırlamasını gösterme start = time.time() threads = [threading.Thread(target=cpu_intensive_task, args=(5_000_000,)) for _ in range(4)] for t in threads: t.start() for t in threads: t.join() print(f"Threading süresi: {time.time() - start:.2f}s") # Çıktı, GIL rekabeti nedeniyle ~4 kat tek thread zamanını gösterir start = time.time() processes = [multiprocessing.Process(target=cpu_intensive_task, args=(5_000_000,)) for _ in range(4)] for p in processes: p.start() for p in processes: p.join() print(f"Multiprocessing süresi: {time.time() - start:.2f}s") # Çıktı, ~1 kat tek thread süresini (4 kat hızlanma) gösterir

Hayattan bir durum

Sorun: Analitik platformumuzun karmaşık regex çıkarımları ve istatistik hesaplamaları ile 10GB log dosyasını işlemesi gerekiyordu. Mühendislik ekibi, 16 çekirdekli bir sunucuda concurrent.futures.ThreadPoolExecutor kullanarak 16 thread içeren bir threading tabanlı işçi havuzu uyguladı. Şaşırtıcı bir şekilde, CPU kullanımı %6-7'de (bir çekirdek) kaldı ve işlem süresi 3 saat sürdü, oysa sıralı işlem süresi 45 dakikaydı. GIL, sıralı yürütmeyi zorunlu kılıyor ve thread değiştirme yükünü ekliyordu.

Çözüm 1: C uzantıları ile optimize edilmiş threading Ağır hesaplamaları NumPy işlemlerine aktararak ve yürütme sırasında GIL'i serbest bırakan C hızlandırıcı kütüphaneleri kullanmayı değerlendirdik.

Artıları: En az kod değişikliği gerektirir; paylaşılan bellek serileştirme maliyetlerini ortadan kaldırır; thread'ler adres alanını paylaştığı için daha düşük bellek ayak izi.

Eksileri: NumPy tarafından desteklenen işlemlerle sınırlıdır; özel algoritmalar hala Python bytecode yürütmeyi gerektirir; C uzantı etkileşimlerini hata ayıklamak karmaşıklık ekler.

Çözüm 2: multiprocessing ile süreç tabanlı paralellik multiprocessing.Pool veya concurrent.futures.ProcessPoolExecutor'e geçiş yapmayı değerlendirerek, ayrı Python yorumlayıcıları başlattık.

Artıları: Tüm CPU çekirdeklerini kullanan gerçek paralellik; CPU ile yoğun görevler için doğrusal ölçeklenebilirlik; izolasyon GIL rekabetini tamamen önler.

Eksileri: Daha yüksek bellek kullanımı (her işlem ayrı Python yorumlayıcısını ~50-100MB yükler); süreçler arası iletişim için verilerin serileştirilmesi/gönderilmesi gereklidir; süreç başlatma gecikmesi yükü.

Seçilen Çözüm: multiprocessing ile chunked veri işleme seçtik. Loglar 16 segmenti bölündü, ProcessPoolExecutor tarafından işlendi ve sonuçlar birleştirildi. Chunking stratejisi, IPC sıklığını azaltarak pickle yükünü minimize etti.

Sonuç: İşlem süresi 3 saatten 4 dakikaya düştü (45 kat hızlanma). CPU kullanımı tüm 16 çekirdek boyunca %98'e ulaştı. Bellek kullanımı, işlem başına 800MB arttı (toplam 12.8GB), bu da 128GB sunucumuzda kabul edilebilir bir durumdu. Birden fazla parti işi arasında başlangıç maliyetlerini amorti etmek için bir süreç havuzu singleton'ı uyguladık.

Adayların sıkça kaçırdığı noktalar


Neden GIL I/O yoğun threading performansını etkilemez?

Birçok aday, thread'lerin Python'da tamamen işe yaramaz olduğunu yanlışlıkla düşünmektedir. GIL, I/O işlemleri sırasında (ağ isteği, disk okuma, veritabanı sorguları) ve açıkça serbest bırakan C uzantıları çağrıldığında (örneğin, NumPy matris işlemleri gibi) serbest bırakılır. Bir thread I/O için engellendiğinde, diğer thread'ler Python kodunu yürütme yapabilir. Böylece, threading, web kazıma veya asyncio tabanlı sunucularda binlerce eşzamanlı bağlantıyı işleme gibi eşzamanlı I/O işlemleri için son derece etkili olmaya devam eder.


Farklı Python uygulamaları, örneğin PyPy veya Jython, bir GIL'ye sahip mi?

Adaylar genellikle GIL'nin kaldırılmasının sadece farklı bir yorumlayıcı kullanmakla ilgili olduğunu varsayıyor. PyPy (JIT derlemeli Python) de thread güvenliğini sürdürmek için bir GIL uygular, ancak farklı nesne modeli thread değiştirmeyi daha verimli hale getirebilir. Ancak, Jython (JVM üzerinde çalışan) ve IronPython (.NET CLR üzerinde çalışan) bir GIL'ye sahip değildir çünkü temel sanal makinenin bellek yönetimi ve thread ile ilgili temel bileşenlerine dayanırlar, böylece JVM thread'lerinde gerçek thread düzeyinde paralellik sağlarlar.


Yeni süreçler başlatmadan GIL'yi manuel olarak serbest bırakabilir misiniz?

Birçok geliştirici, C uzantılarında GIL yönetimi hakkında bilgi sahibi değildir. Cython veya C kodu yazarken, uzun süreli hesaplamaların etrafında Py_BEGIN_ALLOW_THREADS ve Py_END_ALLOW_THREADS makrolarını kullanarak GIL'yi açıkça serbest bırakabilirsiniz. Ayrıca, Python 3.12+ ile her bir yorumlayıcı için ayrı GIL'yi (PEP 684) tanıtan bir yapı getirildi. Bu, bir süreçte ayrı GIL'ye sahip alt yorumlayıcılara olanak tanır, fakat bu, deneysel interpreters modülünü gerektirir ve doğrudan yorumlayıcılar arasında nesneleri paylaşmaz.