CodeGym /Kurslar /C# SELF /Asinxron və sinxron kodun qarşılıqlı əlaqəsi

Asinxron və sinxron kodun qarşılıqlı əlaqəsi

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

1. Giriş

C#-da asinxronluq güclü alətdir. Amma bəzən asinxron kodun sinxron çağırışdan (və ya əksinə) çağırılması kimi hallarla üzləşməli oluruq. Hər şey "möcüzəvi" işləməlidir kimi görünə bilər, amma praktikada qəribə asılılıqlar (deadlock), performans itkisi və hətta gözlənilməz… pozulmuş istifadəçi interfeysi ola bilər. Və çox vaxt bug yalnız real məlumatlar üzərində üzə çıxır: istehsalatda və ya istifadəçi maşınında. Səbəb adətən sinxron və asinxron kodun səhv qarşılıqlı təsiri və .NET task scheduler-in işinin görünməyən təfərrüatlarında gizlənir.

Həmçinin müasir kitabxanalar və framework-lər asinxron metodlardan geniş istifadə edir və sizə lazımdır ki, asinxron kodu mövcud sinxron zəncirlərə düzgün "yapışdırmağı" və ya əksinə asinxron metoddan sinxron kodu necə düzgün çağırmağı başa düşəsiniz.

Qısa xülasə: await zamanı nə baş verir

İstifadə etdikdə:

await SomeAsyncMethod();

Kod iki hissəyə "kəsilir": await-dən əvvəl və sonra. Birinci hissə ilk gözlənilən asinxron əmələ (məsələn, şəbəkə sorğusuna) qədər icra olunur, davamı isə onun bitməsindən sonra. Suallar: "davam" harada icra olunacaq? Eyni thread-də? Başqa thread-də? Əgər desktop tətbiqi (məsələn, WPF və ya WinForms) yazırsınızsa nə olacaq, konsol tətbiqində nə olacaq? Cavab — asılıdır. Və burada, məsələn, ConfigureAwait rol oynayır.

SynchronizationContext nədir?

SynchronizationContext — .NET-ın xüsusi mexanizmidir, hansı ki, asinxron əməliyyatın davamının harada və necə çağırılmalı olduğunu "yadda saxlamaga" imkan verir.

  • Klassik WinForms/WPF tətbiqlərində SynchronizationContext təmin edir ki, await-dən sonra metodun qalan hissəsi eyni UI thread-də icra olunsun, beləliklə "control-a başqa thread-dən müraciət" səhvləri yaranmasın.
  • ASP.NET (köhnə, Core olmayan) da SynchronizationContext HttpContext-i bərpa etməyə və HTTP sorğusu ilə işləməyə imkan verir.
  • Konsol və ASP.NET Core tətbiqlərində SynchronizationContext adətən yoxdur (bərabərdir null) və davamlar thread pool-da icra olunur.

TaskScheduler nədir?

TaskScheduler — daha aşağı səviyyəli mexanizmdir. Çox vaxt siz TaskScheduler.Default ilə işləyirsiniz ki, o .NET thread pool-dan istifadə edir. O qərar verir hansı task nə vaxt və harada icra olunacaq.

2. awaitResult/Wait() qarışdırılmasında deadlock

Bu, C#-da ən məşhur tələdir:

// Haradasa UI-kodda
var result = SomeAsyncMethod().Result;

və ya

SomeAsyncMethod().Wait();

Hər şey: tətbiq "sovur". Niyə?

Bu necə baş verir?

  1. Siz asinxron metodu çağırırsınız və dərhal .Result və ya .Wait() tələb edirsiniz — yəni cari thread-i "bloklayırsınız" və asinxron task-ın bitməsini gözləyirsiniz.
  2. SomeAsyncMethod öz daxilində await edir və davamı eyni thread-ə SynchronizationContext vasitəsilə planlayır, amma bu thread artıq Result/.Wait()-in gözləməsi ilə bloklanıb.
  3. Çünki thread artıq Result/.Wait()-i gözləyir, o continuation-u icra edə bilmir.
  4. Hər şey, deadlock: thread özü özünü gözləyir.

Bu, xüsusilə UI-tətbiqlərdə asanlıqla təkrarlanır, çünki bütün kod UI thread-də işləyir və bütün davamlar məhz bu thread-i gözləyir. Konsol tətbiqlərində və ASP.NET Core-da (synchronization context olmadıqda) belə deadlock-lar nadir olur.

Həyatdan zarafat: Əgər siz .Result ilə deadlock əldə edə bildinizsə — təbriklər, Senior adına bir neçə addım yaxınızsınız :D

3. Asinxron kodu sinxron içində necə düzgün yerləşdirmək?

Birinci tövsiyə: asinxronluq yuxarıdan aşağı

