Unity’de C# Async/Await ile Asenkron Programlama Sanatı

Unity ve C# projelerinizde async/await kullanarak UI donmalarını engelleyin, uzun süreli işlemleri yönetin ve oyun performansını artırın. Detaylı rehber ve ipuçları burada.

Oyun geliştirme süreçlerinde, özellikle Unity gibi platformlarda, uygulamaların kullanıcı arayüzünün (UI) donmaması ve genel performansın korunması kritik öneme sahiptir. Uzun süren işlemler (dosya okuma/yazma, ağ istekleri, karmaşık hesaplamalar) ana iş parçacığını (main thread) bloke ederek uygulamanın yanıt vermemesine neden olabilir. İşte tam bu noktada C# async await anahtar kelimeleriyle asenkron programlama devreye girer. Bu makalede, C# async await yapısının temellerini, Unity projelerinizde nasıl etkin bir şekilde kullanabileceğinizi, yaygın hataları ve performans ipuçlarını detaylı bir şekilde inceleyeceğiz.

Asenkron Programlamaya Giriş: Neden İhtiyaç Duyarız?

Geleneksel olarak, kod satırları yukarıdan aşağıya doğru sırayla çalışır. Bu senkron yaklaşım, bir işlemin tamamlanması için diğerinin bitmesini beklediği anlamına gelir. Eğer bu işlemlerden biri uzun sürerse, tüm uygulama kilitlenir ve kullanıcıya donmuş bir deneyim sunulur. Örneğin, bir oyun sunucudan büyük bir veri paketi indirirken, oyunun arayüzü, karakter hareketleri veya animasyonlar durabilir.

Asenkron programlama ise, uzun süreli işlemleri ana iş parçacığından ayırarak, bu işlemlerin arka planda çalışmasına olanak tanır. İşlem devam ederken ana iş parçacığı serbest kalır ve kullanıcı arayüzü yanıt vermeye devam eder. İşlem tamamlandığında, sonuç ana iş parçacığına geri döndürülür. C# dilinde bu yapıyı kolaylaştıran en güçlü araçlar async ve await anahtar kelimeleridir. Bu ikili, karmaşık iş parçacığı yönetimi yerine daha okunabilir ve yönetilebilir bir kod yazmamızı sağlar.

C# Async/Await Temelleri

Asenkron programlamanın kalbinde async ve await anahtar kelimeleri yatar. Bu ikili, bir görevi başlatıp beklerken ana iş parçacığını serbest bırakma mekanizmasını sağlar.

async Anahtar Kelimesi

Bir metodun asenkron olduğunu ve içinde await anahtar kelimesini kullanabileceğini belirtir. async metotlar genellikle Task veya Task<TResult> döndürür. Eğer metot bir değer döndürmüyorsa, Task döndürmelidir. async void kullanımı özel durumlar dışında (örneğin olay yöneticileri) tavsiye edilmez, çünkü hata yönetimi zorlaşır.

await Anahtar Kelimesi

Asenkron bir işlemin (genellikle bir Task) tamamlanmasını beklerken kontrolü çağırana geri döndürür. İşlem tamamlandığında, kontrol kaldığı yerden devam eder. Bu, ana iş parçacığının bloke olmamasını sağlar. await sadece async olarak işaretlenmiş metotların içinde kullanılabilir.

Task ve Task<T>

Task sınıfı, asenkron bir işlemin tamamlanmasını temsil eder. Bir değer döndürmeyen asenkron işlemleri temsil eder. Task<TResult> ise, belirtilen türde (TResult) bir değer döndüren asenkron işlemleri temsil eder. Bunlar, asenkron kodunuzun sonucunu veya durumunu izlemenizi sağlar.

Basit bir C# async await örneği:

using System; 
using System.Threading.Tasks;
using UnityEngine;

public class AsyncExample : MonoBehaviour
{
    void Start()
    {
        Debug.Log("İşlem Başladı (Start)");
        SimulateLongRunningTask();
        Debug.Log("İşlem Başlatıldı, Devam Ediyorum (Start Son)");
    }

    async void SimulateLongRunningTask()
    {
        Debug.Log("Uzun süreli işlem başladı...");
        await Task.Delay(3000); // 3 saniye bekle
        Debug.Log("Uzun süreli işlem bitti!");
    }
}

Bu örnekte, SimulateLongRunningTask metodu çağrıldığında, Debug.Log("Uzun süreli işlem başladı...") yazdırılır, ardından await Task.Delay(3000) ile 3 saniye beklenir. Bu bekleme sırasında Start metodu yoluna devam eder ve Debug.Log("İşlem Başlatıldı, Devam Ediyorum (Start Son)") yazdırılır. 3 saniye sonra, SimulateLongRunningTask metodu kaldığı yerden devam eder ve Debug.Log("Uzun süreli işlem bitti!") yazdırılır. Bu, ana iş parçacığının bloke olmadığını gösterir.

