CodeGym /Kurslar /C# SELF /Hadisə əsaslı proqramlaşdırmanın optimizasiyası

Hadisə əsaslı proqramlaşdırmanın optimizasiyası

C# SELF
Səviyyə , Dərs
Mövcuddur

1. Giriş

Çox tipik tətbiqlərdə hadisələr sürətli və demək olar ki "pulsuz" işləyir — CLR (Common Language Runtime) onları emal etməyə yaxşı optimizə olunub. Amma tətbiq böyüdükcə, hadisələrin sayı artdıqca, abunəçilər zənciri uzandıqca və performans tələbləri artırsa, ortaya çıxır ki: hətta belə "sadə" konstruktsiya da boğazda darboğaz yarada bilər. Xüsusən real-time yenilənmələrin çox olduğu sistemlərdə, UI-larda və ya IoT tətbiqlərində yüz minlərlə sensor bildirişlərinin emalında bu çox hiss olunur.

Bu mühazirədə biz baxacağıq:

  • Hadisələr və delegatlar performansa necə təsir edir.
  • Harada dar məqamlar olur.
  • Hadisə kodunu necə sürətli yazmaq və performansa mane olan problemlərdən necə qaçmaq.

.NET-də hadisələrin daxili quruluşu

Artıq qeyd etdiyimiz kimi, hadisə delegatın üzərinə bükülmüşdür. Delegat — çağırılacaq metodların siyahısını (invocation list) saxlayan xüsusi obyektdir. Hər hadisə çağırışı zamanı CLR bu siyahını gəzib bütün metodları sinxron şəkildə çağırır. (Asinxronluq yalnız siz ora manual olaraq async kod əlavə etdikdə yaranır.)

Vizual sxem:


[Publisher] ----- (event) ---> [Delegate (Invocation List)] --> [Handler 1]
                                                           --> [Handler 2]
                                                           --> [Handler N]

2. Delegatların və hadisələrin xərci: atomlara ayırma

Yaddaş xərci

  • Hər delegat tam obyektdir.
  • Hər bir handler (metod-abunəçi) əlavə bir delegat yaradır.
  • Abunəçilər nə qədər çoxdursa — obyektlər də o qədər çox, yaddaş xərci də böyük olur.

Sadə hallarda leak və overhead demək olar ki yoxdur. Amma əgər handler-lər minlərlədirsə — artıq düşünüb çıxmaq lazımdır!

Çağırış xərci

  • Hadisə çağırışı = invocation list-in gəzilməsi.
  • Hər metod sinxron olaraq (ardıcıl) çağırılır.
  • Əgər handler ağır iş görürsə və ya uzun müddət "yatırsa", bu bütün digər handler-ləri yavaşladır.

Nümunə: sadə implementasiya


public class Counter
{
    public event EventHandler Counted;

    public void Increment()
    {
        // ... sayma loqikasını burda keçirik
        // Abunəçilər sinxron çağırılır!
        Counted?.Invoke(this, EventArgs.Empty);
    }
}

Əgər bizdə 1000 abunəçi varsa və onların handler-ləri Thread.Sleep(10) tipindədirsə, hadisənin çağırışı təxminən 10 saniyə çəkə bilər...

3. "Ağır" abunəçilər — performansın düşməni

Niyə handler-lər yüngül olmalıdır?

  • Hadisələr sinxron çağırılır, çağıran thread bütün handler-lərin bitməsini gözləyir.
  • Bir yavaş handler bütün zənciri əngəlləyir.
  • Əgər handler istisna atsa — digər handler-lər çağırılmaya bilər (əgər çağırışı try/catch ilə qorunmursunuzsa).

Demonstrasiya


class Program
{
    static void Main()
    {
        var publisher = new Counter();
        // Tez
        publisher.Counted += (s, e) => Console.WriteLine("First");
        // Yavaş
        publisher.Counted += (s, e) => System.Threading.Thread.Sleep(2000);
        // Daha bir
        publisher.Counted += (s, e) => Console.WriteLine("Last");

        // Vaxt ölçümü
        var watch = System.Diagnostics.Stopwatch.StartNew();
        publisher.Increment();
        watch.Stop();
        Console.WriteLine($"Bütün handler-lər {watch.ElapsedMilliseconds} ms ərzində çağırıldı.");
    }
}

İcra edib görəcəksiniz ki, nəzərəçarpacaq pauza var. Birinci handler demək olar ki ani, ikinci "gecikmə", və yalnız sonra üçüncü işləyir.

Praktik nəticə

  • Hadisə handler-lərinə ağır biznes loqikasını birbaşa daxil etməyin!
  • Belə işləri ayrıca thread-ə, task-a və ya asinxron handler-ə köçürmək daha yaxşıdır.

4. Handler-lərdə istisnalar: performans tələləri

Əgər abunəçilərdən biri istisna atırsa, hadisə emalı kəsilə bilər — sonrakı handler-lər çağırılmaya bilər!


publisher.Counted += (s, e) => throw new Exception("Səhv!");
publisher.Counted += (s, e) => Console.WriteLine("Bu sətri görməyəcəksiniz.");

Bundan qaçmaq və bir "pis alma" səbəbindən işi dayandırmamaq üçün hər handler-i qoruyaraq manual iterate istifadə edin.

Hadisə çağırışının təkmilləşdirilmiş versiyası


protected virtual void OnCounted()
{
    var handlers = Counted?.GetInvocationList();
    if (handlers != null)
    {
        foreach (var handler in handlers)
        {
            try
            {
                ((EventHandler)handler)(this, EventArgs.Empty);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Handler-də səhv: {ex.Message}");
                // Loglama, və ya xüsusi səhv emalı
            }
        }
    }
}

Bu hadisəni daha "dayanıqlı" edir: bir abunəçi düşsə belə — digərləri işləməyə davam edir.

5. Asinxron (fire-and-forget) hadisələr

Əgər hadisə yavaş ola bilərsə — bəzən handler-ləri əsas thread-i yormamaq üçün ayrı thread-lərdə və ya task-larda işə salmaq istəyirsən.

Variant 1: hər handler-i ayrıca task-da işə salmaq


protected virtual void OnCountedAsync()
{
    var handlers = Counted?.GetInvocationList();
    if (handlers != null)
    {
        foreach (var handler in handlers)
        {
            // Fire-and-forget: bitməsini gözləmirik!
            System.Threading.Tasks.Task.Run(() =>
            {
                ((EventHandler)handler)(this, EventArgs.Empty);
            });
        }
    }
}

Amma! Paralelliyaya diqqət

  • Əgər abunəçilər paylaşılmış resursdan istifadə edirsə — race condition-lar ola bilər.
  • Fire-and-forget handler-lərdəki istisnaları tutmaq çətin ola bilər.
  • Əgər bütün abunəçilərin bitməsini gözləmək vacibdirsə — task-ları yığmalı və Task.WhenAll istifadə etməlisiniz.

UI üçün (WinForms/WPF) — handler-ləri heç vaxt UI-thread-dən kənar çağırmayın, yoxsa InvalidOperationException alacaqsınız.

Ümumilikdə — asinxron hadisələr diqqətli və planlı dizayn tələb edir!

6. Hadisələrin saxlanmasının və çağırışının optimizasiyası

"Boş" hadisələr: yaddaşa qənaət

Əgər sinifinizdə çox hadisə varsa və onların çoxu nadir istifadə olunursa (məsələn, UI komponentində çoxlu hadisələr), bir trik var: EventHandlerList.

Bu necə işləyir

.NET kontrolleri (məsələn WinForms) hər hadisə üçün ayrı delegat saxlamır, əvəzinə bütün hadisələri bir strukturda (EventHandlerList) saxlayır — yalnız ən azı bir handler-lə abunə olunduqda entry yaranır.

Manual EventHandlerList yaratmaq nümunəsi

using System.ComponentModel; // EventHandlerList burada yerləşir!

class MyControl
{
    private readonly EventHandlerList _events = new EventHandlerList();

    private static readonly object EventMyEvent = new object();

    public event EventHandler MyEvent
    {
        add    { _events.AddHandler(EventMyEvent, value); }
        remove { _events.RemoveHandler(EventMyEvent, value); }
    }

    protected virtual void OnMyEvent()
    {
        var handler = (EventHandler)_events[EventMyEvent];
        handler?.Invoke(this, EventArgs.Empty);
    }
}

Niyə lazımdır: minlərlə "boş" hadisə üçün lazımsız delegat yaratmayıb yaddaşa qənaət edirsiniz.

7. Thread-safe: yarışlardan və bloklanmalardan qaçmaq

.NET-də hadisələr öz-özünə thread-safe deyil! Abunəçi eyni anda bir thread-də subscribe/unsubscribe edərkən başqa thread hadisəni trigger edə bilər. Bu nəticədə delegatın çağırışdan əvvəl null olmasına və NullReferenceException-a gətirib çıxara bilər.

Ən yaxşı təcrübələr

  • ?. operatorundan istifadə edin (Counted?.Invoke(...)) — null-a qarşı qoruyur.
  • Çətin hallarda — abunə/çıxma zamanı lock ilə qoruyun.

Nümunə


private readonly object _lockObj = new object();
private EventHandler _myEvent;

public event EventHandler MyEvent
{
    add { lock (_lockObj) { _myEvent += value; } }
    remove { lock (_lockObj) { _myEvent -= value; } }
}

protected virtual void OnMyEvent()
{
    EventHandler handler;
    lock (_lockObj)
    {
        handler = _myEvent;
    }
    handler?.Invoke(this, EventArgs.Empty);
}

Bu mürəkkəblik nə vaxt lazımdır?

  • Çoxthread-li tətbiqlərdə (məsələn serverlər, çoxthread-li parser-lər və s.).
  • Əgər subscribe/unsubscribe müxtəlif thread-lərdən, çağırış isə başqa thread-dən olursa.

8. add/remove aksesorləri kontrol və optimizasiya üçün

Xüsusi hallarda (məsələn, bütün abunələri loglamaq və ya abunəçi sayını məhdudlaşdırmaq) hadisəni özünüz add/remove aksesorləri ilə əl ilə implement edə bilərsiniz:


private EventHandler _event;
public event EventHandler MyEvent
{
    add
    {
        if (_event == null || _event.GetInvocationList().Length < 10)
            _event += value;
        else
            Console.WriteLine("Məhdudiyyət: 10-dan çox abunəçi olmaz.");
    }
    remove { _event -= value; }
}

Bu imkan verir:

  • Custom loqika yerləşdirməyə.
  • Hadisələri thread-safe etmək.
  • Limitləri yoxlamaq və ya subscribe/unsubscribe hadisələrini loglamaq.

9. Faydalı nüanslar

Lambda ifadələri, closures və performans

Lambda ifadələri "uçuşda" abunə üçün rahatdır:


var button = new Button();
button.Click += (s, e) => Console.WriteLine("Button clicked");

Amma əgər lambda dəyişənləri capture edirsə — closure yaranır və bu yaddaş istifadəsini artıra bilər. Çoxu UI hallarda bu problem deyil, amma low-level kodda capture-ların sayına və capture edilmiş obyektlərin lifetime-ına diqqət etmək lazımdır.

Maraqlı fakt:
Əgər iki eyni lambda-nı ardıcıl əlavə etsəniz, bu iki ayrı delegat obyekti olacaq və metod iki dəfə icra olunacaq.

Hadisələrin və delegatların profilləşdirilməsi

Tətbiq böyüdükcə hadisələri də hər hansı digər kod kimi profilləşdirməlisiniz.

Hadisənin sürətini necə ölçmək?

  • Stopwatch istifadə edərək hadisənin çağırışından handler-lərin bitməsinə qədər vaxtı ölçün.
  • Yaddaş profilləşdirmə alətləri istifadə edin (məsələn, dotMemory, Visual Studio-nun daxili vasitələri), abunəçiləri tapmaq üçün ki, onlar unsubscribe edilməyib və yaddaşda qalıblar.
  • "Zombie" abunəçiləri tapmaq üçün uzun invocation list-lərə malik long-lived obyektlər axtarın.

"Optimizasiyalar və tələlər" cədvəli

Məsələ/Ssenari Həll
Çox long-lived (və faydasız) hadisələr EventHandlerList istifadə etmək
Abunəçi hamını yavaşladır Ağır loqikanı task/ayrı thread-ə köçürmək
Thread-safe tələb olunur Çağırışdan əvvəl delegatı kopyalamaq, əlavə/çıxarışda lock istifadə etmək
Handler-lərdə istisnalar Hər handler ətrafında try/catch etmək
"Zombie" abunəçilərdən yaddaş sızması Həmişə unsubscribe olmaq, IDisposable implementasiya etmək, profilləşdirmək

Diaqram: "Optimizə olunmuş hadisənin həyat dövrü"


+----------------+       +------------------+       +---------------------+
| Abunəçi yaradıldı|  -->  | Abunə olundu (+=) |  -->  | Invocation-ə düşdü |
+----------------+       +------------------+       +---------------------+
                                |                                ^
                                |                                |
                   Unsubscribe (-=) |                     İstisna  |
                                v                                |
+----------------+       +--------------------+      +----------------------+
| Abunəçi Dispose|  -->  | Çağırışdan silindi |  --> | Daha zombie olmayacaq |
+----------------+       +--------------------+      +----------------------+

10. Müsahibədə "hadisə menecmenti"ni necə izah etmək

Əgər sizə "C#-də hadisələr nə üçün səmərəsiz ola bilər?" və ya "Hadisələrin optimizasiyası nə vaxt lazım olar?" sualı verilsə, bilin:

  • Hadisələr loose coupling üçün yaxşıdır, amma kütləvi abunə və ağır handler-lərdə səmərəsiz ola bilər.
  • Onlar default üzrə thread-safe deyillər.
  • Manual unsubscribe tələb edir (əks halda yaddaş sızıntısı yaranır).
  • Massiv producer/consumer ssenarilərdə — EventHandlerList və öz aksesorlər add/remove faydalıdır.
  • Həddən artıq dərin kontrol nadir hallarda lazım olur — əksər işlər standart pattern ilə həll olunur.

Növbəti mühazirədə biz daha qabaqcıl ssenariləri və praktik misalları gözdən keçirəcəyik, orada bütün bu optimizasiyaların real tapşırıqlarda necə işlədiyini görəcəksiniz.

Tez-tez rast gəlinən miflər və anti-patternlər

  • .NET-də hadisələrin həmişə sürətli olduğunu düşünmək — onlar sürətlidirlər, amma abunəçilər çox olduqda və ya ağır handler-lər ortaya çıxdıqda sürət düşür.
  • GC hər şeyi özü təmizləyəcək ümidinə qalmaq — yox, unsubscribe edilməyibsə obyekt həmişə yaşayacaq!
  • Hadisələri biznes-loqikanın uzaq qatları arasında "uzaq" əlaqə üçün istifadə etmək — bunun əvəzinə açıq pattern-lər (məsələn Mediator) daha yaxşıdır.
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION