.NET: Алатки за работа со повеќенишки и асинхрони. Дел 1

Ја објавувам оригиналната статија на Хабр, чиј превод е објавен во корпоративното блог пост.

Потребата да се направи нешто асинхроно, без да се чека резултатот овде и сега, или да се подели голема работа на неколку единици што го извршуваат, постоела пред појавата на компјутерите. Со нивното доаѓање, оваа потреба стана многу опиплива. Сега, во 2019 година, ја пишувам оваа статија на лаптоп со 8-јадрен Intel Core процесор, на кој паралелно работат повеќе од сто процеси, а уште повеќе нишки. Во близина има малку излитен телефон, купен пред неколку години, има процесор со 8 јадра. Тематските ресурси се полни со написи и видеа каде нивните автори се восхитуваат на овогодинешните водечки паметни телефони кои имаат процесори со 16 јадра. MS Azure обезбедува виртуелна машина со процесор од 20 јадра и 128 TB RAM за помалку од 2 долари/час. За жал, невозможно е да се извлече максимумот и да се искористи оваа моќ без да може да се управува со интеракцијата на нишките.

Терминологија

Процес - ОС објект, изолиран адресен простор, содржи нишки.
Нишка - ОС објект, најмалата единица за извршување, дел од процес, нишките споделуваат меморија и други ресурси меѓу себе во рамките на еден процес.
Мултитаскинг - Својство на ОС, можност за извршување на неколку процеси истовремено
Мулти-јадрени - својство на процесорот, можност за користење на неколку јадра за обработка на податоци
Мултипроцесирање - својство на компјутер, можност физички да работи со неколку процесори истовремено
Повеќенишки — својство на процес, способност да се дистрибуира обработката на податоци меѓу неколку нишки.
Паралелизам - извршување на повеќе дејствија физички истовремено по единица време
Асинхронија — извршување на операција без да се чека завршување на оваа обработка; резултатот од извршувањето може да се обработи подоцна.

Метафора

Сите дефиниции не се добри и на некои им треба дополнително објаснување, па затоа ќе додадам метафора за готвење појадок во формално воведената терминологија. Готвењето појадок во оваа метафора е процес.

