.NET: көп ағынды және асинхрониямен жұмыс істеу құралдары. 1 бөлім

Мен Habr туралы түпнұсқа мақаланы жариялап жатырмын, оның аудармасы корпорацияда орналастырылған блог жазбасы.

Нәтижені осы жерде және қазір күтпей-ақ асинхронды түрде жасау немесе үлкен жұмысты оны орындайтын бірнеше блоктар арасында бөлу қажеттілігі компьютерлер пайда болғанға дейін болды. Олардың пайда болуымен бұл қажеттілік өте айқын болды. Енді, 2019 жылы мен бұл мақаланы 8 ядролы Intel Core процессоры бар ноутбукте теріп жатырмын, онда жүзден астам процесс параллель жұмыс істейді, одан да көп ағындар. Жақын жерде бірнеше жыл бұрын сатып алынған сәл ескірген телефон бар, оның бортында 8 ядролы процессоры бар. Тақырыптық ресурстар мақалалар мен бейнелерге толы, олардың авторлары 16 ядролы процессорлары бар биылғы флагмандық смартфондарды таңдайды. MS Azure 20 ядролы процессоры және 128 ТБ жедел жады бар виртуалды машинаны сағатына 2 доллардан азырақ қамтамасыз етеді. Өкінішке орай, жіптердің өзара әрекеттесуін басқара алмай, максималды алу және бұл қуатты пайдалану мүмкін емес.

Терминология

Процесс - ОЖ объектісі, оқшауланған мекенжай кеңістігі, ағындарды қамтиды.
Жіп - ОЖ объектісі, орындаудың ең кіші бірлігі, процестің бөлігі, ағындар процесс ішінде жадты және басқа ресурстарды өзара бөліседі.
Көп функция - ОЖ қасиеті, бір уақытта бірнеше процестерді орындау мүмкіндігі
Көп ядролы - процессордың қасиеті, мәліметтерді өңдеу үшін бірнеше ядроларды пайдалану мүмкіндігі
Көп өңдеу - компьютердің қасиеті, физикалық түрде бірнеше процессорлармен бір уақытта жұмыс істеу мүмкіндігі
Көп ағынды — процестің қасиеті, мәліметтерді өңдеуді бірнеше ағындар арасында тарату мүмкіндігі.
Параллелизм - уақыт бірлігінде физикалық бір уақытта бірнеше әрекеттерді орындау
Асинхрония — осы өңдеудің аяқталуын күтпестен операцияны орындау; орындау нәтижесі кейінірек өңделуі мүмкін.

Метафор

Барлық анықтамалар жақсы емес, ал кейбіреулері қосымша түсіндіруді қажет етеді, сондықтан мен ресми түрде енгізілген терминологияға таңғы ас дайындау туралы метафораны қосамын. Бұл метафорада таңғы ас дайындау - бұл процесс.

Таңертең таңғы ас әзірлеп жатқанда мен (Орталық Есептеуіш Бөлім) Мен ас үйге келдім (Компьютер). Менің 2 қолым бар (Коре). Ас үйде бірнеше құрылғылар бар (IO): пеш, шәйнек, тостер, тоңазытқыш. Газды қосып, табаны салып, қызғанын күтпей май құямын (асинхронды, блокталмайтын IO-күту), Мен жұмыртқаны тоңазытқыштан алып, тәрелкеге ​​бөлемін, сосын бір қолмен шайқаймын (№1 тақырып), және екінші (№2 тақырып) табақты ұстау (Ортақ ресурс). Енді мен шәйнекті қосқым келеді, бірақ қолым жетпейді (Жіптің аштығы) Осы уақыт ішінде таба қызады (Нәтижені өңдеу), оған мен шайқағанымды құямын. Мен шәйнекке қолымды созып, оны қосып, ондағы судың қайнап жатқанын ақымақтықпен бақылап отырмын (Блоктау-IO-күту), дегенмен, осы уақыт ішінде ол омлетті қамшылап жатқан табақты жууға болатын еді.

Мен омлетті тек 2 қолмен пісірдім, менде артық емес, бірақ сонымен бірге омлетті шайқау сәтінде бірден 3 операция жасалды: омлетті шайқау, табақты ұстау, қуыру табасын қыздыру Орталық процессор компьютердің ең жылдам бөлігі болып табылады, IO көбінесе бәрі баяулайды, сондықтан жиі тиімді шешім IO-дан деректерді қабылдау кезінде процессорды бір нәрсемен толтыру болып табылады.

Метафораны жалғастыру:

  • Егер омлетті дайындау барысында мен де киім ауыстыруға тырысатын болсам, бұл көп тапсырманың мысалы болар еді. Маңызды нюанс: компьютерлер адамдарға қарағанда әлдеқайда жақсы.
  • Бірнеше аспазшы бар ас үй, мысалы, мейрамханада - көп ядролы компьютер.
  • Сауда орталығындағы фуд-корттағы көптеген мейрамханалар - деректер орталығы

.NET құралдары

.NET көптеген басқа нәрселер сияқты ағындармен жақсы жұмыс істейді. Әрбір жаңа нұсқамен ол олармен жұмыс істеу үшін көбірек жаңа құралдарды, ОЖ ағындары бойынша абстракцияның жаңа қабаттарын ұсынады. Абстракцияларды құрастырумен жұмыс істегенде, фреймворк әзірлеушілері жоғары деңгейлі абстракцияны пайдалану кезінде бір немесе бірнеше деңгейлерден төмен түсу мүмкіндігін қалдыратын тәсілді пайдаланады. Көбінесе бұл қажет емес, шын мәнінде ол тапаншамен аяққа атуға жол ашады, бірақ кейде, сирек жағдайларда, бұл қазіргі абстракция деңгейінде шешілмеген мәселені шешудің жалғыз жолы болуы мүмкін. .

Құралдар деп мен рамкамен және үшінші тарап пакеттерімен қамтамасыз етілген қолданбалы бағдарламалау интерфейстерін (API), сонымен қатар көп ағынды кодқа қатысты кез келген мәселелерді іздеуді жеңілдететін тұтас бағдарламалық шешімдерді айтамын.

Жіпті бастау

Thread класы ағындармен жұмыс істеуге арналған .NET жүйесіндегі ең негізгі класс болып табылады. Конструктор екі делегаттың бірін қабылдайды:

  • ThreadStart — Параметрлер жоқ
  • ParametrizedThreadStart - типті нысанның бір параметрімен.

Делегат Start әдісін шақырғаннан кейін жаңадан жасалған ағында орындалады.Егер ParametrizedThreadStart түріндегі делегат конструкторға жіберілген болса, онда нысан Start әдісіне берілуі керек. Бұл механизм кез келген жергілікті ақпаратты ағынға тасымалдау үшін қажет. Айта кету керек, ағынды жасау - қымбат операция, ал ағынның өзі ауыр нысан, кем дегенде, ол стекке 1 МБ жадты бөледі және OS API интерфейсімен әрекеттесуді қажет етеді.

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

ThreadPool сыныбы пул ұғымын білдіреді. .NET жүйесінде жіп пулы инженерлік жұмыстың бір бөлігі болып табылады және Microsoft әзірлеушілері оның әртүрлі сценарийлерде оңтайлы жұмыс істейтініне көз жеткізу үшін көп күш жұмсады.

Жалпы түсінік:

Қолданба іске қосылған сәттен бастап ол фондық режимде резервте бірнеше ағындарды жасайды және оларды пайдалану үшін қабылдау мүмкіндігін береді. Егер ағындар жиі және көп мөлшерде пайдаланылса, пул қоңырау шалушының қажеттіліктерін қанағаттандыру үшін кеңейеді. Пулда қажетті уақытта бос ағындар болмаған кезде, ол ағындардың біреуінің оралуын күтеді немесе жаңасын жасайды. Бұдан шығатыны, ағындар пулы кейбір қысқа мерзімді әрекеттер үшін тамаша және қолданбаның бүкіл жұмысында қызмет ретінде іске қосылатын операциялар үшін нашар сәйкес келеді.

Пулдағы ағынды пайдалану үшін ParametrizedThreadStart қолтаңбасы бар WaitCallback түріндегі делегат қабылдайтын QueueUserWorkItem әдісі бар және оған жіберілген параметр бірдей функцияны орындайды.

ThreadPool.QueueUserWorkItem(...);

Азырақ белгілі ағын пулының әдісі RegisterWaitForSingleObject блокталмаған IO операцияларын ұйымдастыру үшін пайдаланылады. Бұл әдіске жіберілген делегат әдіске жіберілген WaitHandle «Шығарылған» болғанда шақырылады.

ThreadPool.RegisterWaitForSingleObject(...)

.NET жүйесінде ағын таймері бар және оның WinForms/WPF таймерлерінен айырмашылығы, оның өңдеушісі пулдан алынған ағында шақырылады.

System.Threading.Timer

Пулдан ағынға делегат жіберудің экзотикалық әдісі де бар - BeginInvoke әдісі.

DelegateInstance.BeginInvoke

Мен жоғарыда аталған әдістердің көпшілігін шақыруға болатын функцияға қысқаша тоқталғым келеді - Kernel32.dll Win32 API-дан CreateThread. Экстерн әдістерінің механизмінің арқасында бұл функцияны шақырудың жолы бар. Мен мұндай қоңырауды мұралық кодтың қорқынышты мысалында бір-ақ рет көрдім, ал дәл осылай жасаған автордың мотивациясы мен үшін әлі күнге дейін құпия болып қала береді.

Kernel32.dll CreateThread

Ағындарды қарау және жөндеу

Сіз жасаған ағындарды, барлық үшінші тарап құрамдастарын және .NET пулын Visual Studio бағдарламасының Threads терезесінде көруге болады. Бұл терезе тек бағдарлама жөндеуде және үзіліс режимінде болғанда ағын туралы ақпаратты көрсетеді. Мұнда әрбір ағынның стек атаулары мен басымдықтарын ыңғайлы түрде көруге және жөндеуді белгілі бір ағынға ауыстыруға болады. Thread сыныбының Priority қасиетін пайдалана отырып, OC және CLR процессор уақытын ағындар арасында бөлу кезінде ұсыныс ретінде қабылдайтын ағынның басымдылығын орнатуға болады.

.NET: көп ағынды және асинхрониямен жұмыс істеу құралдары. 1 бөлім

Тапсырма параллельді кітапхана

Task Parallel Library (TPL) .NET 4.0 жүйесінде енгізілді. Енді бұл асинхрониямен жұмыс істеудің стандартты және негізгі құралы. Ескі тәсілді пайдаланатын кез келген код мұра болып саналады. TPL негізгі бірлігі System.Threading.Tasks аттар кеңістігіндегі Task класы болып табылады. Тапсырма - бұл ағынның үстіндегі абстракция. C# тілінің жаңа нұсқасы арқылы біз Tasks - async/wait операторларымен жұмыс істеудің талғампаз әдісін алдық. Бұл түсініктер асинхронды кодты қарапайым және синхронды сияқты жазуға мүмкіндік берді, бұл тіпті ағындардың ішкі жұмысын түсінбейтін адамдарға оларды пайдаланатын қолданбаларды, ұзақ операцияларды орындау кезінде қатып қалмайтын қолданбаларды жазуға мүмкіндік берді. Асинхронды/күтуді пайдалану бір немесе тіпті бірнеше мақалаларға арналған тақырып, бірақ мен оның мәнін бірнеше сөйлеммен алуға тырысамын:

  • async - тапсырманы немесе void қайтаратын әдіс модификаторы
  • және wait — блокталмаған Тапсырманы күту операторы.

Тағы да: күту операторы жалпы жағдайда (ерекшеліктер бар) ағымдағы орындау ағынын одан әрі босатады және Тапсырма өзінің орындалуын аяқтаған кезде және ағынды (шын мәнінде контекстті айту дұрысырақ болар еді) , бірақ бұл туралы кейінірек) әдісті одан әрі орындауды жалғастырады. .NET ішінде бұл механизм жазбаша әдіс күй машинасы болып табылатын және осы күйлерге байланысты бөлек бөліктерде орындалуы мүмкін тұтас классқа айналғанда, кірісті қайтару сияқты жүзеге асырылады. Кез келген қызығушылық танытқан адам asyns/await көмегімен кез келген қарапайым кодты жаза алады, компилятордың құрылған коды қосылған JetBrains dotPeek көмегімен құрастыруды құрастыра және көре алады.

Тапсырманы іске қосу және пайдалану опцияларын қарастырайық. Төмендегі код мысалында біз пайдалы ештеңе жасамайтын жаңа тапсырма жасаймыз (Thread.Sleep(10000)), бірақ нақты өмірде бұл күрделі процессорды қажет ететін жұмыс болуы керек.

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
}

Тапсырма бірнеше опциялармен жасалады:

  • LongRunning - бұл тапсырманың тез орындалмайтынын білдіреді, яғни басқаларға зиян тигізбеу үшін пулдан жіп алмай, бірақ осы Тапсырма үшін бөлек жасауды қарастырған жөн.
  • AttachedToParent - Тапсырмаларды иерархияда реттеуге болады. Егер бұл опция пайдаланылса, онда Тапсырма өзі орындалған және оның балаларының орындалуын күтіп тұрған күйде болуы мүмкін.
  • PreferFairness – орындауға жіберілген тапсырмаларды кейінірек жіберілгеннен бұрын орындаған дұрыс дегенді білдіреді. Бірақ бұл тек ұсыныс және нәтижеге кепілдік берілмейді.

Әдіске жіберілген екінші параметр CancellationToken болып табылады. Операция басталғаннан кейін оны болдырмауды дұрыс өңдеу үшін орындалатын код CancellationToken күйіне тексерулермен толтырылуы керек. Егер тексерулер болмаса, CancellationTokenSource нысанында шақырылған Cancel әдісі Тапсырманың орындалуын тек ол басталғанға дейін тоқтата алады.

