.НЕТ: Алати за рад са вишенитним и асинхронијом. Део 1

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

Потреба да се нешто ради асинхроно, без чекања на резултат овде и сада, или да се велики посао подели на неколико јединица које га обављају, постојала је пре појаве рачунара. Са њиховим доласком, ова потреба је постала веома опипљива. Сада, 2019. године, куцам овај чланак на лаптопу са 8-језгарним Интел Цоре процесором, на коме паралелно ради више од стотину процеса, па чак и више нити. У близини се налази мало похабан телефон, купљен пре пар година, има 8-језгарни процесор. Тематски ресурси су пуни чланака и видео снимака у којима се њихови аутори диве овогодишњим водећим паметним телефонима који имају процесоре са 16 језгара. МС Азуре обезбеђује виртуелну машину са процесором од 20 језгара и 128 ТБ РАМ-а за мање од 2 УСД по сату. Нажалост, немогуће је извући максимум и искористити ову моћ без могућности управљања интеракцијом нити.

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

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

Метафора

Нису све дефиниције добре и за неке је потребно додатно објашњење, па ћу формално уведеној терминологији додати метафору о кувању доручка. Кување доручка у овој метафори је процес.

Припремајући доручак ујутру ја (Процесор) Долазим у кухињу (Рачунар). Имам 2 руке (Језгра). У кухињи постоји велики број уређаја (IO): рерна, кувало за воду, тостер, фрижидер. Укључујем гас, стављам тигањ и сипам уље не чекајући да се загреје (асинхроно, Нон-Блоцкинг-ИО-Ваит), извадим јаја из фрижидера и разбијем их у тањир, па једном руком истучем (Тхреад#1), и друго (Тхреад#2) држећи плочу (заједнички ресурс). Сада бих желео да укључим чајник, али немам довољно руку (Тхреад Гладовање) За то време се загреје тигањ (Обрада резултата) у који сипам оно што сам умутио. Посегнем за чајником и укључим га и глупо гледам како вода кључа у њему (Блокирање-ИО-Чекај), иако је за то време могао да опере тањир где је умутио омлет.

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

Настављајући метафору:

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

.НЕТ Тоолс

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

Под алатима подразумевам и интерфејсе за програмирање апликација (АПИ) које обезбеђује оквир и пакети независних произвођача, као и цела софтверска решења која поједностављују претрагу било каквих проблема у вези са вишенитним кодом.

Покретање нити

Класа Тхреад је најосновнија класа у .НЕТ-у за рад са нитима. Конструктор прихвата једног од два делегата:

  • ТхреадСтарт — Нема параметара
  • ПараметризедТхреадСтарт - са једним параметром типа објецт.

Делегат ће се извршити у новокреираној нити након позивања методе Старт.Ако је делегат типа ПараметризедТхреадСтарт прослеђен конструктору, онда објекат мора бити прослеђен методи Старт. Овај механизам је потребан за пренос било које локалне информације у ток. Вреди напоменути да је креирање нити скупа операција, а сама нит је тежак објекат, барем зато што додељује 1МБ меморије на стеку и захтева интеракцију са ОС АПИ-јем.

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

ТхреадПоол класа представља концепт базена. У .НЕТ-у, скуп нити је део инжењеринга, а програмери у Мицрософт-у су уложили много труда да осигурају да оно функционише оптимално у широком спектру сценарија.

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

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

Да бисте користили нит из скупа, постоји метода КуеуеУсерВоркИтем која прихвата делегата типа ВаитЦаллбацк, који има исти потпис као ПараметризедТхреадСтарт, а параметар који му је прослеђен обавља исту функцију.

ThreadPool.QueueUserWorkItem(...);

Мање познати метод скупа нити РегистерВаитФорСинглеОбјецт се користи за организовање неблокирајућих ИО операција. Делегат који је прослеђен овом методу биће позван када је ВаитХандле прослеђен методу „Ослободјен“.

ThreadPool.RegisterWaitForSingleObject(...)

.НЕТ има тајмер нити и разликује се од ВинФормс/ВПФ тајмера по томе што ће његов руковалац бити позван у нити преузетој из скупа.

System.Threading.Timer

Постоји и прилично егзотичан начин слања делегата на извршење у нит из пула – метод БегинИнвоке.

DelegateInstance.BeginInvoke

Желео бих укратко да се задржим на функцији којој се могу позвати многе од горе наведених метода – ЦреатеТхреад из Кернел32.длл Вин32 АПИ. Постоји начин, захваљујући механизму екстерних метода, да се ова функција позове. Такав позив сам видео само једном у страшном примеру легаци кода, а мотивација аутора који је управо то урадио и даље ми остаје мистерија.

Kernel32.dll CreateThread

Прегледање и отклањање грешака у нитима

Нити које сте креирали, све компоненте независних произвођача и .НЕТ скуп могу се видети у прозору Тхреадс Висуал Студио-а. Овај прозор ће приказати информације о нити само када је апликација у фази отклањања грешака и у режиму прекида. Овде можете згодно видети називе стекова и приоритете сваке нити и пребацити отклањање грешака на одређену нит. Користећи својство Приорити класе Тхреад, можете поставити приоритет нити, што ће ОЦ и ЦЛР схватити као препоруку приликом поделе времена процесора између нити.

.НЕТ: Алати за рад са вишенитним и асинхронијом. Део 1

Библиотека паралелних задатака

Паралелна библиотека задатака (ТПЛ) уведена је у .НЕТ 4.0. Сада је то стандард и главни алат за рад са асинхронијом. Сваки код који користи старији приступ сматра се наслеђем. Основна јединица ТПЛ-а је класа Таск из именског простора Систем.Тхреадинг.Таскс. Задатак је апстракција преко нити. Са новом верзијом језика Ц#, добили смо елегантан начин рада са задацима – асинц/аваит оператори. Ови концепти су омогућили да се асинхрони код пише као да је једноставан и синхрони, што је омогућило чак и људима који мало разумеју унутрашње функционисање нити да напишу апликације које их користе, апликације које се не замрзавају приликом извођења дугих операција. Коришћење асинц/аваит је тема за један или чак неколико чланака, али покушаћу да схватим суштину у неколико реченица:

  • асинц је модификатор методе која враћа Таск или воид
  • а аваит је неблокирајући оператор задатка на чекању.

Још једном: оператор аваит, у општем случају (има изузетака), ће даље пустити тренутну нит извршења, а када Задатак заврши своје извршење, а нит (у ствари, исправније би било рећи контекст , али више о томе касније) наставиће даље извршавање методе. Унутар .НЕТ-а, овај механизам је имплементиран на исти начин као и враћање приноса, када се писани метод претвара у целу класу, која је машина стања и може се извршити у одвојеним деловима у зависности од ових стања. Свако заинтересован може да напише било који једноставан код користећи асинс/аваит, преведе и погледа склоп користећи ЈетБраинс дотПеек са омогућеним кодом генерисаним компајлером.

Хајде да погледамо опције за покретање и коришћење Таск-а. У примеру кода испод, креирамо нови задатак који не ради ништа корисно (Тхреад.Слееп(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
}

Задатак се креира са неколико опција:

  • ЛонгРуннинг је наговештај да задатак неће бити брзо завршен, што значи да би можда било вредно размислити о томе да не узимате нит из скупа, већ да направите засебну за овај задатак како не бисте повредили друге.
  • АттацхедТоПарент – Задаци се могу распоредити у хијерархији. Ако је коришћена ова опција, онда је Задатак можда у стању у којем је сам завршио и чека извршење својих потомака.
  • ПреферФаирнесс - значи да би било боље извршити задатке који су послати на извршење раније пре оних послатих касније. Али ово је само препорука и резултати нису загарантовани.

Други параметар који се прослеђује методи је ЦанцеллатионТокен. Да би се исправно руковало отказивањем операције након што је започела, код који се извршава мора бити испуњен проверама за стање ЦанцеллатионТокен. Ако нема провера, онда ће метода Цанцел позвана на ЦанцеллатионТокенСоурце објекту моћи да заустави извршење Задатка само пре него што почне.

Последњи параметар је објекат планера типа ТаскСцхедулер. Ова класа и њени потомци су дизајнирани да контролишу стратегије за дистрибуцију задатака међу нитима; подразумевано, задатак ће се извршавати на насумичној нити из скупа.

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

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

На Задатку који је метод СтартНев вратио, као и на било који други, можете позвати метод ЦонфигуреАваит са параметром фалсе, а затим ће се извршавање након чекања наставити не на снимљеном контексту, већ на произвољном. Ово увек треба урадити када контекст извршења није важан за код након чекања. Ово је такође препорука од МС-а приликом писања кода који ће бити испоручен упакован у библиотеци.

Хајде да се задржимо још мало на томе како можете да сачекате завршетак Задатка. Испод је пример кода, са коментарима када је очекивање урађено условно добро, а када је урађено условно лоше.

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
}

У првом примеру чекамо да се Задатак заврши без блокирања позивајуће нити; вратићемо се на обраду резултата тек када је већ тамо; до тада, позивајућа нит је препуштена сопственим уређајима.

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

Још један недостатак овог приступа је компликовано руковање грешкама. Чињеница је да се грешке у асинхроном коду при коришћењу асинц/аваит веома лако обрађују – понашају се исто као да је код синхрони. Док ако применимо синхрони егзорцизам чекања на задатак, оригинални изузетак се претвара у АггрегатеЕкцептион, тј. Да бисте обрадили изузетак, мораћете да испитате тип ИннерЕкцептион и сами напишете иф ланац унутар једног цатцх блока или користите цатцх када се конструише, уместо ланца цатцх блокова који је познатији у свету Ц#.

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

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

Заустављање нити

Из различитих разлога, можда ће бити потребно зауставити ток након што је почео. Постоји неколико начина да се то уради. Класа Тхреад има две методе са одговарајућим именом: Прекид и прекид. Први се веома не препоручује за употребу, јер након што га позовете у било ком насумичном тренутку, током обраде било које инструкције, биће избачен изузетак ТхреадАбортедЕкцептион. Не очекујете да ће такав изузетак бити избачен приликом повећања било које целобројне променљиве, зар не? А када се користи овај метод, ово је врло реална ситуација. Ако желите да спречите ЦЛР да генерише такав изузетак у одређеном делу кода, можете га умотати у позиве Тхреад.БегинЦритицалРегион, Тхреад.ЕндЦритицалРегион. Сваки код написан у финалли блоку је умотан у такве позиве. Из тог разлога, у дубинама кода оквира можете пронаћи блокове са празним покушајем, али не и празним коначно. Мицрософт толико обесхрабрује ову методу да је нису укључили у .нет језгро.

Метода Интеррупт ради предвидљивије. Може прекинути нит са изузетком ТхреадИнтерруптедЕкцептион само у оним тренуцима када је нит у стању чекања. У ово стање улази док виси док чека на ВаитХандле, закључавање или након позива Тхреад.Слееп.

Обе горе описане опције су лоше због своје непредвидљивости. Решење је коришћење структуре ЦанцеллатионТокен и класа ЦанцеллатионТокенСоурце. Поента је у следећем: креира се инстанца класе ЦанцеллатионТокенСоурце и само онај ко је поседује може да заустави операцију позивањем методе отказати. Само ЦанцеллатионТокен се прослеђује самој операцији. Власници ЦанцеллатионТокена не могу сами да откажу операцију, већ могу само да провере да ли је операција отказана. За ово постоји логичко својство ИсЦанцеллатионРекуестед и метод ТхровИфЦанцелРекуестед. Ово последње ће избацити изузетак ТаскЦанцелледЕкцептион ако је метода Цанцел позвана на инстанци ЦанцеллатионТокен која се понавља. И ово је метод који препоручујем да користите. Ово је побољшање у односу на претходне опције добијањем потпуне контроле над тим у ком тренутку операција изузетка може бити прекинута.

Најбруталнија опција за заустављање нити је позивање функције Вин32 АПИ ТерминатеТхреад. Понашање ЦЛР-а након позивања ове функције може бити непредвидиво. На МСДН-у о овој функцији пише следеће: „ТерминатеТхреад је опасна функција коју треба користити само у најекстремнијим случајевима. “

Претварање застарелог АПИ-ја у заснованог на задацима коришћењем ФромАсинц методе

Ако имате довољно среће да радите на пројекту који је започет након што су Задаци уведени и престали да изазивају тихи ужас за већину програмера, онда нећете морати да се бавите пуно старих АПИ-ја, како оних трећих страна, тако и оних који ваш тим је мучио у прошлости. Срећом, .НЕТ Фрамеворк тим се побринуо за нас, иако је можда циљ био да се бринемо о себи. Како год било, .НЕТ има низ алата за безболно претварање кода написаног у старим приступима асинхроног програмирања у нови. Једна од њих је ФромАсинц метода ТаскФацтори-а. У примеру кода испод, умотавам старе асинхронизоване методе класе ВебРекуест у задатак користећи овај метод.

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

Ово је само пример и мало је вероватно да ћете то морати да радите са уграђеним типовима, али сваки стари пројекат једноставно врви од БегинДоСометхинг метода које враћају ИАсинцРесулт и ЕндДоСометхинг метода које га примају.

Претворите застарели АПИ у заснован на задатку помоћу класе ТаскЦомплетионСоурце

Још једно важно средство које треба узети у обзир је класа ТаскЦомплетионСоурце. По функцијама, сврси и принципу рада може донекле подсећати на метод РегистерВаитФорСинглеОбјецт класе ТхреадПоол, о чему сам писао горе. Користећи ову класу, можете лако и згодно умотати старе асинхроне АПИ-је у Задатке.

Рећи ћете да сам већ говорио о ФромАсинц методи класе ТаскФацтори намењеној за ове сврхе. Овде ћемо морати да се присетимо целокупне историје развоја асинхроних модела у .нет-у које је Мицрософт нудио у протеклих 15 година: пре Асинхроног узорка заснованог на задацима (ТАП) постојао је Асинхрони модел програмирања (АПП), који је радило се о методама ПочетакДоСометхинг ретурнинг ИАсинцРесулт и методе КрајДоСометхинг то прихвата и за наслеђе ових година ФромАсинц метода је једноставно савршена, али је временом замењена асинхроним шаблоном заснованим на догађајима (ЕАП-), који је претпоставио да ће се догађај покренути када се асинхрона операција заврши.

ТаскЦомплетионСоурце је савршен за омотавање задатака и застарелих АПИ-ја изграђених око модела догађаја. Суштина његовог рада је следећа: објекат ове класе има јавно својство типа Таск, чије стање се може контролисати преко метода СетРесулт, СетЕкцептион итд. класе ТаскЦомплетионСоурце. На местима где је оператор аваит примењен на овај задатак, он ће бити извршен или неуспешан са изузетком у зависности од методе примењене на ТаскЦомплетионСоурце. Ако још увек није јасно, погледајмо овај пример кода, где је неки стари ЕАП АПИ умотан у задатак користећи ТаскЦомплетионСоурце: када се догађај покрене, задатак ће бити стављен у стање Завршено, а метод који је применио оператор аваит овом Задатку ће наставити са извршењем након што је примио објекат резултат.

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

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

    result completionSource.Task;
}

