.NET: Multithreading və asinxroniya ilə işləmək üçün alətlər. 1-ci hissə

Tərcüməsi korporativdə yerləşdirilən orijinal məqaləni Habr-da dərc edirəm blog yazı.

Burada və indi nəticə gözləmədən nəyisə asinxron etmək və ya onu yerinə yetirən bir neçə bölmə arasında çoxlu işi bölmək zərurəti kompüterlərin yaranmasından əvvəl mövcud idi. Onların görünüşü ilə bu ehtiyac çox hiss olunur. İndi, 2019-cu ildə bu məqaləni 8 nüvəli Intel Core prosessoru olan, yüz proses deyil, daha çox ipin paralel olaraq işlədiyi noutbukda yazırıq. Yaxınlıqda bir az xarab telefon var, bir neçə il əvvəl alınıb, bortunda 8 nüvəli prosessor var. Tematik resurslar, müəlliflərinin 16 nüvəli prosessorları yerləşdirdiyi bu ilin flaqman smartfonlarına heyran olduqları məqalələr və videolarla doludur. MS Azure 20 nüvəli və 128 TB operativ yaddaşa malik virtual maşını saatda 2 dollardan az qiymətə təqdim edir. Təəssüf ki, iplərin qarşılıqlı təsirini idarə edə bilmədən maksimumu çıxarmaq və bu gücü cilovlamaq mümkün deyil.

Terminologiya

Proses - ƏS obyekti, təcrid olunmuş ünvan sahəsi, mövzuları ehtiva edir.
Mövzu - ƏS obyekti, ən kiçik icra vahidi, prosesin bir hissəsi, iplər proses daxilində yaddaşı və digər resursları öz aralarında paylaşır.
Multitasking - ƏS-nin mülkiyyəti, eyni zamanda bir neçə prosesi yerinə yetirmək imkanı
Çox nüvəli - prosessorun mülkiyyəti, verilənlərin emalı üçün çoxlu nüvələrdən istifadə etmək imkanı
multiprocessing - kompüterin xüsusiyyəti, fiziki olaraq bir neçə prosessorla eyni vaxtda işləmək imkanı
Çox iş parçacığı - prosesin xüsusiyyəti, məlumatların işlənməsini bir neçə mövzu arasında yaymaq imkanı.
Paralellik - vaxt vahidində fiziki olaraq eyni vaxtda bir neçə hərəkətin yerinə yetirilməsi
Asinxroniya - bu emalın tamamlanmasını gözləmədən əməliyyatın icrası, icranın nəticəsi sonradan emal oluna bilər.

Metafor

Bütün təriflər yaxşı deyil və bəzilərinin əlavə izaha ehtiyacı var, ona görə də rəsmi olaraq təqdim edilmiş terminologiyaya səhər yeməyi bişirmək haqqında metafora əlavə edəcəyəm. Bu metaforada səhər yeməyi hazırlamaq bir prosesdir.

Səhər səhər yeməyinin hazırlanmasıCPU) mətbəxə gəl (Kompüter). 2 əlim varÖzəyi). Mətbəxdə bir sıra məişət texnikası var (IO): soba, çaydan, toster, soyuducu. Qazı yandırıram üstünə tava qoyub yağ tökürəm isinməsini gözləmədən (asinxron, Bloklanmayan-IO-Gözləyin), yumurtaları soyuducudan çıxarıb boşqaba bölürəm, sonra bir əlimlə döyürəm (Mövzu #1) və ikinci (Mövzu #2) boşqabdan saxlayın (Paylaşılan Resurs). İndi mən hələ də çaydanı yandırardım, amma əllər kifayət deyil (Thread Aclıq) Bu müddət ərzində qovurma qabı qızdırılır (Nəticəni emal edir) orada çırpdığımı tökürəm. Çaydana uzanıb onu yandırıram və axmaqcasına içindəki suyun qaynamasına baxıram (Bloklama-IO-Gözləyin), baxmayaraq ki, bu müddət ərzində omleti çırpdığım boşqabı yuya bildim.

Mən cəmi 2 əlimlə omlet bişirdim, artıq yoxdu, amma eyni zamanda omleti çırpmaq zamanı bir anda 3 əməliyyat oldu: omleti çırpmaq, boşqabı tutmaq, qabı qızdırmaq. CPU kompüterin ən sürətli hissəsidir, IO daha tez-tez hər şeyi yavaşlatan şeydir, buna görə də çox vaxt effektiv həll IO-dan məlumat alınarkən CPU-nu bir şeylə işğal etməkdir.

Metaforaya davam:

  • Omlet bişirmə prosesində mən də paltar dəyişdirməyə çalışardımsa, bu, çoxşaxəli işlərə nümunə olardı. Əhəmiyyətli bir nüans: kompüterlər bu işdə insanlardan daha yaxşıdır.
  • Restoran kimi çoxsaylı aşpazları olan mətbəx çox nüvəli kompüterdir.
  • Alış-veriş mərkəzində yemək meydançasında bir çox restoran - məlumat mərkəzi

.NET alətləri

Bir çox başqa şeylərdə olduğu kimi, .NET də yaxşı işləyir. Hər yeni versiya ilə o, onlarla işləmək üçün getdikcə daha çox yeni alətlər, OS ipləri üzərində yeni abstraksiya qatlarını təqdim edir. Quraşdırma abstraksiyaları ilə işləyərkən çərçivə tərtibatçıları yüksək səviyyəli abstraksiyadan istifadə edərkən bir və ya daha aşağı səviyyədən aşağı enmək imkanını tərk edən bir yanaşma istifadə edirlər. Çox vaxt bu lazım deyil, üstəlik, ov tüfəngi ilə ayağınıza atəş açmaq imkanını açır, lakin bəzən, nadir hallarda, həll etməyən problemi həll etməyin yeganə yolu ola bilər. indiki abstraksiya səviyyəsi.

Alətlər dedikdə mən həm çərçivə, həm də üçüncü tərəf paketləri tərəfindən təmin edilən proqramlaşdırma interfeyslərini (API), həm də çox yivli kodla bağlı hər hansı problemi tapmağı asanlaşdıran bütöv bir proqram həllini nəzərdə tuturam.

Mövzunun başlanması

Thread sinfi .NET-də ən əsas yivləmə sinfidir. Konstruktor iki nümayəndədən birini qəbul edir:

  • ThreadStart - Parametrlər yoxdur
  • ParametrizedThreadStart - tipli obyektin bir parametri ilə.

Nümayəndə Başlanğıc metodu çağırıldıqdan sonra yeni yaradılmış iplikdə icra ediləcək, əgər ParametrizedThreadStart tipli nümayəndə konstruktora ötürülübsə, onda Start metoduna obyekt ötürülməlidir. Bu mexanizm istənilən yerli məlumatı axına ötürmək üçün lazımdır. Qeyd etmək lazımdır ki, ip yaratmaq bahalı əməliyyatdır, ipin özü isə ağır obyektdir, ən azı ona görə ki, hər stek üçün 1MB yaddaş ayırır və OS API ilə qarşılıqlı əlaqə tələb edir.

new Thread(...).Start(...);

ThreadPool sinfi hovuz anlayışını təmsil edir. .NET-də mövzu hovuzu bir mühəndislik parçasıdır və Microsoft-da tərtibatçılar onun müxtəlif ssenarilərdə optimal işləməsi üçün çox səy göstərmişlər.

Ümumi konsepsiya:

Tətbiq işə salındığı andan ehtiyatda fonda bir neçə mövzu yaradır və onları istifadəyə götürmək imkanı verir. Mövzular tez-tez və çox sayda istifadə olunursa, hovuz zəng kodunun ehtiyaclarını ödəmək üçün genişlənir. Hovuzda lazımi vaxtda boş iplər olmadıqda, ya iplərdən birinin qayıtmasını gözləyəcək, ya da yenisini yaradacaq. Buradan belə nəticə çıxır ki, iplik hovuzu bəzi qısa hərəkətlər üçün əladır və proqram boyu xidmət kimi işləyən əməliyyatlar üçün zəif uyğun gəlir.

Hovuzdan ipdən istifadə etmək üçün ParametrizedThreadStart imzasına uyğun gələn WaitCallback tipli nümayəndə qəbul edən QueueUserWorkItem metodu var və ona ötürülən parametr eyni funksiyanı yerinə yetirir.

ThreadPool.QueueUserWorkItem(...);

Bloklanmayan IO əməliyyatlarını təşkil etmək üçün daha az tanınan mövzu hovuzu metodu RegisterWaitForSingleObject istifadə olunur. Bu metoda ötürülən nümayəndə, metoda ötürülən WaitHandle buraxıldıqda çağırılacaq.

ThreadPool.RegisterWaitForSingleObject(...)

.NET-də iplik taymeri var və o, WinForms/WPF taymerlərindən onunla fərqlənir ki, onun işləyicisi hovuzdan götürülmüş iplə çağırılacaq.

System.Threading.Timer

Hovuzdan bir ipə icra üçün bir nümayəndə göndərməyin olduqca ekzotik bir yolu da var - BeginInvoke metodu.

DelegateInstance.BeginInvoke

Mən həmçinin yuxarıda göstərilən metodların çoxunu çağıran funksiya üzərində qısaca dayanmaq istəyirəm - Kernel32.dll Win32 API-dən CreateThread. Xarici metodların mexanizmi sayəsində bu funksiyanı çağırmağın bir yolu var. Mən yalnız bir dəfə miras kodunun ən dəhşətli nümunəsində belə bir çağırışı gördüm və bunu edən müəllifin motivasiyası mənim üçün hələ də sirr olaraq qalır.

Kernel32.dll CreateThread

Mövzulara Baxış və Sazlama

Özünüz tərəfindən yaradılmış mövzulara, bütün XNUMX-cü tərəf komponentlərinə və .NET hovuzuna Visual Studio-nun Threads pəncərəsində baxmaq olar. Bu pəncərə yalnız proqram sazlama mərhələsində və fasilə rejimində olduqda mövzular haqqında məlumatı göstərəcək. Burada siz rahatlıqla hər bir başlığın yığın adlarına və prioritetlərinə baxa, sazlamanı müəyyən bir mövzuya keçirə bilərsiniz. Thread sinifinin Priority xassəsi ilə siz ipin prioritetini təyin edə bilərsiniz, OC və CLR prosessor vaxtını iplər arasında bölərkən tövsiyə kimi qəbul edəcəklər.

.NET: Multithreading və asinxroniya ilə işləmək üçün alətlər. 1-ci hissə

Tapşırıq Paralel Kitabxanası

Task Parallel Library (TPL) .NET 4.0-da təqdim edilmişdir. İndi asinxroniya ilə işləmək üçün standart və əsas vasitədir. Köhnə yanaşmalardan istifadə edən hər hansı kod miras sayılır. TPL-nin əsas vahidi System.Threading.Tasks ad sahəsinin Task sinfidir. Tapşırıq mövzu üzərində abstraksiyadır. C# dilinin yeni versiyası ilə biz Tasks ilə işləmək üçün zərif bir üsul əldə etdik - async/await ifadələri. Bu anlayışlar asinxron kodun sadə və sinxron olduğu kimi yazılmasını mümkün etdi, bu, hətta iplərin daxili işini az başa düşən insanlara belə onları istifadə edən proqramları, uzun əməliyyatları yerinə yetirərkən donmayan proqramları yazmağa imkan verdi. Async/await-dən istifadə bir və ya bir neçə məqalə üçün mövzudur, lakin mən bir neçə cümlə ilə fikri çatdırmağa çalışacağam:

  • async Tapşırığı və ya etibarsızlığı qaytaran metod üçün dəyişdiricidir
  • və await Task-ın bloklanmayan gözləmə operatorudur.

Bir daha: gözləmə operatoru, ümumi halda (istisnalar var), cari icra xəttini daha da buraxacaq və Tapşırıq icrasını başa vurduqda və ipi (əslində, konteksti demək daha düzgündür, lakin bu barədə daha sonra) metodu daha da icra etməyə davam etməkdə sərbəst olacaq. .NET daxilində bu mexanizm, yazılı metod dövlət maşını olan və bu vəziyyətlərdən asılı olaraq ayrı-ayrı parçalarda icra oluna bilən bütöv bir sinfə çevrildiyi zaman verim qaytarılması ilə eyni şəkildə həyata keçirilir. Maraqlanan hər kəs Async/await istifadə edərək istənilən sadə kodu yaza, kompilyasiya edə və Compiler Generated Code aktivləşdirilmiş JetBrains dotPeek istifadə edərək montaja baxa bilər.

Tapşırığı işə salmaq və istifadə etmək variantlarını nəzərdən keçirin. Aşağıdakı misal kodda heç bir faydalı olmayan yeni tapşırıq yaradırıq (Thread.Sleep (10000)), lakin real həyatda bəzi mürəkkəb CPU intensiv iş olmalıdır.

using TCO = System.Threading.Tasks.TaskCreationOptions;

public static async void VoidAsyncMethod() {
    var cancellationSource = new CancellationTokenSource();

    await Task.Factory.StartNew(
        // Code of action will be executed on other context
        () => Thread.Sleep(10000),
        cancellationSource.Token,
        TCO.LongRunning | TCO.AttachedToParent | TCO.PreferFairness,
        scheduler
    );

    //  Code after await will be executed on captured context
}

Tapşırıq bir sıra seçimlərlə yaradılır:

  • LongRunning tapşırığın tez tamamlanmayacağına işarədir, bu o deməkdir ki, yəqin ki, hovuzdan ip götürməmək, qalanlara zərər verməmək üçün bu Tapşırıq üçün ayrıca bir ip yaratmaq barədə düşünməlisiniz.
  • AttachedToParent - Tapşırıqlar iyerarxiyada sıralana bilər. Əgər bu seçim istifadə olunubsa, Tapşırıq özünü tamamlamış və uşaqlarının icrasını gözlədiyi vəziyyətdə ola bilər.
  • PreferFairness - o deməkdir ki, əvvəllər icraya göndərilən tapşırıqları sonradan göndərilənlərdən əvvəl yerinə yetirmək yaxşı olardı. Ancaq bu sadəcə bir tövsiyədir və nəticəyə zəmanət verilmir.

Metoduna ötürülən ikinci parametr CancellationToken-dir. Başladıqdan sonra əməliyyatın ləğvini düzgün idarə etmək üçün icra olunan kod CancellationToken-in vəziyyətinə dair yoxlamalarla doldurulmalıdır. Əgər yoxlamalar yoxdursa, CancellationTokenSource obyektində çağırılan Ləğv üsulu Tapşırığın icrasını yalnız başlamazdan əvvəl dayandıra biləcək.

Son parametr TaskScheduler tipli planlaşdırıcı obyektidir. Bu sinif və onun nəsli, tapşırıqları mövzular arasında paylamaq üçün strategiyalara nəzarət etmək üçün nəzərdə tutulmuşdur, defolt olaraq Tapşırıq hovuzdan təsadüfi ipdə yerinə yetiriləcəkdir.

Gözləmə operatoru yaradılmış Tapşırığa tətbiq edilir, bu o deməkdir ki, ondan sonra yazılan kod, əgər varsa, gözləmədən əvvəlki kodla eyni kontekstdə (çox vaxt bu, eyni mövzuda nəzərdə tutulur) icra olunacaq.

Metod async void kimi qeyd olunur, yəni gözləmə operatorundan istifadə etməyə icazə verilir, lakin zəng kodu icranı gözləyə bilməyəcək. Bu qabiliyyət tələb olunarsa, metod Task qaytarmalıdır. Async etibarsız olaraq qeyd olunan üsullar olduqca yaygındır: bir qayda olaraq, bunlar hadisə idarəediciləri və ya yanğın və unutma prinsipi ilə işləyən digər üsullardır. Nəinki icranın tamamlanmasını gözləmək imkanı vermək, həm də nəticəni qaytarmaq lazımdırsa, Tapşırıqdan istifadə etmək lazımdır.

StartNew metodunun qaytardığı Tapşırıqda, lakin hər hansı digər metodda olduğu kimi, yalançı parametrlə ConfigureAwait metodunu çağıra bilərsiniz, sonra gözləmədən sonra icra tutulan kontekstdə deyil, ixtiyari birində davam edəcəkdir. Bu, gözləmədən sonra icra konteksti kod üçün vacib olmadıqda edilməlidir. Bu, həmçinin kitabxanada qablaşdırılacaq kod yazarkən MS-nin tövsiyəsidir.

Task'i-nin tamamlanmasını necə gözləyə biləcəyiniz üzərində bir az daha dayanaq. Aşağıda gözləmənin şərti olaraq yaxşı və şərti olaraq pis olması ilə bağlı şərhlərlə bir kod nümunəsidir.

public static async void AnotherMethod() {

    int result = await AsyncMethod(); // good

    result = AsyncMethod().Result; // bad

    AsyncMethod().Wait(); // bad

    IEnumerable<Task> tasks = new Task[] {
        AsyncMethod(), OtherAsyncMethod()
    };

    await Task.WhenAll(tasks); // good
    await Task.WhenAny(tasks); // good

    Task.WaitAll(tasks.ToArray()); // bad
}

Birinci misalda, çağıran ipi bloklamadan Tapşırığın tamamlanmasını gözləyirik, nəticəni yalnız o artıq mövcud olduqda emal etməyə qayıdacayıq, o vaxta qədər çağırış ipi özünə qalır.

İkinci seçimdə, metodun nəticəsi hesablanana qədər çağıran ipi bloklayırıq. Bu, təkcə ona görə pisdir ki, biz bu qədər dəyərli proqram resursu olan mövzunu sadə boşluqla zəbt etmişik, həm də ona görə ki, əgər çağırdığımız metodun kodunda gözləmə varsa və sinxronizasiya konteksti zəng edən mövzuya qayıtmağı nəzərdə tutur. gözlədikdən sonra, sonra bir dalana düçar olacağıq: çağırış mövzusu asinxron metodun nəticəsinin qiymətləndirilməsini gözləyir, asinxron metod çağıran ipdə icrasını davam etdirməyə əbəs yerə çalışır.

Bu yanaşmanın başqa bir dezavantajı mürəkkəb səhvlərin idarə olunmasıdır. Fakt budur ki, async/await istifadə edərkən asinxron koddakı səhvləri idarə etmək çox asandır - onlar kodun sinxron olduğu kimi davranırlar. Tapşırıqa sinxron gözləmə cin çıxarma tətbiq etsək, orijinal istisna AggregateException-a bükülür, yəni. İstisna ilə məşğul olmaq üçün siz InnerException növünü araşdırmalı və C # dünyasında daha çox tanış olan tutma blok zəncirinin yerinə if zəncirini özünüz bir tutma blokunun içərisinə yazmalı və ya qurarkən catch-dan istifadə etməli olacaqsınız.

Üçüncü və sonuncu nümunələr də eyni səbəbdən pis olaraq qeyd olunub və eyni problemləri ehtiva edir.

WhenAny və WhenAll üsulları bir qrup Tapşırıq gözləyərkən son dərəcə rahatdır, onlar bir qrup Tapşırıqları qrupdan ilk Tapşırıqda və ya hər kəs icrasını başa vurduqdan sonra işləyəcək birinə yığırlar.

Mövzuların dayandırılması

Müxtəlif səbəblərə görə, başlandıqdan sonra mövzunu dayandırmaq lazım ola bilər. Bunu etmək üçün bir sıra yollar var. Thread sinfinin müvafiq adları olan iki metodu var - bunlardır Ləğv et и Kesme. Birincisi istifadə üçün çox tövsiyə olunur, çünki. hər hansı bir təsadüfi anda onun çağırışından sonra, hər hansı bir təlimatın işlənməsi zamanı bir istisna atılacaq ThreadAbortedException. Tam dəyişən artırıldıqda belə bir istisnanın atılacağını gözləmirsiniz, elə deyilmi? Və bu üsuldan istifadə edərkən bu, çox real vəziyyətdir. CLR-nin kodun müəyyən bir hissəsində belə bir istisna atmasının qarşısını almaq lazımdırsa, onu zənglərə bağlaya bilərsiniz. Thread.BeginCritical Region, Thread.EndCriticalRegion. Finally blokunda yazılan istənilən kod belə zənglərə çevrilir. Bu səbəbdən, çərçivə kodunun bağırsaqlarında boş bir cəhdlə blokları tapa bilərsiniz, lakin nəhayət boş deyil. Microsoft bu metoddan o qədər çəkinir ki, onlar onu .net core-a daxil etməyiblər.

Interrupt metodu daha proqnozlaşdırılan şəkildə işləyir. İstisna ilə mövzunu kəsə bilər ThreadInterruptedException yalnız ip gözləmə vəziyyətində olduqda. Bu vəziyyətə WaitHandle, kilidi gözləyərkən və ya Thread.Sleep-ə zəng etdikdən sonra asılaraq daxil olur.

Yuxarıda təsvir edilən hər iki variant gözlənilməzliyi üçün pisdir. Çıxış yolu strukturdan istifadə etməkdir Ləğv Token və sinif Ləğv TokenSource. Nəticə budur: CancellationTokenSource sinifinin bir nümunəsi yaradılır və yalnız ona sahib olan metodu çağıraraq əməliyyatı dayandıra bilər. ləğv etmək. Yalnız CancellationToken əməliyyatın özünə ötürülür. CancellationToken sahibləri əməliyyatı özləri ləğv edə bilməzlər, ancaq əməliyyatın ləğv edilib-edilmədiyini yoxlaya bilərlər. Bunun üçün bir boolean xüsusiyyəti var Ləğv edilmək istəndi və üsul ThrowIfCancelRequested. Sonuncu bir istisna atacaq TaskCancelledException Əgər Ləğv üsulu CancellationToken-i pariasiya edən CancellationTokenSource instansiyasında çağırılıbsa. Və bu mənim tövsiyə etdiyim üsuldur. Bu, bir istisna əməliyyatının nə vaxt dayandırıla biləcəyinə tam nəzarət verməklə əvvəlki seçimlərdən daha yaxşıdır.

Mövzu dayandırmağın ən qəddar yolu Win32 API TerminateThread funksiyasını çağırmaqdır. Bu funksiyanı çağırdıqdan sonra CLR-nin davranışı gözlənilməz ola bilər. MSDN-də bu funksiya haqqında aşağıdakılar yazılmışdır: “TerminateThread təhlükəli funksiyadır və yalnız ən ekstremal hallarda istifadə olunmalıdır. "

FromAsync Metodundan istifadə edərək köhnə API-nin Tapşırıqlara çevrilməsi

Əgər Tasks təqdim edildikdən sonra başlanmış və artıq əksər tərtibatçıların sakit dəhşətini oyatmayan layihə üzərində işləmək üçün şanslısınızsa, o zaman çoxlu köhnə API-lərlə (hər ikisi də üçüncü tərəf) məşğul olmayacaqsınız. və komandanızın keçmişdə işgəncə verdiyi. Xoşbəxtlikdən, .NET Framework inkişaf komandası bizimlə maraqlandı, baxmayaraq ki, məqsəd özümüzə qayğı göstərmək ola bilərdi. Nə olursa olsun, .NET köhnə asinxron proqramlaşdırma yanaşmalarında yazılmış kodu ağrısız şəkildə yenisinə çevirmək üçün bir sıra alətlərə malikdir. Onlardan biri TaskFactory-in FromAsync metodudur. Aşağıdakı kod nümunəsində mən WebRequest sinifinin köhnə asinxron üsullarını bu metoddan istifadə edərək Tapşırıqda əhatə edirəm.

object state = null;
WebRequest wr = WebRequest.CreateHttp("http://github.com");
await Task.Factory.FromAsync(
    wr.BeginGetResponse,
    we.EndGetResponse
);

Bu sadəcə bir nümunədir və çətin ki, daxili tiplərlə bunu etmək məcburiyyətindəsiniz, lakin hər hansı köhnə layihə sadəcə olaraq IAsyncResult-u qaytaran BeginDoSomething metodları və onu qəbul edən EndDoSomething metodları ilə doludur.

TaskCompletionSource Sinifindən istifadə edərək Legacy API-ni Tapşırıq Əsasına çevirmək

Nəzərə alınmalı olan digər vacib vasitə sinifdir Tapşırıq Tamamlama Mənbəsi. Funksiyaları, məqsədi və iş prinsipi baxımından yuxarıda yazdığım ThreadPool sinfinin RegisterWaitForSingleObject metoduna bir qədər bənzəyir. Bu sinifdən istifadə edərək, Tasks-da köhnə asinxron API-ləri asanlıqla və rahat şəkildə bağlaya bilərsiniz.

Deyəcəksiniz ki, mən artıq bu məqsədlə TaskFactory sinfinin FromAsync metodundan danışmışam. Burada Microsoft-un son 15 il ərzində təklif etdiyi .net-də asinxron modellərin inkişafının bütün tarixini xatırlamalıyıq: Tapşırıq Əsaslı Asinxron Modeldən (TAP) əvvəl, Asinxron Proqramlaşdırma Modeli (APP) mövcud idi. üsullarla bağlı idi BaşlamaqDoSomething qayıdır IAsyncResult və üsulları sonDoSomething onun ev sahibi və bu illərin mirası üçün FromAsync metodu sadəcə əladır, lakin zaman keçdikcə o, Hadisə Əsaslı Asinxron Nümunə ilə əvəz olundu (EAP), asinxron əməliyyat tamamlandıqda hadisənin qaldırılacağını güman edirdi.

TaskCompletionSource hadisə modeli ətrafında qurulmuş Tapşırıq və köhnə API-ləri bağlamaq üçün mükəmməldir. Onun işinin mahiyyəti belədir: bu sinfin obyekti Task tipli ictimai xassə malikdir, onun vəziyyəti TaskCompletionSource sinfinin SetResult, SetException və s. metodları vasitəsilə idarə oluna bilər. Gözləmə operatorunun bu Tapşırığa tətbiq edildiyi yerlərdə, TaskCompletionSource-a tətbiq edilən metoddan asılı olaraq, istisna olmaqla, icra ediləcək və ya qəzaya uğrayacaq. Əgər hələ də aydın deyilsə, onda gəlin bu kod nümunəsinə baxaq, burada bəzi köhnə EAP dövrünün API-si TaskCompletionSource istifadə edərək Tapşırığa bükülür: hadisə işə salındıqda Tapşırıq Tamamlanmış vəziyyətinə təyin olunacaq və onu tətbiq edən metod Bu Tapşırığı gözləyən operator obyekt əldə edərək icrasını davam etdirəcək nəticələnəcək.

public static Task<Result> DoAsync(this SomeApiInstance someApiObj) {

    var completionSource = new TaskCompletionSource<Result>();
    someApiObj.Done += 
        result => completionSource.SetResult(result);
    someApiObj.Do();

    result completionSource.Task;
}

Tapşırığın Tamamlanması Mənbə Məsləhətləri və Fəndləri

Köhnə API-ləri bağlamaq TaskCompletionSource ilə edə biləcəyiniz tək şey deyil. Bu sinifdən istifadə mövzuları tutmayan Tapşırıqlarda müxtəlif API-lərin layihələndirilməsi üçün maraqlı imkanlar açır. Və axın, xatırladığımız kimi, bahalı bir mənbədir və onların sayı məhduddur (əsasən RAM miqdarı ilə). Məsələn, mürəkkəb iş məntiqi ilə yüklənmiş veb tətbiqini inkişaf etdirərkən bu məhdudiyyətə nail olmaq asandır. Uzun səsvermə kimi bir hiylənin həyata keçirilməsi ilə bağlı bəhs etdiyim imkanları nəzərdən keçirək.

Qısacası, hiylənin mahiyyəti belədir: API-dən onun tərəfində baş verən bəzi hadisələr haqqında məlumat almaq lazımdır, halbuki API nədənsə hadisə haqqında məlumat verə bilməz, ancaq vəziyyəti qaytara bilər. Buna misal olaraq WebSocket dövründən əvvəl və ya nədənsə bu texnologiyadan istifadə etmək mümkün olmadıqda HTTP üzərində qurulmuş bütün API-ləri göstərmək olar. Müştəri HTTP serverindən soruşa bilər. HTTP serveri özbaşına müştəri ilə əlaqə qura bilməz. Sadə bir həll serveri taymerdə sorğulamaqdır, lakin bu, serverdə əlavə yük və orta hesabla əlavə gecikmə TimerInterval / 2 yaradır. Bunun qarşısını almaq üçün serverdən cavabın gecikdirilməsini nəzərdə tutan Long Polling adlı hiylə icad edilmişdir. Taymout başa çatana və ya hadisə baş verənə qədər. Əgər hadisə baş veribsə, ona baxılır, baş verməyibsə, sorğu yenidən göndərilir.

while(!eventOccures && !timeoutExceeded)  {

  CheckTimout();
  CheckEvent();
  Thread.Sleep(1);
}

Ancaq hadisəni gözləyən müştərilərin sayı artdıqca belə bir həll dəhşətli olacaq, çünki Hər bir belə müştəri hadisə gözləyərkən bütöv bir mövzu tutur. Bəli və hadisə işə salındıqda əlavə 1 ms gecikmə alırıq, çox vaxt bu əhəmiyyətli deyil, amma niyə proqramı ola biləcəyindən daha pis edirsiniz? Thread.Sleep(1)-i çıxarsaq, boş yerə bir prosessor nüvəsini 100% boş vəziyyətə yükləyirik, faydasız bir dövrədə fırlanır. TaskCompletionSource istifadə edərək, bu kodu asanlıqla yenidən yaza və yuxarıda göstərilən bütün problemləri həll edə bilərsiniz:

class LongPollingApi {

    private Dictionary<int, TaskCompletionSource<Msg>> tasks;

    public async Task<Msg> AcceptMessageAsync(int userId, int duration) {

        var cs = new TaskCompletionSource<Msg>();
        tasks[userId] = cs;
        await Task.WhenAny(Task.Delay(duration), cs.Task);
        return cs.Task.IsCompleted ? cs.Task.Result : null;
    }

    public void SendMessage(int userId, Msg m) {

        if (tasks.TryGetValue(userId, out var completionSource))
            completionSource.SetResult(m);
    }
}

Bu kod istehsala hazır deyil, sadəcə demodur. Real hallarda istifadə etmək üçün siz həmçinin heç kimin gözləmədiyi vaxtda mesaj gəldiyi zaman vəziyyəti idarə etməlisiniz: bu halda AsseptMessageAsync metodu artıq tamamlanmış Tapşırığı qaytarmalıdır. Bu hal ən tez-tez baş verirsə, ValueTask-dan istifadə etmək barədə düşünə bilərsiniz.

Mesaj üçün sorğu alanda biz TaskCompletionSource yaradıb lüğətə yerləşdiririk və sonra ilk olaraq nə baş verəcəyini gözləyirik: göstərilən vaxt intervalı başa çatır və ya mesaj qəbul edilir.

ValueTask: niyə və necə

Async/await ifadələri, gəlir qaytarma ifadəsi kimi, metoddan vəziyyət maşını yaradır və bu, demək olar ki, həmişə vacib olmayan, lakin nadir hallarda problem yarada bilən yeni obyektin yaradılmasıdır. Bu hal həqiqətən tez-tez çağırılan bir üsul ola bilər, biz saniyədə onlarla və yüz minlərlə zəngdən danışırıq. Əgər belə bir metod elə yazılıbsa ki, əksər hallarda bütün gözləmə metodlarından yan keçərək nəticəni qaytarır, onda .NET bunu optimallaşdırmaq üçün alət - ValueTask strukturunu təqdim edir. Bunu aydınlaşdırmaq üçün onun istifadəsinə dair bir nümunə nəzərdən keçirək: çox vaxt getdiyimiz bir önbellek var. Orada bəzi dəyərlər var və sonra biz onları sadəcə qaytarırıq, əgər yoxsa, onlardan sonra yavaş IO-ya keçirik. Sonuncunu asinxron etmək istəyirəm, yəni bütün metod asinxron olur. Beləliklə, bir metod yazmağın açıq yolu aşağıdakı kimidir:

public async Task<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return val;
    return await RequestById(id);
}

Bir az optimallaşdırmaq istəyindən və bu kodu tərtib edərkən Roslyn-in nə yaradacağına dair bir az qorxudan bu nümunəni aşağıdakı kimi yenidən yazmaq olar:

public Task<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return Task.FromResult(val);
    return RequestById(id);
}

Həqiqətən də, bu vəziyyətdə optimal həll isti yolu optimallaşdırmaq olardı, yəni heç bir lazımsız ayırmalar və GC-yə yükləmədən lüğətdən dəyər əldə etmək, nadir hallarda isə hələ də IO-ya getməli olduğumuz zaman. məlumatlar, hər şey köhnə şəkildə artı / mənfi olaraq qalacaq:

public ValueTask<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return new ValueTask<string>(val);
    return new ValueTask<string>(RequestById(id));
}

Bu kod fraqmentinə daha yaxından nəzər salaq: əgər önbellekdə dəyər varsa, struktur yaradırıq, əks halda real tapşırıq mənalı birinə büküləcək. Çağırılan kodun bu kodun hansı yolda icra olunduğuna əhəmiyyət vermir: ValueTask, C# sintaksisi baxımından bu halda adi Tapşırıq kimi davranacaq.

TaskSchedulers: Tapşırıqları işə salmaq üçün strategiyaların idarə edilməsi

Nəzərə almaq istədiyim növbəti API sinifdir Tapşırıq Planlayıcısı və onun törəmələri. Yuxarıda qeyd etdim ki, TPL tapşırıqları mövzular arasında paylamaq üçün strategiyaları idarə etmək qabiliyyətinə malikdir. Bu cür strategiyalar TaskScheduler sinifinin varislərində müəyyən edilir. Demək olar ki, sizə lazım ola biləcək hər hansı bir strategiya kitabxanada tapılacaq Paralel Extensions Əlavələr, microsoft tərəfindən hazırlanmış, lakin .NET-in bir hissəsi deyil, Nuget paketi kimi təqdim edilmişdir. Onlardan bəzilərinə qısaca nəzər salaq:

  • CurrentThreadTaskScheduler - cari mövzuda Tapşırıqları yerinə yetirir
  • LimitedConcurrencyLevelTaskScheduler - konstruktorda qəbul edilən N parametri ilə eyni vaxtda yerinə yetirilən Tapşırıqların sayını məhdudlaşdırır
  • SifarişliTaskScheduler - LimitedConcurrencyLevelTaskScheduler(1) kimi müəyyən edilir, ona görə də tapşırıqlar ardıcıllıqla yerinə yetiriləcək.
  • WorkStealingTaskScheduler - həyata keçirir iş oğurluğu tapşırıqların bölüşdürülməsi yanaşması. Əslində, bu ayrı bir ThreadPooldur. Problemi həll edir ki, .NET ThreadPool-da bütün proqramlar üçün bir statik sinifdir, yəni proqramın bir hissəsində onun həddən artıq yüklənməsi və ya sui-istifadəsi digərində yan təsirlərə səbəb ola bilər. Üstəlik, bu cür qüsurların səbəbini anlamaq son dərəcə çətindir. Bu. ThreadPool-dan istifadənin aqressiv və gözlənilməz ola biləcəyi proqramın həmin hissələrində ayrıca WorkStealingTaskSchedulers-dən istifadə etmək zərurəti yarana bilər.
  • QueuedTaskScheduler - prioritet növbə qaydalarına uyğun olaraq tapşırıqları yerinə yetirməyə imkan verir
  • ThreadPerTaskScheduler - üzərində icra olunan hər bir Tapşırıq üçün ayrıca mövzu yaradır. Tamamlanması gözlənilməz miqdarda vaxt alan vəzifələr üçün faydalı ola bilər.

Yaxşı detalı var məqalə Microsoft bloqunda TaskSchedulers haqqında.

Tapşırıqlarla əlaqəli hər şeyin rahat şəkildə sazlanması üçün Visual Studio-da Tapşırıqlar pəncərəsi var. Bu pəncərədə siz tapşırığın cari vəziyyətini görə və hazırda icra olunan kod xəttinə keçə bilərsiniz.

.NET: Multithreading və asinxroniya ilə işləmək üçün alətlər. 1-ci hissə

PLinq və Paralel sinfi

Tapşırıqlar və .NET-də deyilən hər şeydən əlavə, daha iki maraqlı alət var: PLinq(Linq2Parallel) və Parallel sinfi. Birincisi, bütün Linq əməliyyatlarını paralel olaraq birdən çox mövzuda yerinə yetirməyi vəd edir. Mövzuların sayı WithDegreeOfParallelism genişləndirmə metodu ilə konfiqurasiya edilə bilər. Təəssüf ki, əksər hallarda PLinq öz standart rejimində əhəmiyyətli sürət artımını təmin etmək üçün məlumat mənbəyinizin daxili hissələri haqqında kifayət qədər məlumata malik olmayacaq, digər tərəfdən cəhdin dəyəri çox aşağıdır: sadəcə olaraq AsParallel metodunu çağırmaq lazımdır. Linq metod silsiləsi və performans testləri həyata keçirin. Bundan əlavə, Bölmə mexanizmindən istifadə edərək məlumat mənbəyinizin təbiəti haqqında əlavə məlumatı PLinq-ə ötürmək mümkündür. Ətraflı oxuya bilərsiniz burada и burada.

Statik Parallel sinfi paralel olaraq Foreach kolleksiyası vasitəsilə iterasiya, For döngəsini icra etmək və Invoke paralelində çoxsaylı nümayəndələr yerinə yetirmək üçün üsulları təmin edir. Cari ipin icrası hesablamalar tamamlanmazdan əvvəl dayandırılacaq. Mövzuların sayı ParallelOptions-ı sonuncu arqument kimi ötürməklə konfiqurasiya edilə bilər. Siz həmçinin opsiyalardan istifadə edərək TaskScheduler və CancellationToken təyin edə bilərsiniz.

Tapıntılar

Hesabatımın materialları və ondan sonra işim zamanı topladığım məlumatlar əsasında bu yazını yazmağa başlayanda bunun bu qədər çox olacağını gözləmirdim. İndi bu məqaləni yazdığım mətn redaktoru mənə məzəmmətlə 15-ci səhifənin getdiyini söyləyəndə, aralıq nəticələri ümumiləşdirəcəyəm. Digər fəndlər, API-lər, vizual alətlər və tələlər növbəti məqalədə müzakirə olunacaq.

Sonuç:

  • Müasir fərdi kompüterlərin resurslarından istifadə etmək üçün iplərlə işləmək, asinxroniya və paralellik alətlərini bilməlisiniz.
  • Bunun üçün .NET-də çoxlu müxtəlif alətlər mövcuddur.
  • Onların hamısı bir anda görünmədi, çünki siz tez-tez köhnə olanları tapa bilərsiniz, lakin köhnə API-ləri çox səy göstərmədən çevirməyin yolları var.
  • .NET-də Threading Thread və ThreadPool sinifləri ilə təmsil olunur.
  • Thread.Abort, Thread.Interrupt, TerminateThread Win32 API funksiyaları təhlükəlidir və tövsiyə edilmir. Bunun əvəzinə CancellationTokens mexanizmindən istifadə etmək daha yaxşıdır
  • Axın qiymətli resursdur, onların sayı məhduddur. Mövzuların hadisələri gözləməklə məşğul olduğu vəziyyətlərdən çəkinin. Bunun üçün TaskCompletionSource sinfindən istifadə etmək rahatdır.
  • Paralellik və asinxroniya ilə məşğul olmaq üçün ən güclü və təkmil .NET alətləri Tapşırıqlardır.
  • c# async/await ifadələri bloklanmayan gözləmə konsepsiyasını həyata keçirir
  • Siz TaskScheduler törəmə siniflərindən istifadə edərək mövzular arasında Tapşırıqların paylanmasına nəzarət edə bilərsiniz
  • ValueTask strukturu isti yolların və yaddaş trafikinin optimallaşdırılmasında faydalı ola bilər
  • Visual Studio-nun Tapşırıqlar və Mövzular pəncərələri çox yivli və ya asinxron kodu aradan qaldırmaq üçün çoxlu faydalı məlumat verir.
  • PLinq əla vasitədir, lakin məlumat mənbəyiniz haqqında kifayət qədər məlumata malik olmaya bilər, lakin bu, bölmə mexanizmindən istifadə etməklə düzəldilə bilər.
  • Davam etmək üçün ...

Mənbə: www.habr.com

Добавить комментарий