Соңғы параметр TaskScheduler түріндегі жоспарлаушы нысаны болып табылады. Бұл сынып және оның ұрпақтары ағындар бойынша тапсырмаларды тарату стратегияларын басқаруға арналған; әдепкі бойынша, тапсырма пулдан кездейсоқ ағында орындалады.

Күту операторы жасалған тапсырмаға қолданылады, яғни одан кейін жазылған код, егер бар болса, күту алдындағы код сияқты контексте (көбінесе бұл бір ағында дегенді білдіреді) орындалады.

Әдіс асинхронды жарамсыз деп белгіленген, яғни күту операторын пайдалана алады, бірақ шақыру коды орындалуды күте алмайды. Егер мұндай мүмкіндік қажет болса, әдіс тапсырманы қайтаруы керек. Асинхронды жарамсыз деп белгіленген әдістер өте кең таралған: әдетте бұл оқиғаларды өңдеушілер немесе өрт және ұмыту принципінде жұмыс істейтін басқа әдістер. Егер сізге орындаудың соңына дейін күтуге мүмкіндік беріп қана қоймай, нәтижені қайтару қажет болса, онда тапсырманы пайдалану керек.

StartNew әдісі қайтарған тапсырмада, сондай-ақ кез келген басқа нұсқада жалған параметрмен ConfigureAwait әдісін шақыруға болады, содан кейін күтуден кейін орындау түсірілген контексте емес, ерікті түрде жалғасады. Күтуден кейін орындау контексі код үшін маңызды болмаса, мұны әрқашан жасау керек. Бұл сонымен қатар кітапханада оралатын кодты жазу кезінде MS ұсынған ұсыныс.

Тапсырманың аяқталуын қалай күтуге болатыны туралы толығырақ тоқталайық. Төменде күтудің шартты түрде жақсы орындалғаны және шартты түрде нашар орындалғаны туралы түсініктемелері бар код үлгісі берілген.

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
}

Бірінші мысалда біз шақырушы ағынды бұғаттамай Тапсырманың аяқталуын күтеміз; нәтиже бар болған кезде ғана өңдеуге ораламыз; оған дейін шақырушы ағын өз құрылғыларында қалады.

Екінші нұсқада әдістің нәтижесі есептелгенше шақырушы ағынды блоктаймыз. Бұл нашар, өйткені біз бағдарламаның осындай құнды ресурсын қарапайым бос тұрып алғандықтан ғана емес, сонымен қатар, егер біз шақыратын әдістің коды күтуді қамтитын болса және синхрондау контексті шақырушы ағынға қайта оралуды талап ететіндіктен де жаман. күтіңіз, содан кейін біз тығырықтан шығамыз : Шақырушы ағын асинхронды әдістің нәтижесін есептеуді күтеді, асинхронды әдіс шақырушы ағында орындалуын жалғастыру үшін бекер әрекет жасайды.

Бұл тәсілдің тағы бір кемшілігі қателерді өңдеудің күрделілігі болып табылады. Шындығында, асинхронды кодтағы қателерді асинхронды/күтуді пайдалану кезінде өңдеу өте оңай - олар код синхронды болған сияқты әрекет етеді. Тапсырмаға синхронды күту экзорцизмін қолданатын болсақ, бастапқы ерекшелік AggregateException түріне айналады, яғни. Ерекше жағдайды өңдеу үшін сізге InnerException түрін тексеру керек және C# әлемінде көбірек таныс catch блоктарының тізбегінің орнына бір catch блогының ішіне if тізбегін өзіңіз жазуыңыз немесе құрастыру кезінде catch пайдалануыңыз керек.

Үшінші және соңғы мысалдар да сол себепті нашар деп белгіленіп, барлығы бірдей проблемаларды қамтиды.

WhenAny және WhenAll әдістері Тапсырмалар тобын күту үшін өте ыңғайлы; олар Тапсырмалар тобын біреуіне жинайды, олар топтан Тапсырма алғаш рет іске қосылғанда немесе олардың барлығы орындалып болған кезде іске қосылады.

Жіптерді тоқтату

Әртүрлі себептерге байланысты ағын басталғаннан кейін оны тоқтату қажет болуы мүмкін. Мұны істеудің бірнеше жолы бар. Thread сыныбында екі дұрыс аталған әдіс бар: Аяқтау и сөзінүзу. Біріншісін пайдалану ұсынылмайды, өйткені оны кез келген кездейсоқ сәтте шақырғаннан кейін, кез келген нұсқауды өңдеу кезінде ерекшелік тасталады ThreadAbortedException. Кез келген бүтін айнымалы мәнді ұлғайту кезінде мұндай ерекшелік жойылады деп күтпейсіз бе? Және бұл әдісті қолданғанда, бұл өте нақты жағдай. Егер сізге CLR кодтың белгілі бір бөлімінде мұндай ерекшелік жасауына жол бермеу қажет болса, оны қоңырауларға орауға болады. Thread.BeginCriticalRegion, Thread.EndCriticalRegion. Finally блогында жазылған кез келген код мұндай қоңырауларға оралады. Осы себепті, рамкалық кодтың тереңдігінде сіз бос әрекеті бар блоктарды таба аласыз, бірақ соңында бос емес. Майкрософт бұл әдісті қабылдамағаны сонша, олар оны .net ядросына қоспады.

Үзу әдісі болжамды түрде жұмыс істейді. Ол ағынды ерекше жағдайда үзуі мүмкін ThreadInterruptedException жіп күту күйінде болған сәттерде ғана. Бұл күйге WaitHandle күту, құлыптау немесе Thread.Sleep шақырғаннан кейін ілу кезінде кіреді.

Жоғарыда сипатталған екі нұсқа да күтпегендіктен нашар. Шешім құрылымды пайдалану болып табылады CancellationToken және сынып CancellationTokenSource. Мәселе мынада: CancellationTokenSource класының данасы жасалады және оған иелік ететін адам ғана әдісті шақыру арқылы операцияны тоқтата алады. күшін жою. Тек CancellationToken операцияның өзіне жіберіледі. CancellationToken иелері операциядан бас тарта алмайды, бірақ операцияның тоқтатылғанын тексере алады. Бұл үшін логикалық сипат бар CancellationRequested және әдіс ThrowIfCancelRequested. Соңғысы ерекше жағдайды шығарады TaskCancelledException егер Cancel әдісі тотығатын CancellationToken данасында шақырылған болса. Және бұл әдісті мен қолдануға кеңес беремін. Бұл қай кезде ерекшелік операциясын тоқтатуға болатынын толық бақылауға алу арқылы алдыңғы опциялармен салыстырғанда жақсарту.

Ағынды тоқтатудың ең қатал опциясы - Win32 API TerminateThread функциясын шақыру. Бұл функцияны шақырғаннан кейін CLR әрекетін болжау мүмкін емес. MSDN-де бұл функция туралы келесідей жазылған: «TerminateThread - қауіпті функция, оны тек ең төтенше жағдайларда ғана пайдалану керек. «

Бұрынғы API файлын FromAsync әдісі арқылы Task Based түріне түрлендіру

Егер сіз Tasks енгізілгеннен кейін басталған жобада жұмыс істеу бақытына ие болсаңыз және көптеген әзірлеушілер үшін тыныш қорқынышты тудыруды тоқтатса, онда сізге көптеген ескі API интерфейстерімен, үшінші тараппен де, сіздің командаңызбен де айналысудың қажеті жоқ. бұрын азаптаған. Бақытымызға орай, .NET Framework тобы бізге қамқорлық жасады, бірақ мақсатымыз өзімізге қамқорлық болған шығар. Қалай болғанда да, .NET-те ескі асинхронды бағдарламалау тәсілдерінде жазылған кодты жаңасына ауыртпалықсыз түрлендіруге арналған бірқатар құралдар бар. Олардың бірі - TaskFactory-тің FromAsync әдісі. Төмендегі код мысалында мен WebRequest сыныбының ескі асинхронды әдістерін осы әдісті пайдаланып Тапсырмаға орап аламын.

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

Бұл жай ғана мысал және сіз мұны кірістірілген түрлермен жасауыңыз екіталай, бірақ кез келген ескі жоба оны қабылдайтын IAsyncResult және EndDoSomething әдістерін қайтаратын BeginDoSomething әдістеріне толы.

Бұрынғы API интерфейсін TaskCompletionSource сыныбы арқылы Task Based түріне түрлендіру

Тағы бір маңызды құрал - бұл сынып Тапсырманы аяқтау көзі. Функциялары, мақсаты және жұмыс істеу принципі бойынша ол мен жоғарыда жазған ThreadPool класының RegisterWaitForSingleObject әдісін біршама еске түсіруі мүмкін. Осы сыныпты пайдалана отырып, Tasks ішінде ескі асинхронды API интерфейстерін оңай және ыңғайлы орап алуға болады.

Мен осы мақсаттарға арналған TaskFactory сыныбының FromAsync әдісі туралы айттым деп айтасыз. Бұл жерде біз Microsoft соңғы 15 жыл ішінде ұсынған .net жүйесіндегі асинхронды үлгілердің бүкіл даму тарихын еске түсіруіміз керек: тапсырмаға негізделген асинхронды үлгі (TAP) алдында асинхронды бағдарламалау үлгісі (APP) болды, ол әдістері туралы болды БАСТАDoSomething қайтарылады IAsyncResult және әдістері СоңыОны қабылдайтын DoSomething және осы жылдардағы мұра үшін FromAsync әдісі өте жақсы, бірақ уақыт өте ол оқиғаға негізделген асинхронды үлгімен ауыстырылды (EAP), ол асинхронды операция аяқталған кезде оқиға көтеріледі деп болжанған.

TaskCompletionSource оқиға үлгісінің айналасында құрылған тапсырмалар мен бұрынғы API интерфейстерін орау үшін тамаша. Оның жұмысының мәні мынада: бұл класс объектісінің Task типті жалпы қасиеті бар, оның күйін TaskCompletionSource класының SetResult, SetException және т.б. әдістері арқылы басқаруға болады. Күту операторы осы тапсырмаға қолданылған жерлерде ол TaskCompletionSource қолданылған әдіске байланысты ерекше жағдайда орындалады немесе орындалады. Егер ол әлі түсініксіз болса, кейбір ескі EAP API TaskCompletionSource арқылы тапсырмаға оралған осы код мысалын қарастырайық: оқиға іске қосылғанда, тапсырма Аяқталған күйіне және күту операторын қолданған әдіске ауыстырылады. Осы Тапсырма объектіні алғаннан кейін оның орындалуын жалғастырады нәтиже.

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

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

    result completionSource.Task;
}

Тапсырманы аяқтау көзі туралы кеңестер мен амалдар

Ескі API интерфейстерін орау TaskCompletionSource көмегімен жасалуы мүмкін нәрсе емес. Бұл сыныпты пайдалану ағындарды пайдаланбайтын тапсырмаларда әртүрлі API интерфейстерін жобалаудың қызықты мүмкіндігін ашады. Ал ағын, біздің есімізде, қымбат ресурс және олардың саны шектеулі (негізінен ЖЖҚ көлемі бойынша). Бұл шектеуге, мысалы, күрделі бизнес логикасы бар жүктелген веб-қосымшаны әзірлеу арқылы оңай қол жеткізуге болады. Мен айтып отырған мүмкіндіктерді Long-Polling сияқты трюкті жүзеге асырған кезде қарастырайық.

Қысқаша айтқанда, трюктің мәні мынада: API-дан оның жағында болып жатқан кейбір оқиғалар туралы ақпаратты алу керек, ал API қандай да бір себептермен оқиға туралы хабарлай алмайды, тек күйді қайтара алады. Олардың мысалы ретінде WebSocket уақытына дейін немесе қандай да бір себептермен бұл технологияны пайдалану мүмкін болмаған кезде HTTP үстіне салынған барлық API интерфейстері болып табылады. Клиент HTTP серверінен сұрай алады. HTTP сервері клиентпен байланысты өзі бастай алмайды. Қарапайым шешім - серверді таймер арқылы сұрау, бірақ бұл серверде қосымша жүктемені және орташа TimerInterval / 2 бойынша қосымша кешіктіруді тудырады. Бұны айналып өту үшін Long Polling деп аталатын трюк ойлап табылды, ол жауапты кейінге қалдыруды қамтиды. серверді күту уақыты біткенше немесе оқиға орын алғанша. Оқиға орын алса, ол өңделеді, егер жоқ болса, сұрау қайтадан жіберіледі.

while(!eventOccures && !timeoutExceeded)  {

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

Бірақ мұндай шешім оқиғаны күткен клиенттердің саны артқан сайын қорқынышты болады, өйткені... Әрбір осындай клиент оқиғаны күтіп тұрған бүкіл ағынды алады. Иә, оқиға іске қосылғанда біз қосымша 1 мс кідіріс аламыз, көбінесе бұл маңызды емес, бірақ бағдарламалық жасақтаманы неге одан да нашарлату керек? Егер біз Thread.Sleep(1)-ді алып тастасақ, онда біз пайдасыз циклде айналатын процессордың бір ядросын 100% бос күйде жүктейміз. TaskCompletionSource көмегімен сіз осы кодты оңай қайта жасай аласыз және жоғарыда анықталған барлық мәселелерді шеше аласыз:

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);
    }
}

Бұл код өндіріске дайын емес, жай демонстрация. Оны нақты жағдайларда пайдалану үшін, кем дегенде, хабар ешкім күтпеген уақытта келген жағдайды өңдеу қажет: бұл жағдайда AsseptMessageAsync әдісі аяқталған тапсырманы қайтаруы керек. Бұл ең жиі кездесетін жағдай болса, ValueTask пайдалану туралы ойлануға болады.