ТаскЦомплетионСоурце Савети и трикови

Паковање старих АПИ-ја није све што се може урадити помоћу ТаскЦомплетионСоурце. Коришћење ове класе отвара интересантну могућност дизајнирања различитих АПИ-ја на задацима који не заузимају нити. А ток је, као што се сећамо, скуп ресурс и њихов број је ограничен (углавном количином РАМ-а). Ово ограничење се може лако постићи развојем, на пример, учитане веб апликације са сложеном пословном логиком. Хајде да размотримо могућности о којима говорим када имплементирамо такав трик као што је Лонг-Поллинг.

Укратко, суштина трика је следећа: од АПИ-ја треба да добијете информације о неким догађајима који се дешавају на његовој страни, док АПИ из неког разлога не може да пријави догађај, већ може само да врати стање. Пример за то су сви АПИ-ји изграђени на врху ХТТП-а пре времена ВебСоцкет-а или када је из неког разлога било немогуће користити ову технологију. Клијент може питати ХТТП сервер. ХТТП сервер не може сам да покрене комуникацију са клијентом. Једноставно решење је анкетирање сервера помоћу тајмера, али то ствара додатно оптерећење на серверу и додатно кашњење у просеку ТимерИнтервал / 2. Да би се ово заобишло, измишљен је трик под називом Лонг Поллинг, који укључује одлагање одговора од сервер све док не истекне временско ограничење или се догоди догађај. Ако се догађај догодио, онда се обрађује, ако није, онда се захтев поново шаље.

while(!eventOccures && !timeoutExceeded)  {

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

Али такво решење ће се показати страшним чим се повећа број клијената који чекају догађај, јер... Сваки такав клијент заузима читаву нит чекајући догађај. Да, и добијамо додатно кашњење од 1 мс када се догађај покрене, најчешће то није значајно, али зашто учинити софтвер лошијим него што може бити? Ако уклонимо Тхреад.Слееп(1), узалуд ћемо једно језгро процесора учитати 100% неактивно, ротирајући у бескорисном циклусу. Користећи ТаскЦомплетионСоурце, можете лако да преправите овај код и решите све проблеме идентификоване изнад:

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

Овај код није спреман за производњу, већ само демо. Да бисте га користили у стварним случајевима, потребно је, у најмању руку, да се носите са ситуацијом када порука стигне у време када је нико не очекује: у овом случају, метода АссептМессагеАсинц треба да врати већ завршен задатак. Ако је ово најчешћи случај, онда можете размислити о коришћењу ВалуеТаск-а.

Када примимо захтев за поруком, креирамо и постављамо ТаскЦомплетионСоурце у речник, а затим чекамо шта се прво деси: наведени временски интервал истиче или се порука прима.

ВалуеТаск: зашто и како

Оператори асинц/аваит, попут оператора враћања приноса, генеришу машину стања из методе, а то је креирање новог објекта, што скоро увек није важно, али у ретким случајевима може да створи проблем. Овај случај може бити метода која се зове заиста често, говоримо о десетинама и стотинама хиљада позива у секунди. Ако је такав метод написан на такав начин да у већини случајева враћа резултат заобилазећи све методе чекања, онда .НЕТ пружа алат за оптимизацију овога - структуру ВалуеТаск. Да би било јасно, погледајмо пример његове употребе: постоји кеш у који често посећујемо. У њему постоје неке вредности и онда их једноставно враћамо; ако не, онда идемо на неки спори ИО да их добијемо. Желим да урадим ово друго асинхроно, што значи да се цео метод испостави да је асинхрони. Дакле, очигледан начин за писање методе је следећи:

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

Заиста, оптимално решење у овом случају би било да се оптимизује хот-патх, наиме, добијање вредности из речника без непотребних алокација и учитавања ГЦ-а, док у оним ретким случајевима када још увек треба да идемо у ИО за податке , све ће остати плус/минус на стари начин:

public ValueTask<string> GetById(int id) {

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

Хајде да ближе погледамо овај део кода: ако постоји вредност у кешу, креирамо структуру, иначе ће прави задатак бити умотан у смислени задатак. Позивном коду није важно на којој путањи је овај код извршен: ВалуеТаск, са становишта Ц# синтаксе, понашаће се исто као и обичан задатак у овом случају.

ТаскСцхедулерс: управљање стратегијама покретања задатака

Следећи АПИ који бих желео да размотрим је класа ТаскСцхедулер и његових деривата. Већ сам поменуо да ТПЛ има могућност управљања стратегијама за дистрибуцију задатака кроз нити. Такве стратегије су дефинисане у потомцима класе ТаскСцхедулер. Скоро свака стратегија која вам је потребна може се наћи у библиотеци. ПараллелЕктенсионсЕктрас, који је развио Мицрософт, али није део .НЕТ-а, већ се испоручује као Нугет пакет. Погледајмо укратко неке од њих:

  • ЦуррентТхреадТаскСцхедулер — извршава задатке на тренутној нити
  • ЛимитедЦонцурренциЛевелТаскСцхедулер — ограничава број задатака који се извршавају истовремено параметром Н, који је прихваћен у конструктору
  • ОрдередТаскСцхедулер — је дефинисан као ЛимитедЦонцурренциЛевелТаскСцхедулер(1), тако да ће се задаци извршавати узастопно.
  • ВоркСтеалингТаскСцхедулер - имплементира рад-крађа приступ расподели задатака. У суштини, то је посебан ТхреадПоол. Решава проблем да је у .НЕТ ТхреадПоол статична класа, једна за све апликације, што значи да њено преоптерећење или неправилна употреба у једном делу програма може довести до нежељених ефеката у другом. Штавише, веома је тешко разумети узрок таквих недостатака. То. Можда постоји потреба да се користе одвојени ВоркСтеалингТаскСцхедулерс у деловима програма где употреба ТхреадПоол-а може бити агресивна и непредвидива.
  • КуеуедТаскСцхедулер — омогућава вам да извршавате задатке према правилима редоследа приоритета
  • ТхреадПерТаскСцхедулер — креира засебну нит за сваки задатак који се на њему извршава. Може бити корисно за задатке за које је потребно непредвидиво дуго времена.

Постоји добар детаљ чланак о ТаскСцхедулерима на Мицрософт блогу.

За практично отклањање грешака у свему што је повезано са задацима, Висуал Студио има прозор Задаци. У овом прозору можете видети тренутно стање задатка и скочити на ред кода који се тренутно извршава.

.НЕТ: Алати за рад са вишенитним и асинхронијом. Део 1

ПЛинк и класа Параллел

Поред задатака и свега што је о њима речено, у .НЕТ-у постоје још два занимљива алата: ПЛинк (Линк2Параллел) и класа Параллел. Први обећава паралелно извршавање свих Линк операција на више нити. Број нити се може конфигурисати помоћу методе проширења ВитхДегрееОфПараллелисм. Нажалост, најчешће ПЛинк у свом подразумеваном режиму нема довољно информација о унутрашњости вашег извора података да би обезбедио значајно повећање брзине, с друге стране, цена покушаја је веома ниска: само треба да позовете АсПараллел метод пре него што ланац Линк метода и покренути тестове перформанси. Штавише, могуће је проследити додатне информације ПЛинк-у о природи вашег извора података помоћу механизма Партиције. Можете прочитати више овде и овде.

Статичка класа Параллел обезбеђује методе за паралелно понављање кроз колекцију Фореацх, извршавање петље Фор и извршавање више делегата у паралелном Инвоке-у. Извршавање текуће нити ће бити заустављено док се прорачуни не заврше. Број нити се може конфигурисати преношењем ПараллелОптионс као последњег аргумента. Такође можете да наведете ТаскСцхедулер и ЦанцеллатионТокен користећи опције.

Налази

Када сам почео да пишем овај чланак на основу материјала мог извештаја и информација које сам прикупио током рада након њега, нисам очекивао да ће га бити толико. Сада, када ми уредник текста у који куцам овај чланак прекорно каже да је страница 15 отишла, сумираћу привремене резултате. Други трикови, АПИ-ји, визуелни алати и замке ће бити покривени у следећем чланку.

Закључци:

  • Морате да познајете алате за рад са нитима, асинхронију и паралелизам да бисте користили ресурсе савремених рачунара.
  • .НЕТ има много различитих алата за ове сврхе
  • Нису се сви појавили одједном, тако да често можете пронаћи застареле, међутим, постоје начини за претварање старих АПИ-ја без много труда.
  • Рад са нитима у .НЕТ-у представљен је класама Тхреад и ТхреадПоол
  • Методе Тхреад.Аборт, Тхреад.Интеррупт и Вин32 АПИ ТерминатеТхреад су опасне и не препоручују се за употребу. Уместо тога, боље је користити механизам ЦанцеллатионТокен
  • Проток је вредан ресурс и његово снабдевање је ограничено. Треба избегавати ситуације у којима су нити заузете чекањем догађаја. За ово је згодно користити класу ТаскЦомплетионСоурце
  • Најмоћнији и најнапреднији .НЕТ алати за рад са паралелизмом и асинхроношћу су Задаци.
  • Ц# оператори асинц/аваит имплементирају концепт чекања без блокирања
  • Можете да контролишете дистрибуцију задатака по нитима помоћу класа изведених из ТаскСцхедулера
  • Структура ВалуеТаск може бити корисна у оптимизацији врућих путева и меморијског саобраћаја
  • Висуал Студио прозори са задацима и нитима пружају много информација корисних за отклањање грешака у вишенитном или асинхроном коду
  • ПЛинк је кул алат, али можда нема довољно информација о вашем извору података, али то се може поправити помоћу механизма партиционирања
  • Наставиће се ...

Извор: ввв.хабр.цом

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