Додека го подготвував појадокот наутро јас (Процесорот) Доаѓам во кујната (Компјутер). имам 2 раце (јадра). Во кујната има голем број уреди (IO): рерна, котел, тостер, фрижидер. Го вклучувам гасот, ставам тава за пржење и турам масло без да чекам да се загрее (асинхроно, Non-Blocking-IO-Wait), ги вадам јајцата од фрижидер и ги кршам во чинија, па ги матам со едната рака (Тема #1), и второ (Тема #2) држење на плочата (Shared Resource). Сега би сакал да го вклучам котелот, но немам доволно раце (Тешка гладување) За тоа време се загрева тавата (Обработка на резултатот) во која го истурам изматеното. Посегнувам по котелот и го вклучувам и глупаво гледам како водата врие во него (Блокирање-IO-Чекај), иако за тоа време можеше да ја измие чинијата каде што го измати омлетот.

Сварив омлет со само 2 раце, а немам повеќе, но во исто време во моментот на матење на омлетот се случија 3 операции одеднаш: матење на омлетот, држење на чинијата, загревање на тавата. Процесорот е најбрзиот дел од компјутерот, IO е она што најчесто се забавува, па често ефикасно решение е да се окупира процесорот со нешто додека се примаат податоци од IO.

Продолжувајќи ја метафората:

  • Ако во процесот на подготовка на омлет би се обидел и да се пресоблечам, ова би бил пример за мултитаскинг. Важна нијанса: компјутерите се многу подобри во ова од луѓето.
  • Кујна со неколку готвачи, на пример во ресторан - повеќејадрен компјутер.
  • Многу ресторани во суд за храна во трговски центар - центар за податоци

.NET алатки

.NET е добар во работата со нишки, како и со многу други работи. Со секоја нова верзија, тој воведува се повеќе и повеќе нови алатки за работа со нив, нови слоеви на апстракција преку нишките на ОС. Кога работат со конструкција на апстракции, развивачите на рамка користат пристап кој остава можност, кога се користи апстракција на високо ниво, да се спушти едно или повеќе нивоа подолу. Најчесто тоа не е неопходно, всушност ја отвора вратата за пукање во ногата со сачмарка, но понекогаш, во ретки случаи, може да биде единствениот начин да се реши проблем што не е решен на сегашното ниво на апстракција. .

Под алатки, мислам на интерфејсите за програмирање на апликации (API) обезбедени од рамката и пакетите од трети страни, како и цели софтверски решенија кои го поедноставуваат пребарувањето за какви било проблеми поврзани со кодот со повеќе нишки.

Започнување на нишка

Класата Thread е најосновната класа во .NET за работа со нишки. Конструкторот прифаќа еден од двајцата делегати:

  • ThreadStart - Нема параметри
  • ParametrizedThreadStart - со еден параметар од типот објект.

Делегатот ќе биде извршен во новосоздадената нишка по повикување на методот Start.Ако делегат од типот ParametrizedThreadStart е предаден на конструкторот, тогаш објектот мора да биде предаден на методот Start. Овој механизам е потребен за да се пренесат какви било локални информации во потокот. Вреди да се напомене дека создавањето на нишка е скапа операција, а самата нишка е тежок предмет, барем затоа што доделува 1MB меморија на оџакот и бара интеракција со OS API.

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

Класата ThreadPool го претставува концептот на базен. Во .NET, базенот за нишки е дел од инженерството, а програмерите во Microsoft вложија многу напор за да се осигураат дека тој работи оптимално во широк спектар на сценарија.

Општ концепт:

Од моментот кога апликацијата започнува, таа создава неколку нишки во резерва во позадина и дава можност да ги земете за употреба. Ако нишките се користат често и во голем број, базенот се проширува за да ги задоволи потребите на повикувачот. Кога нема слободни нишки во базенот во вистинско време, тој или ќе почека да се врати една од нишките или ќе создаде нова. Оттука произлегува дека базенот со нишки е одличен за некои краткорочни дејства и слабо прилагоден за операции што работат како услуги во текот на целата работа на апликацијата.

За да се користи нишка од базенот, постои метод QueueUserWorkItem кој прифаќа делегат од типот WaitCallback, кој го има истиот потпис како ParametrizedThreadStart, а параметарот што му е предаден ја извршува истата функција.

ThreadPool.QueueUserWorkItem(...);

Помалку познатиот метод на збир на нишки RegisterWaitForSingleObject се користи за организирање на неблокирани операции на IO. Делегатот предаден на овој метод ќе биде повикан кога WaitHandle предадена на методот е „Ослободен“.

ThreadPool.RegisterWaitForSingleObject(...)

.

System.Threading.Timer

Исто така, постои прилично егзотичен начин да се испрати делегат за извршување до нишка од базенот - методот BeginInvoke.

DelegateInstance.BeginInvoke

Би сакал накратко да се задржам на функцијата на која може да се наречат многу од горенаведените методи - CreateThread од Kernel32.dll Win32 API. Постои начин, благодарение на механизмот на надворешни методи, да се повика оваа функција. Сум видел ваков повик само еднаш во страшен пример на наследен код, а мотивацијата на авторот кој го направил токму тоа сè уште ми останува мистерија.

Kernel32.dll CreateThread

Преглед и дебагирање на теми

Нишките креирани од вас, сите компоненти од трети страни и .NET базенот може да се гледаат во прозорецот Threads на Visual Studio. Овој прозорец ќе прикажува информации за нишките само кога апликацијата е под дебагирање и во режим на прекин. Овде можете лесно да ги прегледате имињата на стековите и приоритетите на секоја нишка и да го префрлите дебагирањето на одредена нишка. Користејќи го својството Priority од класата Thread, можете да го поставите приоритетот на нишката, која OC и CLR ќе ја сфатат како препорака кога ќе го делат времето на процесорот помеѓу нишките.

.NET: Алатки за работа со повеќенишки и асинхрони. Дел 1

Паралелна библиотека со задачи

Task Parallel Library (TPL) беше воведена во .NET 4.0. Сега тоа е стандардна и главна алатка за работа со асинхронија. Секој код што користи постар пристап се смета за наследство. Основната единица на TPL е класата Task од именскиот простор System.Threading.Tasks. Задачата е апстракција преку нишка. Со новата верзија на јазикот C#, добивме елегантен начин за работа со Tasks - оператори async/wait. Овие концепти овозможија да се пишува асинхрон код како да е едноставен и синхрон, тоа им овозможи дури и на луѓето со малку разбирање за внатрешната работа на нишките да пишуваат апликации што ги користат, апликации кои не се замрзнуваат при долги операции. Користењето на async/wait е тема за една или дури неколку статии, но ќе се обидам да ја разберам суштината во неколку реченици:

  • async е модификатор на метод кој враќа Task or void
  • и await е неблокирачки оператор за чекање задачи.

Уште еднаш: операторот на чекање, во општиот случај (има исклучоци), ќе ја ослободи тековната нишка на извршување понатаму, и кога Задачата ќе заврши со извршувањето, и нишката (всушност, би било поправилно да се каже контекстот , но повеќе за тоа подоцна) ќе продолжи со извршување на методот понатаму. Внатре во .NET, овој механизам се имплементира на ист начин како и враќањето на приносот, кога пишаниот метод се претвора во цела класа, која е државна машина и може да се изврши во посебни делови во зависност од овие состојби. Секој заинтересиран може да напише кој било едноставен код користејќи asynс/wait, да го компајлира и да го прегледа склопот со помош на JetBrains dotPeek со овозможен код генериран од компајлерот.

Да ги погледнеме опциите за стартување и користење на Task. Во примерот на кодот подолу, создаваме нова задача што не прави ништо корисно (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. Ако нема проверки, тогаш методот Cancel повикан на објектот CancellationTokenSource ќе може да го запре извршувањето на Задачата само пред да започне.

Последниот параметар е објект на распоредувач од типот TaskScheduler. Оваа класа и нејзините потомци се дизајнирани да управуваат со стратегии за дистрибуција на задачи низ нишки; стандардно, Задачата ќе се извршува на случајна нишка од базенот.

Операторот на чекање се применува на креираната Задача, што значи дека кодот напишан после него, доколку го има, ќе се изврши во истиот контекст (често тоа значи на истата нишка) како и кодот пред чекање.

Методот е означен како асинхронизиран неважечки, што значи дека може да го користи операторот на чекање, но кодот за повикување нема да може да чека за извршување. Ако е неопходна таква карактеристика, тогаш методот мора да ја врати задачата. Методите означени како async void се доста вообичаени: по правило, ова се ракувачи со настани или други методи кои работат на принципот на оган и заборавање. Ако треба не само да дадете можност да чекате до крајот на извршувањето, туку и да го вратите резултатот, тогаш треба да ја користите Task.

На Задачата што ја врати методот 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
}

Во првиот пример, чекаме задачата да заврши без да ја блокираме нишката за повикување; ќе се вратиме на обработка на резултатот само кога тој е веќе таму; дотогаш, нишката за повикување е оставена сама на себе.

Во втората опција, ја блокираме нишката за повикување додека не се пресмета резултатот од методот. Ова е лошо не само затоа што окупиравме нишка, толку вреден ресурс на програмата, со едноставно безделничење, туку и затоа што ако кодот на методот што го повикуваме содржи чекање, а контекстот за синхронизација бара враќање во нишката што се повикува после чекај, тогаш ќе добиеме ќорсокак : нишката што се јавува чека додека се пресметува резултатот од асинхрониот метод, асинхрониот метод залудно се обидува да го продолжи своето извршување во нишката што се јавува.

Друг недостаток на овој пристап е комплицираното справување со грешки. Факт е дека грешките во асинхрониот код при користење на async/wait се многу лесни за ракување - тие се однесуваат исто како кодот да е синхрон. Додека ако примениме синхроно егзорцизам на чекање на Task, оригиналниот исклучок се претвора во AggregateException, т.е. За да се справите со исклучокот, ќе треба да го испитате типот InnerException и сами да напишете синџир if во еден блок за фаќање или да го користите зафатот кога го конструирате, наместо синџирот на блокови за фаќање што е попознат во светот на C#.

Третиот и последен пример се исто така означени како лоши поради истата причина и ги содржат сите исти проблеми.

Методите WhenAny и WhenAll се исклучително погодни за чекање на група задачи; тие ја обвиткуваат група на Tasks во една, која ќе се активира или кога Task од групата првпат ќе се активира или кога сите ќе го завршат нивното извршување.

Запирање на нишки

Од различни причини, може да биде неопходно да се запре протокот откако ќе започне. Постојат голем број начини да го направите ова. Класата Thread има два соодветно именувани методи: Прекинете и Прекини. Првиот не се препорачува за употреба, бидејќи по повикување во кој било случаен момент, за време на обработката на која било инструкција, ќе се фрли исклучок ThreadAbortedException. Не очекувате да се фрли таков исклучок при зголемување на која било цел бројна променлива, нели? И кога се користи овој метод, ова е многу реална ситуација. Ако треба да спречите CLR да генерира таков исклучок во одреден дел од кодот, можете да го завиткате во повици Тема.BeginCriticalRegion, Тема.EndCriticalRegion. Секој код напишан во конечно блок е завиткан во такви повици. Поради оваа причина, во длабочините на рамковниот код можете да најдете блокови со празен обид, но не и празен конечно. Мајкрософт го обесхрабрува овој метод толку многу што не го вклучи во .net јадрото.

Методот Interrupt работи попредвидливо. Може да ја прекине нишката со исклучок ThreadInterruptedException само во оние моменти кога конецот е во состојба на чекање. Влегува во оваа состојба додека виси додека чека WaitHandle, заклучување или откако ќе се јави Thread.Sleep.

И двете опции опишани погоре се лоши поради нивната непредвидливост. Решението е да се користи структура Токен за откажување и класа ОткажувањеTokenSource. Поентата е следна: се креира примерок од класата CancellationTokenSource и само оној што ја поседува може да ја прекине операцијата со повикување на методот Откажи. Само CancellationToken се пренесува на самата операција. Сопствениците на CancellationToken не можат сами да ја откажат операцијата, туку можат само да проверат дали операцијата е откажана. Постои Булова сопственост за ова Се бара откажување и метод ThrowIfCancelRequested. Вториот ќе фрли исклучок TaskCancelledException ако методот Cancel бил повикан на примерот на CancellationToken кој се папагал. И ова е методот што го препорачувам да го користам. Ова е подобрување во однос на претходните опции со стекнување целосна контрола врз тоа во кој момент може да се прекине операцијата со исклучок.

Најбруталната опција за запирање на нишка е да ја повикате функцијата Win32 API TerminateThread. Однесувањето на CLR по повикувањето на оваа функција може да биде непредвидливо. На MSDN е напишано следново за оваа функција: „TerminateThread е опасна функција која треба да се користи само во најекстремните случаи. “

Конвертирање на наследениот API во Task Based со помош на методот FromAsync

Ако сте доволно среќни да работите на проект што е започнат откако Tasks беа воведени и престанаа да предизвикува тивок хорор за повеќето програмери, тогаш нема да морате да се занимавате со многу стари API-а, како со трети лица, така и со оние на вашиот тим. мачел во минатото. За среќа, тимот на .NET Framework се погрижи за нас, иако можеби целта беше да се грижиме за себе. Како и да е, .NET има голем број алатки за безболно конвертирање на код напишан во старите асинхрони програмски пристапи кон новиот. Еден од нив е методот FromAsync на TaskFactory. Во примерот на кодот подолу, ги завиткам старите асинхронизирани методи на класата WebRequest во Задача користејќи го овој метод.

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

Ова е само пример и веројатно нема да морате да го правите ова со вградените типови, но секој стар проект едноставно изобилува со методи BeginDoSomething кои ги враќаат методите IAsyncResult и EndDoSomething што го примаат.

Претворете го наследениот API во Task Based користејќи класа на TaskCompletionSource

Друга важна алатка што треба да се земе предвид е класата TaskCompletionSource. Во однос на функциите, целта и принципот на работа, можеби донекаде потсетува на методот RegisterWaitForSingleObject од класата ThreadPool, за кој напишав погоре. Користејќи ја оваа класа, можете лесно и практично да ги завиткате старите асинхрони API во Tasks.

Ќе кажете дека веќе зборував за методот FromAsync од класата TaskFactory наменет за овие цели. Овде ќе треба да се потсетиме на целата историја на развојот на асинхрони модели во .net што Мајкрософт ги понуди во текот на изминатите 15 години: пред Асинхроната шема заснована на задачи (TAP), постоеше асинхроната шема за програмирање (APP), која беше за методи ЗапочнетеСе враќа нешто IAsyncрезултат и методи крајотDoSomething што го прифаќа и за наследството од овие години, методот FromAsync е едноставно совршен, но со текот на времето, тој беше заменет со асинхроната шема базирана на настани (ЕАП), кој претпоставува дека настанот ќе се подигне кога ќе заврши асинхроната операција.

TaskCompletionSource е совршен за завиткување на Tasks и наследни API изградени околу моделот на настанот. Суштината на неговата работа е како што следува: објект од оваа класа има јавна сопственост од типот Task, чија состојба може да се контролира преку методите SetResult, SetException итн. од класата TaskCompletionSource. На места каде што операторот на чекање е применет на оваа Задача, таа ќе се изврши или не успее со исклучок во зависност од методот применет на 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;
}