Хабарлама сұрауын алған кезде біз сөздікте TaskCompletionSource жасап, орналастырамыз, содан кейін алдымен не болатынын күтеміз: көрсетілген уақыт аралығы аяқталады немесе хабар алынады.

ValueTask: неге және қалай

Асинхронды/күту операторлары кірісті қайтару операторы сияқты әдістен күй машинасын жасайды және бұл әрқашан дерлік маңызды емес жаңа нысанды жасау, бірақ сирек жағдайларда ол мәселені тудыруы мүмкін. Бұл жағдай шынымен жиі шақырылатын әдіс болуы мүмкін, біз секундына ондаған және жүздеген мың қоңыраулар туралы айтып отырмыз. Егер мұндай әдіс көп жағдайда барлық күту әдістерін айналып өтетін нәтижені қайтаратындай етіп жазылса, .NET оны оңтайландыру құралын - ValueTask құрылымын ұсынады. Түсінікті болу үшін оны пайдаланудың мысалын қарастырайық: біз жиі кіретін кэш бар. Онда кейбір мәндер бар, содан кейін біз оларды қайтарамыз; егер жоқ болса, біз оларды алу үшін баяу IO-ға барамыз. Мен соңғысын асинхронды түрде орындағым келеді, яғни бүкіл әдіс асинхронды болып шығады. Осылайша, әдісті жазудың айқын жолы келесідей:

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

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

Кішкене оңтайландыруды қалағандықтан және осы кодты құрастырған кезде Рослин не тудыратынынан аздап қорқып, бұл мысалды келесідей қайта жазуға болады:

public Task<string> GetById(int id) {

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

Шынында да, бұл жағдайда оңтайлы шешім ыстық жолды оңтайландыру болар еді, атап айтқанда сөздіктен мәнді қажетсіз бөлусіз және GC жүктемесіз алу, ал сирек жағдайларда деректер үшін IO-ға бару қажет болады. , бәрі ескі жолмен плюс/минус болып қалады:

public ValueTask<string> GetById(int id) {

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

Осы код бөлігін толығырақ қарастырайық: кэште мән бар болса, біз құрылым жасаймыз, әйтпесе нақты тапсырма мағыналы тапсырмаға оралады. Қоңырау шалушы код бұл кодтың қай жолда орындалғаны маңызды емес: ValueTask, C# синтаксисі тұрғысынан, бұл жағдайда әдеттегі тапсырма сияқты әрекет етеді.

TaskSchedulers: тапсырмаларды іске қосу стратегияларын басқару

Мен қарастырғым келетін келесі API - бұл сынып Тапсырма жоспарлаушы және оның туындылары. Мен жоғарыда TPL-де ағындар бойынша тапсырмаларды тарату стратегияларын басқару мүмкіндігі бар екенін айттым. Мұндай стратегиялар TaskScheduler сыныбының ұрпақтарында анықталған. Сізге қажет болуы мүмкін кез келген стратегияны кітапханадан табуға болады. ParallelExtensionsExtras, Microsoft әзірлеген, бірақ .NET бөлігі емес, бірақ Nuget бумасы ретінде жеткізіледі. Олардың кейбіріне қысқаша тоқталайық:

  • CurrentThreadTaskScheduler — ағымдағы ағындағы тапсырмаларды орындайды
  • LimitedConcurrencyLevelTaskScheduler — конструкторда қабылданған N параметрі бойынша бір уақытта орындалатын Тапсырмалар санын шектейді
  • OrderedTaskScheduler — LimitedConcurrencyLevelTaskScheduler(1) ретінде анықталады, сондықтан тапсырмалар дәйекті түрде орындалады.
  • WorkStealingTaskScheduler - құралдар жұмысты ұрлау тапсырманы бөлу тәсілі. Негізінде бұл бөлек ThreadPool. .NET ThreadPool жүйесіндегі барлық қолданбаларға арналған статикалық класс болып табылатын мәселені шешеді, бұл оның шамадан тыс жүктелуі немесе бағдарламаның бір бөлігінде дұрыс пайдаланылмауы басқасында жанама әсерлерге әкелуі мүмкін дегенді білдіреді. Сонымен қатар, мұндай ақаулардың себебін түсіну өте қиын. Бұл. ThreadPool пайдалану агрессивті және болжау мүмкін емес болуы мүмкін бағдарламаның бөліктерінде бөлек WorkStealingTaskSchedulers пайдалану қажет болуы мүмкін.
  • QueuedTaskScheduler — приоритеттік кезек ережелері бойынша тапсырмаларды орындауға мүмкіндік береді
  • ThreadPerTaskScheduler — онда орындалатын әрбір Тапсырма үшін жеке ағын жасайды. Аяқтауға күтпеген ұзақ уақыт кететін тапсырмалар үшін пайдалы болуы мүмкін.

Жақсы деталь бар мақала Microsoft блогындағы TaskSchedulers туралы.

Тапсырмаларға қатысты барлық нәрсені ыңғайлы жөндеу үшін Visual Studio бағдарламасында Тапсырмалар терезесі бар. Бұл терезеде тапсырманың ағымдағы күйін көруге және кодтың ағымдағы орындалатын жолына өтуге болады.

.NET: көп ағынды және асинхрониямен жұмыс істеу құралдары. 1 бөлім

PLinq және Параллель класы

Тапсырмалардан және олар туралы айтылғандардың барлығынан басқа, .NET жүйесінде тағы екі қызықты құрал бар: PLinq (Linq2Parallel) және Parallel класы. Біріншісі бірнеше ағындардағы барлық Linq операцияларының параллель орындалуын уәде етеді. Ағындар санын WithDegreeOfParalelism кеңейтім әдісі арқылы конфигурациялауға болады. Өкінішке орай, көбінесе әдепкі режимінде PLinq айтарлықтай жылдамдықты арттыру үшін деректер көзінің ішкі бөліктері туралы жеткілікті ақпаратқа ие емес, екінші жағынан, әрекет ету құны өте төмен: сіз жай ғана AsParallel әдісіне қоңырау шалу керек. Linq әдістері тізбегі және өнімділік сынақтарын орындаңыз. Сонымен қатар, Бөлімдер механизмі арқылы деректер көзінің сипаты туралы PLinq-ке қосымша ақпаратты жіберуге болады. Толығырақ оқи аласыз осында и осында.

Parallel static класы Foreach жиыны арқылы параллельді қайталау, For циклін орындау және параллель Invoke ішінде бірнеше делегаттарды орындау әдістерін қамтамасыз етеді. Ағымдағы ағынның орындалуы есептеулер аяқталғанша тоқтатылады. Ағындардың санын ParallelOptions параметрін соңғы аргумент ретінде беру арқылы конфигурациялауға болады. Сондай-ақ опцияларды пайдаланып TaskScheduler және CancellationToken параметрлерін көрсетуге болады.

қорытындылар

Мен бұл мақаланы өз баяндамамның материалдары мен одан кейінгі жұмыс барысында жинаған мәліметтері бойынша жаза бастағанда, бұлай көп болады деп ойламаған едім. Енді мен осы мақаланы жазып жатқан мәтіндік редактор маған 15-беттің кеткенін сөгіспен айтқанда, мен аралық нәтижелерді қорытындылаймын. Басқа амалдар, API интерфейстері, көрнекі құралдар мен қателіктер келесі мақалада қарастырылады.

Қорытынды:

  • Қазіргі ДК ресурстарын пайдалану үшін ағындармен жұмыс істеу, асинхрония және параллелизм құралдарын білу қажет.
  • .NET-те осы мақсаттар үшін көптеген әртүрлі құралдар бар
  • Олардың барлығы бірден пайда болған жоқ, сондықтан сіз ескіргендерін жиі таба аласыз, дегенмен ескі API интерфейстерін көп күш жұмсамай түрлендірудің жолдары бар.
  • .NET жүйесінде ағындармен жұмыс Thread және ThreadPool сыныптары арқылы ұсынылған
  • Thread.Abort, Thread.Interrupt және Win32 API TerminateThread әдістері қауіпті және пайдалану ұсынылмайды. Оның орнына CancellationToken механизмін қолданған дұрыс
  • Ағын – құнды ресурс және оның жеткізілімі шектеулі. Жіптер оқиғаларды күтумен айналысатын жағдайлардан аулақ болу керек. Ол үшін TaskCompletionSource класын пайдалану ыңғайлы
  • Параллелизммен және асинхрониямен жұмыс істеуге арналған ең қуатты және жетілдірілген .NET құралдары Тапсырмалар болып табылады.
  • C# async/await операторлары блокталмаған күту тұжырымдамасын жүзеге асырады
  • TaskScheduler-дан алынған сыныптарды пайдаланып, ағындар бойынша тапсырмалардың таралуын басқара аласыз
  • ValueTask құрылымы жедел жолдар мен жад трафигін оңтайландыруда пайдалы болуы мүмкін
  • Visual Studio тапсырмалары мен ағындары терезелері көп ағынды немесе асинхронды кодты жөндеу үшін пайдалы көптеген ақпаратты қамтамасыз етеді.
  • PLinq - керемет құрал, бірақ оның деректер көзі туралы ақпарат жеткіліксіз болуы мүмкін, бірақ оны бөлу механизмі арқылы түзетуге болады.
  • Жалғасы бар…

Ақпарат көзі: www.habr.com

пікір қалдыру