Orta Seviye Async/Await Kullanımı

ConfigureAwait(false) ve Unity

await ifadesi varsayılan olarak, asenkron işlem tamamlandığında kodun orijinal bağlama (synchronization context) geri dönmesini sağlamaya çalışır. Unity’de bu genellikle ana iş parçacığıdır. Ancak bu bağlama geri dönmek her zaman gerekli değildir ve bazen performans düşüşüne yol açabilir. .ConfigureAwait(false) kullanarak, await‘in orijinal bağlama geri dönme zorunluluğunu ortadan kaldırabilirsiniz. Bu, özellikle arka plan iş parçacığında devam edebilecek ve UI ile etkileşime girmeyen işlemler için faydalıdır. Unity’de ana iş parçacığına geri dönmeniz gerektiğinde (örneğin, bir GameObject’in transform’unu değiştirmek gibi), ConfigureAwait(false) kullanmayın veya UnitySynchronizationContext‘i kullanarak açıkça ana iş parçacığına geçin.

Hata Yönetimi

Asenkron metotlardaki hatalar, senkron metotlardaki gibi try-catch blokları ile yakalanabilir. Eğer bir Task içinde bir hata oluşursa, bu hata await edildiği noktada fırlatılır:

async Task PerformOperationAsync()
{
    try
    {
        await SomeFailingTask();
    }
    catch (Exception ex)
    {
        Debug.LogError($"Hata oluştu: {ex.Message}");
    }
}

Birden Fazla Asenkron İşlem: WhenAll ve WhenAny

  • Task.WhenAll(task1, task2, ...): Tüm belirtilen Task‘lerin tamamlanmasını bekler. Eğer herhangi biri başarısız olursa, bir AggregateException fırlatılır.
  • Task.WhenAny(task1, task2, ...): Belirtilen Task‘lerden herhangi birinin tamamlanmasını bekler. Hangisinin tamamlandığını döndürür.

İşlemleri İptal Etme: CancellationTokenSource

Uzun süreli asenkron işlemlerin belirli bir zamanda iptal edilebilmesi önemlidir. CancellationTokenSource ve CancellationToken bu mekanizmayı sağlar. Bir CancellationTokenSource oluşturup, onun Token‘ını asenkron metodunuza geçirirsiniz. Metot içinde belirli aralıklarla veya işlem adımlarında token’ın iptal edilip edilmediğini kontrol eder ve iptal edildiyse işlemi durdurursunuz:

async Task LongRunningProcess(CancellationToken token)
{
    for (int i = 0; i < 100; i++)
    {
        token.ThrowIfCancellationRequested();
        // Uzun süreli işlem adımı...
        await Task.Delay(100, token);
    }
}

void Start()
{
    CancellationTokenSource cts = new CancellationTokenSource();
    // 5 saniye sonra iptal et
    cts.CancelAfter(5000);
    try
    {
        // Bu metodu çağırırken cts.Token'ı kullanıyoruz
        LongRunningProcess(cts.Token);
    }
    catch (OperationCanceledException)
    {
        Debug.Log("İşlem iptal edildi.");
    }
}

Unity’de Pratik Async/Await İpuçları

İpucu 1: Uzun Süreli Dosya İşlemleri

Büyük dosyaları okumak veya yazmak, özellikle mobil platformlarda, ana iş parçacığını kolayca bloke edebilir. C# async await kullanarak bu işlemleri arka plana taşıyabilirsiniz:

using System.IO;
using System.Threading.Tasks;
using UnityEngine;

public class FileOps : MonoBehaviour
{
    async void Start()
    {
        await WriteAndReadAsync("mydata.txt", "Hello Async World!");
    }

    async Task WriteAndReadAsync(string filename, string content)
    {
        string path = Path.Combine(Application.persistentDataPath, filename);
        Debug.Log($"Yazma işlemi başlıyor: {path}");
        await File.WriteAllTextAsync(path, content);
        Debug.Log("Yazma işlemi bitti.");

        Debug.Log("Okuma işlemi başlıyor...");
        string readContent = await File.ReadAllTextAsync(path);
        Debug.Log($"Okunan içerik: {readContent}");
        Debug.Log("Okuma işlemi bitti.");
    }
}

İpucu 2: Web İstekleri ve API Çağrıları

Bir sunucudan veri çekmek veya göndermek genellikle ağ gecikmeleri nedeniyle uzun sürebilir. Unity’nin UnityWebRequest sınıfı asenkron işlemleri destekler. C# async await ile bu işlemleri daha temiz bir şekilde yönetebilirsiniz:

using System.Net.Http;
using System.Threading.Tasks;
using UnityEngine;

public class WebRequestExample : MonoBehaviour
{
    private readonly HttpClient _httpClient = new HttpClient();

    async void Start()
    {
        string url = "https://jsonplaceholder.typicode.com/todos/1";
        string result = await GetRequestAsync(url);
        Debug.Log($"API'den gelen veri: {result}");
    }

