1. Parallel.For və Parallel.ForEach daxilində istisnaların işi
Adi for dövründə hər şey sadədir: dövrün bədənində istisna atılarsa — dövr dərhal dayandırılır və istisna xaricə ötürülür. Paralel dövrlərdə bu belə deyil. Gəlin aydınlaşdıraq.
Bütün istisnalar bir "çantada" yığılır
Paralel dövrün bir iterasiyasında (Parallel.For/ForEach) istisna yarananda, o dərhal xaricə ötürülmür, əksinə paketlənir. Proses davam edir: digər iterasiyalar ya tamamlanır, ya da onlar da istisna atırlar. Nəticə: paralel dövr icrasını yekunlaşdıranda (və ya məcburi dayandırılanda) bütün "atılan" istisnalar bir obyekt şəklində — AggregateException — xaricə verilir.
AggregateException — bu, paralel iterasiyalar zamanı baş vermiş bütün istisnələrin kolleksiyasını özündə saxlayan "konteyner"dir. Bu faydalıdır: biz həmişə BÜTÜN səhvləri əldə edirik (ya da ən azı əsas axınlar tamamlanana qədər toplanmışları).
Praktikada necə görünür
Nümunə: bəzən istisna atılan paralel emal
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
int[] numbers = { 1, 2, 0, 4, 0, 6, 7, 8 };
try
{
Parallel.ForEach(numbers, number =>
{
// Biz qəsdən bölürük, bəzən sıfıra bölünmə olur!
// Bu DivideByZeroException yaradacaq
int result = 100 / number;
Console.WriteLine($"100 / {number} = {result}");
});
}
catch (AggregateException ex)
{
Console.WriteLine("Parallel dövrdə səhvlər aşkarlanıb!");
// Baş vermiş bütün istisnələri keçirək
foreach (var inner in ex.InnerExceptions)
{
Console.WriteLine($"Tip: {inner.GetType().Name} — Mesaj: {inner.Message}");
}
}
}
}
Nə baş verəcək:
- Əsas kolleksiyada sıfırlar var, və sıfıra bölmə riyaziyyatda (və C#-da) qadağandır: DivideByZeroException yaranacaq.
- Paralel dövr emala başlayır. Haradasa sıfıra bölmə baş verən kimi — dövr dərhal dayanmayacaq, artıq başlamış bütün iterasiyalar davam edəcək.
- Bütün axınlar işini bitirəndən sonra xaricə bütün baş vermiş istisnələri ehtiva edən AggregateException atılacaq.
İstisna emal mexanikasını vizuallaşdıraq
flowchart LR
A[Thread 1]
B[Thread 2]
C[Thread 3]
D[Thread 4]
E[Parallel.ForEach]
F[Exception 1]
G[Exception 2]
H[AggregateException]
subgraph Iterations
A --> F
B --> G
C --> E
D --> E
F --> H
G --> H
E --> H
end
Şəkildə görünür: müxtəlif axınlar fərqli səhvlərlə qarşılaşa bilər və nəticədə hamısı bir AggregateException-ə paketlənir.
2. Səhvlərin praktiki xüsusiyyətləri
AggregateException ilə nə etmək lazımdır?
AggregateException-i ələ keçirəndə adətən iki ssenari olur:
- İstifadəçiyə (və ya loga) bütün səhvləri çıxarmaq, təcrübə toplamaq üçün.
- Hansı səhvin kritik, hansının önemsiz olduğunu anlamaq: bütün əməliyyatı uğursuz saymaq, yoxsa ayrı-ayrı uğursuzluqları gözardı etmək.
Tipik pattern: Handle ilə emal
try
{
Parallel.For(0, 10, i =>
{
if (i == 3 || i == 7)
throw new InvalidOperationException($"Səhv iterasiyada {i}");
Console.WriteLine($"Emal edildi: {i}");
});
}
catch (AggregateException ex)
{
ex.Handle(e =>
{
if (e is InvalidOperationException)
{
Console.WriteLine("Tutulan səhv: " + e.Message);
// true = səhv emal edilmiş sayılır
return true;
}
// false = emal olunmayıb, yenidən atılacaq
return false;
});
}
Belə yanaşma yalnız sizin "normal" hesab etdiyiniz səhvləri emal etməyə imkan verir, qalanlarını isə yuxarı ötürür ki, kritik nasazlıqları qaçırmayasınız.
Maraqlı (və təhlükəli) implementasiya nüansları
Dövr nə vaxt dayandırılır?
Iterasiyada istisna yarandıqda, Parallel.For/ForEach yeni iterasiyaları başlatmamağa çalışır, amma artıq başlamış olanlar işləməyə davam edir. Bütün aktiv iterasiyalar tamamlandıqdan sonra AggregateException atılır. Əgər axınlar çoxdursa, işin "qanadı" hələ də bitəcək — buna görə də bir neçə səhv ola bilər.
Əgər istisnaları tutmasaq, tətbiq çökəcək.
Əgər Parallel.For/ForEach-i try-catch blokuna almalısınız, əks halda tətbiq bütün iterasiyalar tamamlandıqdan sonra birinci rast gələn səhvdə qəzaya uğrayacaq — istifadəçi üçün xoş deyil.
İstisnanı dövrün "içinə" ötürmək.
Bəzən xüsusi yanaşma lazımdır. Məsələn, ayrı iterasiyaların bütün işi pozmasını istəmirsinizsə, istisnaları paralel dövrün bədəni daxilində emal edə bilərsiniz:
Parallel.ForEach(numbers, number =>
{
try
{
int result = 100 / number;
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine($"Sayı {number}-də səhv: {ex.Message}");
}
});
Belə üsul yaxşıdır əgər sizə bütün istisnaların "paket" şəklində lazım olmadığıdır — hər uğursuzluğu yerindəcə emal edirsiniz (məsələn, log yazırsınız). Amma diqqətli olun: bu halda heç bir AggregateException yaranmayacaq və ümumi vəziyyətin necə olduğu barədə məlumatınız olmayacaq.
Break() və Stop() çağırışları.
Əgər iterasiya ParallelLoopState.Break() və ya ParallelLoopState.Stop() çağırırsa, dövr yeni iterasiyaları dayandırmağa çalışır: Break() cari indeksdən sonrakıları dayandırmağa çalışır, Stop() isə bütün iterasiyaları. Lakin eyni zamanda istisna yaranırsa, o saxlanılır və bütün aktiv iterasiyalar tamamlandıqdan sonra AggregateException şəklində atılacaq.
3. Faydalı nüanslar
Adi dövrlərdə istisnalar vs paralel dövrlərdə
Adi dövrdə hər hansı səhv işin dərhal dayandırılmasına səbəb olur: istisna xaricə atılır və hər şey bloklanır.
Paralel dövrlərdə C# daha kompromissli yanaşma götürür: artıq başlamış tasklar işləməyə davam edir və yalnız bütün proses tamamlanandan sonra bütün səhvlər "birdəfəlik" xaricə çıxarılır. Bu, bütün səhvləri toplamağa və dövr bitdikdən sonra qərar verməyə imkan verir.
4. Parallel.For və Parallel.ForEach ilə işləyərkən tipik səhvlər
Səhv №1: AggregateException-i gözardı etmək.
Əgər AggregateException-i tutmazsanız, tətbiq bütün iterasiyalar tamamlandıqdan sonra qəza edəcək, bu da məlumat itkisinə və server/GUID tətbiqlərdə nasazlığa səbəb olacaq.
Səhv №2: .Wait()-i try-catch olmadan istifadə etmək.
.Wait()-in Parallel.For/ForEach üçün çağırışı AggregateException emalı olmadan idarə olunmayan istisnaya gətirib çıxaracaq və diaqnostikanı çətinləşdirəcək.
Səhv №3: təkrarlanan səhvləri nəzərə almamaq.
Müəyyən məlumatlarda (məsələn, sıfırlar) eyni səhvə dəfələrlə rast gəlinə bilər. InnerExceptions-i analiz etmədən problemin kökünü qaçırmaq olar.
Səhv №4: bütün istisnaları udmaq.
Dövür daxilində catch (Exception) { /* boş */ } istifadə etmək səhvləri gizlədir, bu da vacib məlumatın itirilməsinə və "gözlə görünməyən" buglara səbəb olur.
Səhvlərin müxtəlif dövrlərdə davranışı
| Variant | Adi for/foreach | Parallel.For / ForEach |
|---|---|---|
| İstisna emal olunur | Dərhal | Bütün iterasiyalar tamamlandıqdan sonra |
| Səhv formatı | Tək exception | Kolleksiyalı AggregateException |
| Digər iterasiyalar | İcra olunmur | Artıq başlamışlar tamamlanır |
| Daxildə səhvləri tutmaq | Bəli | Bəli |
| Çöldən səhvləri tutmaq | Bəli | Bəli, AggregateException vasitəsilə |
"Fikirlər" və qısa müsahibə sualları:
- Əgər AggregateException emal olunmazsa nə olacaq?
Tətbiq bütün iterasiyalar tamamlandıqdan sonra çökmüş olacaq — səhv hansı iterasiyada yarandığından asılı olmayaraq. - Hansı iterasiyada səhv yarandığını necə öyrənmək olar?
Yalnız əgər siz özü istisnaya indeks və ya məlumat haqda məlumat əlavə edərsinizsə. - AggregateException boş ola bilərmi?
Xeyr, o ən azı bir daxili istisna olduqda yaradılır. Əgər səhv yoxdursa, o atılmır. - Daxildə səhvi tutduqda onlar emal olunurmu?
Bəli, amma bu zaman xaricə heç nə atılmayacaq və AggregateException yaranmayacaq.
İndi siz təkcə çoxlu axınlı dövrləri işə salmağa hazır deyilsiniz, həm də onların paralel "qəzalarını" çevik idarə edə bilərsiniz! Və hər zaman diqqətli olun çoxaxınlılıqla: o sürprizləri sevir, xüsusən də onları heç kim tutmursa.
GO TO FULL VERSION