Совети и трикови на TaskCompletionSource

Завиткувањето на старите API не е сè што може да се направи со користење на TaskCompletionSource. Користењето на оваа класа отвора интересна можност за дизајнирање на различни API на Tasks кои не се окупирани од нишки. И потокот, како што се сеќаваме, е скап ресурс и нивниот број е ограничен (главно од количината на RAM меморија). Ова ограничување може лесно да се постигне со развивање, на пример, вчитана веб-апликација со сложена деловна логика. Ајде да ги разгледаме можностите за кои зборувам при спроведување на таков трик како Long-Polling.

Накратко, суштината на трикот е оваа: треба да добивате информации од API за некои настани што се случуваат на негова страна, додека API, поради некоја причина, не може да го пријави настанот, туку може само да ја врати состојбата. Пример за нив се сите API изградени на врвот на HTTP пред времето на WebSocket или кога беше невозможно поради некоја причина да се користи оваа технологија. Клиентот може да побара од серверот HTTP. HTTP серверот сам не може да иницира комуникација со клиентот. Едноставно решение е да се анкетира серверот со помош на тајмер, но тоа создава дополнително оптоварување на серверот и дополнително доцнење во просек TimerInterval / 2. За да се заобиколи ова, измислен е трик наречен Long Polling, кој вклучува одложување на одговорот од сервер додека не истече времето или ќе се случи настан. Ако настанот се случил, тогаш се обработува, ако не, тогаш барањето се испраќа повторно.

while(!eventOccures && !timeoutExceeded)  {

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

Но, таквото решение ќе се покаже како страшно штом ќе се зголеми бројот на клиенти кои го чекаат настанот, бидејќи ... Секој таков клиент зафаќа цела нишка чекајќи настан. Да, и добиваме дополнително доцнење од 1 ms кога настанот се активира, најчесто тоа не е значајно, но зошто да го направиме софтверот полош отколку што може да биде? Ако го отстраниме 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: зошто и како

Операторите async/wait, како операторот за враќање на приносот, генерираат машина за состојба од методот, а тоа е создавање на нов објект, кој скоро секогаш не е важен, но во ретки случаи може да создаде проблем. Овој случај можеби е метод што се нарекува навистина често, зборуваме за десетици и стотици илјади повици во секунда. Ако таков метод е напишан на таков начин што во повеќето случаи враќа резултат заобиколувајќи ги сите методи на чекање, тогаш .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);
}

Навистина, оптимално решение во овој случај би било да се оптимизира hot-path, имено, да се добие вредност од речникот без никакви непотребни распределби и оптоварување на 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 има способност да управува со стратегии за дистрибуција на Tasks низ нишки. Ваквите стратегии се дефинирани во потомците на класата TaskScheduler. Речиси секоја стратегија што можеби ви треба може да се најде во библиотеката. ParallelExtensionsExtras, развиен од Microsoft, но не е дел од .NET, туку испорачан како Nuget пакет. Ајде накратко да погледнеме некои од нив:

  • CurrentThreadTaskScheduler — извршува Tasks на тековната нишка
  • LimitedConcurrencyLevelTaskScheduler — го ограничува бројот на задачи кои се извршуваат истовремено со параметарот N, кој е прифатен во конструкторот
  • OrderedTaskScheduler — се дефинира како LimitedConcurrencyLevelTaskScheduler(1), така што задачите ќе се извршуваат последователно.
  • WorkStealingTaskScheduler - спроведува работа-крадење пристап кон дистрибуција на задачи. Во суштина тоа е посебен ThreadPool. Го решава проблемот што во .NET ThreadPool е статична класа, една за сите апликации, што значи дека нејзиното преоптоварување или неправилна употреба во еден дел од програмата може да доведе до несакани ефекти во друг. Покрај тоа, исклучително е тешко да се разбере причината за таквите дефекти. Тоа. Можеби ќе има потреба да се користат посебни WorkStealingTaskScheduler во делови од програмата каде што употребата на ThreadPool може да биде агресивна и непредвидлива.
  • Распоредувач на задачи во редица — ви овозможува да извршувате задачи според правилата за приоритетни редици
  • ThreadPerTaskScheduler — создава посебна нишка за секоја задача што се извршува на неа. Може да биде корисно за задачи за кои е потребно непредвидливо долго време да се завршат.

Има добар детален Член за TaskScheduler на блогот на Microsoft.

За практично дебагирање на сè што е поврзано со Tasks, Visual Studio има прозорец Tasks. Во овој прозорец можете да ја видите моменталната состојба на задачата и да скокнете на линијата на код што моментално се извршува.

.NET: Алатки за работа со повеќенишки и асинхрони. Дел 1

PLinq и класата Parallel

Покрај Tasks и се што е кажано за нив, во .NET има уште две интересни алатки: PLinq (Linq2Parallel) и класата Parallel. Првиот ветува паралелно извршување на сите Linq операции на повеќе нишки. Бројот на нишки може да се конфигурира со помош на методот за проширување WithDegreeOfParallelism. За жал, најчесто PLinq во неговиот стандарден режим нема доволно информации за внатрешните делови на вашиот извор на податоци за да обезбеди значително зголемување на брзината, од друга страна, цената на обидот е многу ниска: само треба да го повикате методот AsParallel пред синџирот на методи на Linq и извршете тестови за изведба. Покрај тоа, можно е да се пренесат дополнителни информации до PLinq за природата на вашиот извор на податоци користејќи го механизмот за партиции. Можете да прочитате повеќе тука и тука.

Класата Parallel статичка обезбедува методи за повторување низ колекцијата Foreach паралелно, извршување For циклус и извршување на повеќе делегати паралелно Invoke. Извршувањето на тековната нишка ќе биде запрено додека не се завршат пресметките. Бројот на нишки може да се конфигурира со додавање на ParallelOptions како последен аргумент. Можете исто така да наведете TaskScheduler и CancellationToken користејќи опции.