    async Task<string> GetRequestAsync(string url)
    {
        Debug.Log($"Web isteği başlıyor: {url}");
        try
        {
            HttpResponseMessage response = await _httpClient.GetAsync(url);
            response.EnsureSuccessStatusCode(); // Hata durumunda exception fırlatır
            string responseBody = await response.Content.ReadAsStringAsync();
            Debug.Log("Web isteği tamamlandı.");
            return responseBody;
        }
        catch (HttpRequestException e)
        {
            Debug.LogError($"Web isteği hatası: {e.Message}");
            return null;
        }
    }
}

İpucu 3: Akıcı Kullanıcı Arayüzü İçin Ağ İşlemleri

Oyun içi mağaza verilerini çekme, kullanıcı profillerini güncelleme gibi ağa bağımlı işlemler sırasında kullanıcı arayüzünün donmaması için C# async await kullanmak hayati önem taşır. Bu sayede kullanıcı, veriler yüklenirken bile menülerde gezinebilir veya geri bildirim alabilir.

Yaygın Hatalar ve Çözümleri

async void Kullanımı

async void metotlar, içinde fırlatılan istisnaların yakalanmasını zorlaştırır ve genellikle uygulamanın çökmesine neden olabilir. Ayrıca, bu metotların ne zaman tamamlandığını izlemek mümkün değildir. Genellikle sadece olay yöneticileri için kullanılmalıdır. Diğer durumlarda async Task kullanın.

await Unutmak

Eğer bir async metot çağırır ancak sonucunu await etmezseniz, metot asenkron olarak çalışmaya başlar ancak ana iş parçacığı onun tamamlanmasını beklemez. Bu, beklediğiniz sıranın bozulmasına veya race condition’lara yol açabilir. Her zaman asenkron bir işlemi beklediğinizden emin olun.

UI Güncellemeleri ve Thread Sorunları

Unity’de UI elemanları veya GameObject’lerin transform bilgileri gibi bileşenler sadece ana iş parçacığında güncellenebilir. Arka plan iş parçacığında çalışan bir Task‘ten doğrudan bu elemanlara erişmeye çalışmak hataya neden olur. Bu durumda, işlemi tamamladıktan sonra ana iş parçacığına geri dönmek için await ifadesini kullanın (varsayılan olarak geri döner) veya UnitySynchronizationContext‘i kullanarak açıkça ana iş parçacığına geçin. ConfigureAwait(false) kullandıysanız, UI güncellemesi yapmadan önce ana iş parçacığına dönmeniz gerekir.

Deadlock Tuzakları

Asenkron metotların sonucunu senkron bir şekilde almaya çalışmak (örneğin Task.Result veya Task.Wait() kullanmak), özellikle UI bağlamı olan yerlerde (Unity ana iş parçacığı gibi) deadlock’lara neden olabilir. Çünkü Task.Result, asenkron işlemin tamamlanmasını beklerken çağrı yapan iş parçacığını bloke eder. Eğer asenkron işlem tamamlanmak için ana iş parçacığına geri dönmesi gerekiyorsa (ki Unity’de sıkça böyledir), ancak ana iş parçacığı Task.Result yüzünden bloke olmuşsa, bir deadlock oluşur. Bu tür durumları önlemek için her zaman await kullanın ve asenkron kodunuzu baştan sona asenkron tutun.

Performans ve Optimizasyon Notları

  • Gereksiz Kullanımdan Kaçının: Çok kısa süren veya zaten hızlı olan işlemler için C# async await kullanmaktan kaçının. Asenkronizasyonun kendi ek yükü vardır. Faydası, işlemin ana iş parçacığını bloke edeceği durumlar için ortaya çıkar.
  • ConfigureAwait(false) Kullanımı: Eğer asenkron işleminizden sonra ana iş parçacığına geri dönmeniz gerekmiyorsa (yani UI veya Unity API’leriyle etkileşime girmeyecekseniz), .ConfigureAwait(false) kullanmak gereksiz iş parçacığı geçişlerini önleyerek performansı artırabilir.
  • Task Havuzlama (Pooling): Sürekli yeni Task nesneleri oluşturmak yerine, özellikle çok sık çağrılan asenkron metotlar için Task havuzlama tekniklerini araştırabilirsiniz. Ancak bu genellikle daha gelişmiş senaryolar içindir ve çoğu Unity projesi için gerekli değildir.

C# async await, Unity’de modern, duyarlı ve yüksek performanslı oyunlar geliştirmek için vazgeçilmez bir araçtır. Doğru kullanıldığında, oyunlarınızın akıcılığını ve kullanıcı deneyimini önemli ölçüde artırabilir. Bu yapıları anlamak ve doğru uygulamak, daha sağlam ve yönetilebilir bir kod tabanı oluşturmanıza yardımcı olacaktır.

Leave a Reply

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir