1. Giriş
Heç vaxt düşməyən proqramlar yazmaq IDE-nizin səhvlərdən qorxduğunu düşünmək kimidir. Əslində kod nə qədər mürəkkəbləşirsə (xüsusən çoxiprolu və asinxron mühitdə), səhvlər bir o qədər yaradıcı olur və daha incə yollarla ortaya çıxır. Threadlərdə və tasklarda istisnaları görməməzlikdən gəlməklə resurs sızmaları, donmalar, məlumat itkisi və ya işə düşəndən saatlarla sonra gözlənilməz çökmələr almaq asandır.
Bu mühazirədə biz C#-da çoxiprolu və asinxron proqramlaşdırmada xətaların emalına dair ən yaxşı təcrübələri bir yerə yığıcıq. Siz öyrənəcəksiniz necə düzgün istisnaları tutmaq, necə eyni vaxtda baş verən xətaların «yığını»nı (məsələn, AggregateException tərzində) parçalamaq, «unutturma» ilə işə salınmış tapşırıqlarla nə etməli və niyə istisnaları görməməzlikdən gəlmək təhlükəli və mənasızdır.
Niyə hər şey bu qədər sadə deyil
- Xəta baş verən thread try-catch yazdığınız thread-dən fərqli ola bilər.
- Asinxron tapşırıqlar istisnaları dərhal atmırlar — onlar «paketlənir» və await zamanı (və ya sinxron gözləmədə) emal olunmağı gözləyirlər.
- Bir neçə task ilə eyni vaxtda işləyərkən (məsələn, Task.WhenAll) bir neçə xəta yarana bilər — onları nəzərə almalısınız.
- fire-and-forget tipli əməliyyatlar istisnanı açıq handler olmadan tamamilə «itirə» bilər.
Bu xüsusiyyət — icra kontekstinin bölünməsi. Proqramı bir neçə arena olan sirk kimi təsəvvür edin: birində yanğın olsa, o dərhal digərində görünməyə bilər. Belə «yanğınları» düzgün izləməyi və söndürməyi bacarmaq vacibdir.
2. Tapşırıqlarda istisnalar: Task və Task<TResult>
Tapşırıqlar xətaları necə siqnal verir
Task-da emal olunmamış istisna baş verəndə, o dərhal «uçmur». Task Faulted olur və istisna içində saxlanılır. Onu almaq olar:
- await edərək (və ya task.Wait()/task.Result vasitəsilə — amma belə etmək yaxşı deyil);
- task.Exception xüsusiyyətini yoxlayaraq — orada AggregateException olacaq.
Nümunə
// Asinxron metodda xəta
async Task FailAsync()
{
await Task.Delay(100);
throw new InvalidOperationException("Nəsə düzgün getmədi");
}
async Task MainAsync()
{
try
{
await FailAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Xəta: {ex.Message}");
}
}
Əgər try-catch qoymasanız, proqram çökmüş olar. Əgər qoysanız — istisna düzgün tutulacaq, hətta xəta başqa task-da baş vermiş olsa belə.
AggregateException: xətalar kütləvi
await Task.WhenAll(tasks) zamanı bir neçə task-dan gələn xətlər bir AggregateException-də toplanır (onun InnerExceptions içində).
async Task MultiFailAsync()
{
Task t1 = Task.Run(() => throw new InvalidOperationException("Xəta 1"));
Task t2 = Task.Run(() => throw new ArgumentException("Xəta 2"));
try
{
await Task.WhenAll(t1, t2);
}
catch (Exception ex)
{
if (ex is AggregateException agg)
{
foreach (var e in agg.InnerExceptions)
Console.WriteLine($"İstisna: {e.Message}");
}
else
{
Console.WriteLine($"Tək xətə: {ex.Message}");
}
}
}
Diqqət: await Task.WhenAll(tasks) istifadə edildikdə .NET AggregateException-i «açar» və catch-də sizə «ilk» istisnanı verə bilər. Tam siyahı task.Exception.InnerExceptions vasitəsilə əlçatandır, əgər task xəta ilə bitibsə.
3. «Fire and Forget»: niyə sadəcə task-ı unutmaq olmaz
«Başlat və unut» tez-tez gizli uğursuzluqlara aparır. Nümunə:
Task.Run(() => { throw new Exception("Bum!"); }); // Xəta boşluğa "uçur".
Müasir runtime xətanı task-da saxlayır və emal olunmamış istisna səbəbindən prosesi bitirə bilər. Ən yaxşı yol — task-ın referansını saxlamaq və/və ya TaskScheduler.UnobservedTaskException hadisəsinə abunə olmaqdır.
Necə düzgün?
- Task-ı saxlayın ki, onun bitməsini gözləyib xətanı emal edəsiniz;
- fire‑and‑forget üçün handler-i birbaşa delegate daxilində qoyun.
Task.Run(() => {
try
{
// Xəta ata biləcək kod
}
catch (Exception ex)
{
// Loqla, bildiriş ver, amma xətanı xaricə buraxma
Console.WriteLine($"Fire-and-forget-də xəta: {ex.Message}");
}
});
4. Thread-lərdə xətalar: "tutmaq mümkün deyil?"
Yeni Thread-də olan istisnanı xaricdəki try-catch ilə tutmaq olmaz — yalnız thread-in bədəninin içində.
var thread = new Thread(() =>
{
try
{
throw new Exception("Thread-də xəta");
}
catch (Exception ex)
{
Console.WriteLine($"Thread daxilində xətanı tutduq: {ex.Message}");
}
});
thread.Start();
Əgər handler qoyulmazsa, istisna yalnız həmin thread-i bitirəcək (əgər o background-dirsə: thread.IsBackground = true). Non-background thread-lər üçün emal olunmamış istisna bütün prosesi dayandıra bilər. Həmişə thread daxilində try-catch qoyun.
Thread-dən nəticə və xətaları necə qaytarmaq?
- Nəticələri/xətaları ötürmək üçün queue/kolleksiyalardan istifadə edin;
- Event-based model;
- Daha yaxşısı task-lara keçin — onların xətalarını emal etmək daha rahatdır.
5. Paralel dövrlər: xətaları xüsusi şəkildə tuturuq
Paralel dövrlərdə müxtəlif budaqlardan gələn xətalar AggregateException-də toplanır.
try
{
Parallel.For(0, 5, i =>
{
if (i % 2 == 0)
throw new Exception($"İterasiya {i}-də xəta");
});
}
catch (AggregateException ex)
{
foreach (var e in ex.InnerExceptions)
Console.WriteLine($"[Paralel dövr] Xəta: {e.Message}");
}
Əgər lokal uğursuzluqları loglamaq və digər budaqları davam etdirmək lazımdırsa, hər budaqda daxili try-catch qoyun.
6. Task-ların ləğvi zamanı xətaların emalı
CancellationToken vasitəsilə ləğv olunduqda adət üzrə OperationCanceledException atılır — bu səhv deyil, normal dayandırılmadır.
async Task DoWorkAsync(CancellationToken token)
{
for (int i = 0; i < 10; i++)
{
token.ThrowIfCancellationRequested();
await Task.Delay(100);
}
}
// Haradasa kodda:
var cts = new CancellationTokenSource();
var task = DoWorkAsync(cts.Token);
cts.Cancel(); // Təqribən 200 ms sonra
try
{
await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("Əməliyyat ləğv olundu!");
}
ThrowIfCancellationRequested()-dən əlavə, token-i dəstəkləyən çoxlu metodlar (məsələn, Task.Delay, HttpClient.GetAsync) özləri OperationCanceledException atacaqlar. Ləğv dəstəyini yoxlayın.
7. Faydalı nüanslar
Hər vəziyyət üçün yanaşmalar
- Asinxron metodların yuxarı səviyyəsində try-catch qoyun — beləliklə «qaçmış» xətaları tutacaqsınız.
- Task-ları görməməzlikdən gəlməyin: referansları saxlayın, bitməsini gözləyin, xətaları loglayın.
- Paralel əməliyyatlar üçün (Task.WhenAll / Parallel.ForEach) AggregateException-i nəzərə alın.
- Ləğvi və uğursuzluqları ayırın: OperationCanceledException-i ayrıca tutun.
- Xətaları loglayın, xüsusən də tətbiqi qırmayan «səssiz» xətaları.
- Detalları saxlayın: bütün daxili istisnaları loglayın, yalnız ilkini yox.
- Xətaları «yerində» emal edin: nə edəcəyinizi bilmirsinizsə — ən azı loglayın.
Çoxiprolu və asinxron koddə xətanı harada tutmaq
flowchart TD
A[Main thread / UI] -->|Task işə salır| B[Task/async]
B -->|Task daxilində| C[try/catch asinxron metodun içində]
B -->|await əsas thread-də| D[await ətrafında try/catch]
A -->|Thread işə salır| E[Thread]
E -->|Daxildə| F[thread daxilində try/catch]
B -->|Çoxlu task| G[Task.WhenAll / Parallel.ForEach]
G -->|Xəta| H[AggregateException]
8. Tipik səhvlər və tələlər
Səhv №1: task-ı .Result və ya .Wait() ilə gözləmək. Deadlock və/və ya gözlənilməz AggregateException ola bilər.
Səhv №2: fire‑and‑forget-i daxili try-catch olmadan işə salmaq — task səssizcə düşə bilər, diaqnostika olmayacaq.
Səhv №3: paralel dövrlərdə itmiş xətalar — işin bir hissəsi icra olunmayıb, amma siz bunu görmürsünüz.
Səhv №4: ləğvi (OperationCanceledException) və real xətaları qarışdırmaq.
Səhv №5: bir neçə task arasında yalnız ilk xətanı loglamaq — digərləri «kölgədə» qalır.
Səhv №6: bütün tasklar üçün eyni Exception obyektindən yenidən istifadə etmək — hər xətanın öz nümunəsi olmalıdır.
Səhv №7: UI-thread-də istisnaların emalının olmaması — fon xətası gözə dəyməz, interfeys «ruhlu» davranar.
GO TO FULL VERSION