Наоди

Кога почнав да ја пишувам оваа статија врз основа на материјалите од мојот извештај и информациите што ги собрав за време на мојата работа после него, не очекував дека ќе има толку многу. Сега, кога уредникот на текст во кој ја пишувам оваа статија со прекор ќе ми каже дека страницата 15 ја нема, ќе ги сумирам привремените резултати. Други трикови, API, визуелни алатки и стапици ќе бидат опфатени во следната статија.

Заклучоци:

  • Треба да ги знаете алатките за работа со нишки, асинхронија и паралелизам за да ги користите ресурсите на современите компјутери.
  • .NET има многу различни алатки за овие цели
  • Не се појавија сите одеднаш, така што често можете да најдете наследни, сепак, постојат начини да ги конвертирате старите API без многу напор.
  • Работата со нишки во .NET е претставена со класите Thread и ThreadPool
  • Методите Thread.Abort, Thread.Interrupt и Win32 API TerminateThread се опасни и не се препорачуваат за употреба. Наместо тоа, подобро е да се користи механизмот CancellationToken
  • Протокот е вреден ресурс и неговата понуда е ограничена. Треба да се избегнуваат ситуации кога нишките се зафатени со чекање за настани. За ова е погодно да се користи класата TaskCompletionSource
  • Најмоќните и најнапредните .NET алатки за работа со паралелизам и асинхронија се Tasks.
  • Операторите c# async/wait го имплементираат концептот на чекање без блокирање
  • Можете да ја контролирате дистрибуцијата на Tasks низ нишки користејќи класи добиени од TaskScheduler
  • Структурата ValueTask може да биде корисна за оптимизирање на топли патеки и сообраќај со меморија
  • Прозорците Tasks и Threads на Visual Studio обезбедуваат многу информации корисни за дебагирање на повеќенишки или асинхрони кодови
  • PLinq е одлична алатка, но можеби нема доволно информации за вашиот извор на податоци, но ова може да се поправи со помош на механизмот за партиционирање
  • Да се ​​продолжи ...

Извор: www.habr.com

Додадете коментар