Əgər asinxron metod yaranıbsa, async/await-i çağırış stack-ı boyunca UI və ya giriş nöqtəsinə qədər qaldırın. Asinxronluğu yolun yarısında "tutmayın".

Pis (thread-i bloklayır):

// Sinxron metod asinxronu .Result ilə çağırır
public void DoStuff()
{
    var data = GetDataAsync().Result;
    // ...
}

Yaxşı (asinxronluq bütünlüklə):

public async Task DoStuffAsync()
{
    var data = await GetDataAsync();
    // ...
}

Əgər mümkün isə — həmişə await-dən istifadə edin, .Result / .Wait() deyil.

4. Amma belə hallar olur: async-i sync-dən çağırmaq lazımdır

Üst ağacı tamamilə async-ə yazmaq (ən yaxşı seçim — seçim imkanınız varsa).

Xüsusi patternlərdən istifadə etmək: məsələn, task-ı ayrıca thread-də Task.Run ilə işə salmaq və içəridə async-metodu çağırmaq.

public void DoStuff()
{
    var result = Task.Run(() => SomeAsyncMethod()).Result;
}

Amma: burada da UI-tətbiqlərdə sinxronizasiya ilə bağlı nüanslar var, ona görə də böyük ehtiyac yoxdursa belə etməmək daha yaxşıdır.

5. ConfigureAwait(false) nə edir?

Bəzən sizə lazım deyil ki, await-dən sonra kod eyni thread-də davam etsin (məsələn, server tətbiqlərində və ya UI tələb etməyən kitabxana kodunda). Əksinə, .NET-in bir thread-ə bağlı qalmaması daha yaxşıdır — bu işi sürətləndirir!

Sintaksis və iş prinsipi

await SomeAsyncMethod().ConfigureAwait(false);
  • ConfigureAwait(false) deyir: "mənə yerli SynchronizationContext lazım deyil, davamı harada olsa icra et, istənilən thread ola bilər".
  • ConfigureAwait(true) (default) — "davamı orada icra et, harda await çağırıldı, ideal halda eyni SynchronizationContext".

Vizual sxem


        ┌─────────────────────────────────────────────────────┐
        │                     SynchronizationContext          │
        └─────────────────────────────────────────────────────┘
                      ↑                             ↑
       (UI-potok)   await SomeAsyncMethod()        Continuation (await-dən sonra)
                  ──────────────────────────────>  (eyni potok — əgər ConfigureAwait(true))
                      ↓
                    (hər hansı potok — əgər ConfigureAwait(false))

ConfigureAwait(false) nümunəsi — "kitabxana" kodu

Tutaq ki, kitabxana yazırsınız və onu hamı istifadə edə bilər: WinForms, WPF, ASP.NET, konsol tətbiqləri…

Siz onların thread modelinə bağlı olmamalısınız. Ona görə kitabxananızın asinxron metodlarında ConfigureAwait(false)-dən istifadə edin:

public async Task<string> LoadDataFromUrlAsync(string url)
{
    using var client = new HttpClient();
    string content = await client.GetStringAsync(url).ConfigureAwait(false);
    return content;
}

İndi metodunuz konkret SynchronizationContext-dən icazə istəməyəcək. Bu daha təhlükəsizdir və performanslıdır (az thread switch).

Məsələn: await ConfigureAwait olmadan nə olur

WPF tətbiqini nəzərdən keçirək:

private async void Button_Click(object sender, RoutedEventArgs e)
{
    Button1.Content = "Yüklənir...";
    await Task.Delay(2000);  // Uzun əməliyyatın imitasiya edilməsi
    Button1.Content = "Hazır!";
}

Task.Delay özündə "await" edir. Default olaraq await-dən sonra idarəetmə UI thread-ə qaytarılır ki, elementləri yeniləmək mümkün olsun.

Əgər uzun əməliyyat daxilində ConfigureAwait(false) istifadə edilsə:

await Task.Delay(2000).ConfigureAwait(false);
Button1.Content = "Hazır!";  // Səhv!

İstisna yaranacaq: InvalidOperationException: "The calling thread cannot access this object because a different thread owns it."
Çünki indi "davam" başqa thread-də icra olunur və UI-ya müraciət etmək olmaz.

Nəticə: ConfigureAwait(false)-dən yalnız UI/kontekstə ehtiyac olmayan yerlərdə istifadə edin.

6. Faydalı nüanslar

ConfigureAwait-ı harada və nə vaxt istifadə etmək

Ssenari .ConfigureAwait(false)-dan istifadə lazımdır? Niyə?
Kitabxana kodu Bəli Hər hansı kontekstdə çağırıla bilər, UI lazım deyil
ASP.NET Core daxilində Bəli (kontekst demək olar ki, yoxdur) Performansı artırır
WinForms/WPF, UI-ya müraciət edilərkən Xeyr UI-thread-ə geri qaytarmaq lazımdır
Sinxron metodlar Mənası yoxdur SynchronizationContext yoxdur
Konsol tətbiqləri Ola bilər, amma effekti görünmür Kontekst yoxdur

SynchronizationContext-in olub-olmadığını necə bilmək olar?

Bunu birbaşa koddən yoxlaya bilərsiniz:

Console.WriteLine(SynchronizationContext.Current == null
    ? "Kontekst yoxdur"
    : "Kontekst mövcuddur");
  • Konsol və ASP.NET Core tətbiqlərində "Kontekst yoxdur" çıxacaq.
  • WinForms/WPF-də isə "Kontekst mövcuddur".

Thread keçidlərinin vizuallaşdırılması

sequenceDiagram
    participant MainThread as Əsas thread (UI/Console)
    participant ThreadPool as Pool-dan thread

    MainThread->>SomeAsyncMethod: Metodun çağırışı
    SomeAsyncMethod->>MainThread: await ConfigureAwait yoxdur
    Note right of MainThread: await-dən sonra həmin thread-ə qayıdırıq
    SomeAsyncMethod->>ThreadPool: await ConfigureAwait(false)
    Note right of ThreadPool: await-dən sonra hər hansı thread-də işləmək olar

Qısa istifadə qaydaları

  • UI-kodda asinxron metodlarla işləyərkən .Result.Wait()-dən istifadə etməyin.
  • Kitabxana kodunda bütün await-lər üçün mütləq ConfigureAwait(false)-dan istifadə edin.
  • UI-tətbiqlərdə ConfigureAwait(false)-dən yalnız interfeys elementlərinə müraciət etməyən metodların içində istifadə edin.
  • Sinxron və asinxron kodu qarışdırmaq lazım olmadıqda qarışdırmayın.
  • Async-i sync-dən çağırmaq lazım gəldikdə iki dəfə fikirləşin: bütün zənciri async etmək mümkün deyilmi?

7. Asinxronluqla işləyən başlayanların tipik səhvləri

Səhv №1: hər yerdə .Result və ya .Wait() istifadə etmək.
Çox vaxt programçılar, xüsusən də bir neçə məqalə oxuduqdan sonra, asinxron metod çağırışlarını "sinxronizasiya etmək" üçün hər tərəfə .Result və ya .Wait() əlavə etməyə başlayırlar. İlk baxışda rahat görünə bilər, amma praktikada bu UI-tətbiqlərdə deadlock-ın zəmanətli yoludur. Xüsusilə asinxron metodların içində ConfigureAwait(false) istifadə olunmursa — UI thread bloklanır və continuation-u yerinə yetirə bilmir.

Səhv №2: ConfigureAwait(false)-dən yanlış və ya həddindən artıq istifadə.
Bəzi başlayanlar ConfigureAwait(false)-i hər yerdə tətbiq edirlər, hətta UI elementləri ilə işləyən kodlarda. UI-tətbiqlərdə bu elementlərə müraciət zamanı InvalidOperationException-lara gətirir, çünki davam indi UI-thread-dən fərqli thread-də icra olunur.

Səhv №3: ConfigureAwait-in yalnız awaitable obyektlərdə işlədiyini unudurlar.
Çoxları hesab edir ki, ConfigureAwait(false) hər metod üçün istifadə oluna bilər, amma əslində o yalnız Task, Task<T>, ValueTask və digər awaitable tiplərlə işləyir. Sinxron metodlara ConfigureAwait tətbiq etmək mənasızdır və gözləmə asinxron olmur.

Səhv №4: zərurət olmadan sinxron və asinxron kodu qarışdırırlar.
Asinxron metodu sinxron koddan strateji düşünmədən çağırmaq cəhdləri (məsələn, .Result, .Wait() və ya Task.Run vasitəsilə) tez-tez subtil səhvlərə, performans itkilərinə və diaqnostikası çətin deadlock-lara gətirib çıxarır. Hətta metod qısa və təhlükəsiz görünə bilərsə də, belə çağırışlar zənciri pozaraq bütün tətbiqin işini poza bilər.

Səhv №5: SynchronizationContext-in təsirini az qiymətləndirirlər.
Yeni başlayanlar çox vaxt unudurlar ki, await-dən sonra davam eyni thread-də (UI) icra oluna bilər, əgər ConfigureAwait(false) istifadə olunmayıbsa. Bu, xüsusən UI və kitabxana kodunun qarışdırıldığı yerlərdə gözlənilməz davranışlara gətirir, harada ki wait-lər və davamlar bir-biri ilə dolaşır.

1
Sorğu/viktorina
, səviyyə, dərs
Əlçatan deyil
Asinxron data axınları
Asinxronluğa dərindən baş vurmaq
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION