Unity Eğitim
Dersler
Forum Sınav Merkezi Premium 💎
C# Dersleri

Unity İş Sistemi: JobHandle.Schedule() Derinlemesine

Paylaşan: Unity Eğitim 10 October 2025 10 dakika okuma 49 görüntülenme

Unity oyun geliştirme sürecinde performans, özellikle karmaşık simülasyonlar veya büyük dünya etkileşimleri söz konusu olduğunda kritik bir faktördür. Geleneksel C# programlamada, çoğu işlem ana iş parçacığında (main thread) gerçekleşir ve bu da kare hızlarında düşüşlere neden olabilir. İşte tam bu noktada Unity'nin C# İş Sistemi (C# Job System) ve onun kalbindeki JobHandle.Schedule() metodu devreye girer. Bu makalede, Unity İş Sistemi'nin ne olduğunu, JobHandle.Schedule()'ın nasıl çalıştığını ve uygulamalarınızda performansı artırmak için nasıl kullanabileceğinizi detaylı bir şekilde inceleyeceğiz.

Unity İş Sistemi'ne Giriş

Unity'nin C# İş Sistemi, geliştiricilere çoklu çekirdek işlemcilerin gücünü güvenli ve verimli bir şekilde kullanma imkanı sunar. Geleneksel çoklu iş parçacığı (multi-threading) programlama karmaşık ve hata eğilimli olabilirken, İş Sistemi bu süreci basitleştirir. Temelde, CPU'da ağır yük oluşturan işleri, ana iş parçacığından ayırarak birden fazla çekirdek üzerinde paralel olarak çalıştırmanıza olanak tanır. Bu sayede ana iş parçacığı (genellikle oyun mantığı, input ve render işlemlerini yürüten yer) takılmadan çalışmaya devam eder ve genel oyun akıcılığı artar.

İş Sistemi'nin temel yapı taşlarından biri IJob arayüzüdür. Bu arayüzü uygulayan her struct, bir iş olarak tanımlanabilir ve bir işçi iş parçacığı (worker thread) üzerinde çalıştırılabilir. Ancak bir işin gerçekten çalıştırılabilmesi için zamanlanması gerekir. İşte burada JobHandle.Schedule() metodu devreye girer.

JobHandle.Schedule() Nedir ve Nasıl Çalışır?

JobHandle.Schedule(), bir IJob'ı veya IJobParallelFor'ı Unity'nin İş Sistemi'ne kaydederek, uygun bir işçi iş parçacığında çalıştırılmak üzere sıraya koyan metottur. Bu metodun çağrılması, işin hemen yürütülmeye başlandığı anlamına gelmez; yalnızca sistem tarafından yönetilmek üzere bir kuyruğa alınır. İş, sistemin uygun gördüğü bir zamanda, genellikle bir sonraki karede veya mevcut bir işçi iş parçacığı boşaldığında çalışmaya başlar.

Schedule() metodu genellikle iki ana parametre alır:

  1. int innerloopBatchCount: Bu parametre, işin kaç elemanlık gruplar halinde işçi iş parçacıklarına dağıtılacağını belirler. Özellikle IJobParallelFor gibi paralel işlerde önemlidir. Doğru bir batchCount seçimi, iş parçacıkları arasında yük dengelemesini (load balancing) ve önbellek kullanımını optimize edebilir. Çok küçük bir değer, iş parçacığı yönetim overhead'ini artırabilir; çok büyük bir değer ise bazı iş parçacıklarının diğerlerinden daha geç bitirmesine neden olabilir.
  2. JobHandle inputDependencies: Bu parametre, zamanladığınız işin, başka bir işin tamamlanmasını beklemesi gerektiğini belirtir. İş Sistemi, bu bağımlılıkları otomatik olarak yönetir ve bağımlı işler tamamlanmadan mevcut işi çalıştırmaz. Bu, işlerin doğru sırada yürütülmesini ve veri yarışlarının (data races) önlenmesini sağlar. Birden fazla bağımlılık için JobHandle.CombineDependencies() kullanılabilir.

Schedule() metodu bir JobHandle nesnesi döndürür. Bu JobHandle, zamanlanan işin benzersiz bir kimliğidir ve işin durumu hakkında bilgi almak (IsCompleted) veya işin tamamlanmasını beklemek (Complete()) için kullanılır. Bu handle, aynı zamanda diğer işler için bir bağımlılık olarak da hizmet edebilir.

Native Container'lar ve Veri Güvenliği

Unity İş Sistemi ile çalışırken, işler içinde yalnızca değer tipleri (int, float, struct vb.) ve özel "Native Container" tipleri (NativeArray<T>, NativeList<T>, NativeHashMap<TKey, TValue> vb.) kullanılmalıdır. Bu Native Container'lar, yönetilmeyen (unmanaged) bellekte depolanır ve iş parçacıkları arasında güvenli ve hızlı veri paylaşımı sağlar. Managed (yönetilen) bellek nesnelerine (örneğin, GameObject, Transform, List<T>) işler içinden doğrudan erişim yasaktır ve genellikle çalışma zamanı hatalarına yol açar.

Native Container'lar manuel bellek yönetimi gerektirir; yani işiniz bittiğinde Dispose() metotlarını çağırmalısınız. Aksi takdirde bellek sızıntıları yaşanabilir. Genellikle, işin tamamlandığından emin olduktan sonra (JobHandle.Complete() çağrısı sonrası) veya MonoBehaviour'ın OnDestroy() metodunda Dispose() çağrısı yapılır.

İş Bağımlılıkları ve Zincirleme

Karmaşık uygulamalarda, bir işin çıktısının başka bir işin girdisi olması gerekebilir. Bu senaryolarda iş bağımlılıkları hayati öneme sahiptir. JobHandle.Schedule() metodunun ikinci parametresi olan inputDependencies sayesinde işleri zincirleyebiliriz. Örneğin:

JobHandle firstJobHandle = firstJob.Schedule();
JobHandle secondJobHandle = secondJob.Schedule(firstJobHandle);
JobHandle thirdJobHandle = thirdJob.Schedule(secondJobHandle);

Bu örnekte, secondJob, firstJob bitmeden başlamayacak; thirdJob ise secondJob bitmeden başlamayacaktır. Bu, işlerin doğru sırada ve güvenli bir şekilde çalışmasını garanti eder.

Birden fazla işin aynı anda tamamlanmasını beklemek için JobHandle.CombineDependencies() metodunu kullanabilirsiniz:

JobHandle combinedHandle = JobHandle.CombineDependencies(handle1, handle2, handle3);
finalJob.Schedule(combinedHandle);

JobHandle.Complete() metodu, ana iş parçacığını çağrılan işin tamamlanana kadar bekletir. Bu, işin sonucuna ana iş parçacığından hemen ihtiyacınız olduğunda kullanışlıdır, ancak performansı düşürebilir. Mümkün olduğunca bu çağrıdan kaçınmak veya işleri bağımlılıklar üzerinden yönetmek daha iyidir.

Pratik İpuçları

  1. Burst Compiler Kullanımı: Unity İş Sistemi ile birlikte Burst Compiler'ı kullanmak, işlerinizin performansını katlayabilir. İş struct'larınızın üzerine [BurstCompile] niteliğini ekleyerek, Unity'nin kodunuzu yüksek performanslı makine koduna dönüştürmesini sağlayın. Bu, özellikle matematiksel yoğunluklu işlerde büyük fark yaratır.
  2. Doğru Batch Boyutu (innerloopBatchCount) Ayarı: IJobParallelFor gibi işlerde, batchCount doğru ayarlanmalıdır. Genellikle, CPU'nuzun çekirdek sayısının bir katı veya 32-128 gibi değerler iyi bir başlangıç noktası olabilir. Deneme yanılma ile en iyi değeri bulmaya çalışın. Çok küçük batch'ler overhead yaratır, çok büyük batch'ler ise iş parçacığı dengelemesini bozabilir.
  3. Native Container'ları Doğru Yönetin: Her zaman NativeArray, NativeList gibi yapıları işiniz bittiğinde Dispose() etmeyi unutmayın. Bunu genellikle JobHandle.Complete() çağrısından sonra veya MonoBehaviour'ın OnDestroy() metodunda yapmalısınız. Aksi takdirde bellek sızıntıları yaşanacaktır.
  4. Gereksiz JobHandle.Complete() Çağrılarından Kaçının: Complete() metodu ana iş parçacığını bloke eder. Mümkün olduğunca işleri bağımlılıklar üzerinden zincirleyerek yönetin ve Complete()'ı yalnızca işin sonucuna ana iş parçacığında acilen ihtiyacınız olduğunda çağırın. İşin tamamlanıp tamamlanmadığını kontrol etmek için JobHandle.IsCompleted özelliğini kullanabilirsiniz.

Yaygın Hatalar ve Çözümleri

  1. NativeContainer'ları Dispose Etmemek: Bu, bellek sızıntılarının bir numaralı nedenidir. Çalışma zamanında oyununuzun belleği şişer ve performans düşüşleri yaşanır. Çözüm: İşin JobHandle.Complete() çağrısından sonra veya MonoBehaviour'ın OnDestroy() metodunda NativeContainer.Dispose() çağrısını yapmayı alışkanlık haline getirin.
  2. Ana İş Parçacığını Bloke Etmek: JobHandle.Complete() metodunu her iş zamanlamasından hemen sonra çağırmak, çoklu iş parçacığı kullanmanın faydasını ortadan kaldırır. İşler paralel çalışmak yerine seri hale gelir. Çözüm: İşleri mümkün olduğunca zincirleyin ve Complete() çağrısını oyun mantığınızın en geç ihtiyaç duyduğu noktaya erteleyin.
  3. Managed Bellek Erişimi: İşler içinde GameObject, Transform, List<T> gibi referanslara erişmeye çalışmak çalışma zamanı hatalarına yol açar. Unity İş Sistemi, güvenliği sağlamak için bu tür erişimleri engeller. Çözüm: İşlere sadece değer tipleri ve Native Container'lar aracılığıyla veri aktarın. Gerekli tüm verileri işin Execute() metoduna girmeden önce Native Container'lara kopyalayın.
  4. Yanlış Bağımlılık Yönetimi: İşlerin yanlış sırada çalışması veya birden fazla işin aynı veriye aynı anda yazmaya çalışması (veri yarışları) beklenmedik sonuçlara veya hatalara yol açabilir. Çözüm: JobHandle parametrelerini doğru kullanarak işler arasındaki bağımlılıkları açıkça tanımlayın. Gerekirse JobHandle.CombineDependencies() kullanın.

Performans ve Optimizasyon Notları

Unity İş Sistemi'nin ana amacı performanstır. Bu sistemi kullanırken maksimum verim almak için şunları göz önünde bulundurun:

  • Burst Compiler: İşlerinizin üzerine [BurstCompile] eklemeyi asla unutmayın. Bu, performansı dramatik şekilde artırır.
  • Native Container'lar: Sadece Native Container'ları kullanarak bellek yönetimini optimize edin ve işler arası güvenli veri transferi sağlayın.
  • Minimal Complete() Çağrıları: JobHandle.Complete()'ı mümkün olduğunca az kullanın ve çağrıları ana iş parçacığı üzerinde en az etki yaratacak şekilde planlayın.
  • Uygun batchCount: İş yükünü iş parçacıkları arasında eşit ve verimli bir şekilde dağıtmak için innerloopBatchCount değerini dikkatlice seçin.
  • İşleri Küçük ve Bağımsız Tutun: Mümkün olduğunca küçük, odaklanmış ve birbirinden bağımsız işler oluşturmaya çalışın. Bu, İş Sistemi'nin işleri daha verimli bir şekilde dağıtmasına olanak tanır.

Örnek Kod: Basit Bir İş Zamanlama

Aşağıdaki örnek, basit bir toplama işlemini bir iş olarak nasıl tanımlayacağınızı ve JobHandle.Schedule() ile nasıl zamanlayacağınızı gösterir.

using Unity.Collections;
using Unity.Jobs;
using UnityEngine;
using Unity.Burst; // Burst Compiler için

// Burst Compile niteliği ile performansı artırıyoruz
[BurstCompile]
public struct ToplamaIşi : IJob
{
    public float sayı1;
    public float sayı2;
    // Sonucu depolamak için NativeArray kullanıyoruz
    public NativeArray<float> sonuç;

    public void Execute()
    {
        sonuç[0] = sayı1 + sayı2;
    }
}

public class İşZamanlayıcıÖrneği : MonoBehaviour
{
    void Start()
    {
        // NativeArray'ı oluştur. Allocator.TempJob, iş tamamlandığında otomatik olarak dispose edilmesini önerir.
        // Ancak biz manuel kontrol için TempJob kullandık ve Complete sonrası dispose edeceğiz.
        NativeArray<float> işSonucu = new NativeArray<float>(1, Allocator.TempJob);

        // İş struct'ını oluştur ve verileri ata
        ToplamaIşi iş = new ToplamaIşi
        {
            sayı1 = 15.5f,
            sayı2 = 25.0f,
            sonuç = işSonucu
        };

        // İşlemci çekirdeklerinde çalışması için işi zamanla.
        // İkinci parametre (inputDependencies) şu an için boş (JobHandle.Empty) çünkü bağımlılık yok.
        JobHandle işKolu = iş.Schedule();

        // İşin tamamlanmasını bekle ve sonucu al (bu ana thread'i bloke eder).
        // Gerçek uygulamalarda bu çağrıyı mümkün olduğunca ertelemeye çalışın.
        işKolu.Complete();

        Debug.Log("Toplama İşinin Sonucu: " + işSonucu[0]);

        // NativeArray'ı dispose etmeyi unutma!
        işSonucu.Dispose();
    }
}

Bu örnek, JobHandle.Schedule()'ın temel kullanımını göstermektedir. Daha karmaşık senaryolarda, birden fazla işi zincirlemek ve IJobParallelFor ile büyük veri setlerini paralel işlemek için bu prensipleri genişletebilirsiniz. Unity İş Sistemi, oyunlarınızın performansını artırmak için güçlü bir araçtır ve JobHandle.Schedule() bu gücün kilidini açan anahtardır.

🧠 Ders Sonu Değerlendirme Testi

Dersi tamamladıktan sonra bilgilerinizi test edin ve ekstra puanlar kazanın.

🔥 +50 XP Ödül
🔒

Sınava Katılmak İçin Giriş Yapın

Bu ders sonu testini çözebilmek, bilginizi test edip **+50 XP** kazanmak ve **Sınav Şampiyonu** rozetinin kilidini açmak için üye girişi yapmalısınız.

Yorumlar (0)

Yorum yazabilmek ve derslere katkıda bulunabilmek için giriş yapmalısınız.

İlk yorumu siz yapın!