.NET: Multithreading жана асинхрония менен иштөө үчүн куралдар. 1-бөлүк

Мен Habr боюнча оригиналдуу макаланы жарыялап жатам, анын котормосу корпоративде жайгаштырылган блог.

Бул жерде жана азыр натыйжаны күтпөстөн, бир нерсени асинхрондук түрдө жасоо же чоң ишти аны аткарган бир нече бирдиктердин ортосунда бөлүштүрүү зарылчылыгы компьютерлер пайда болгонго чейин болгон. Алардын пайда болушу менен бул муктаждык абдан сезилип калды. Азыр, 2019-жылы, мен бул макаланы 8 ядролуу Intel Core процессору бар ноутбукка жазып жатам, анда жүздөн ашык процесс параллелдүү иштеп жатат, андан да көп жиптер. Жакын жерде бир нече жыл мурун сатылып алынган бир аз эскирген телефон бар, бортунда 8 ядролуу процессор бар. Тематикалык ресурстар макалаларга жана видеолорго толгон, анда алардын авторлору 16 ядролуу процессорлорду камтыган быйылкы флагмандык смартфондорго суктанышат. MS Azure 20 негизги процессору жана 128 ТБ оперативдүү эс тутуму бар виртуалдык машинаны саатына 2 доллардан азыраак камсыз кылат. Тилекке каршы, жиптердин өз ара аракеттенүүсүн башкара албай туруп, бул күчтү максималдуу түрдө алуу жана колдонуу мүмкүн эмес.

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

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

салыштыруу

Бардык аныктамалар жакшы эмес, кээ бирлери кошумча түшүндүрмөлөрдү талап кылат, ошондуктан мен расмий түрдө киргизилген терминологияга эртең мененки тамак жасоо жөнүндө метафораны кошом. Бул метафорада эртең мененки тамакты даярдоо процесс.

Эртең мененки тамакты даярдап жатып мен (CPU) Мен ашканага келдим (компьютер). менин 2 колум бар (Са). Ашканада бир нече аппараттар бар (IO): меш, чайнек, тостер, муздаткыч. Газды күйгүзүп, сковородканы коюп, ысыганын күтпөй май куюп жатам (асинхрондуу, бөгөттөлбөгөн IO-күтүү), Жумуртканы муздаткычтан алып тарелкага сындырып алам, анан бир колум менен чаап алам (Тема №1), жана экинчи (Тема №2) табак кармап туруу (Shared Resource). Эми чайнекти күйгүзгүм келет, бирок колум жетпейт (Thread Starvation) Бул убакыттын ичинде таба ысып кетет (Натыйжасын иштеп чыгуу) ага мен камчылаганымды куям. Мен чайнекке жетип, аны күйгүзүп, андагы суунун кайнап жатканын келесоо карап жатам (Blocking-IO-Wait), бирок бул убакыттын ичинде ал омлет камчылаган табакты жууп алмак.

Мен 2 эле колду колдонуп омлет бышырдым, менде жок, бирок ошол эле учурда омлетті камчылоо учурунда бирден 3 операция жасалды: омлетти камчылоо, тарелканы кармоо, сковородканы ысытуу. Процессор компьютердин эң ылдам бөлүгү, IO көбүнчө бардыгы жайлайт, ошондуктан көп учурда эффективдүү чечим IOдон маалыматтарды алууда процессорду бир нерсе менен ээлөө болуп саналат.

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

  • Эгерде омлет даярдоо процессинде мен дагы кийим алмаштырууга аракет кылсам, бул көп тапшырманын мисалы болмок. Маанилүү нюанс: компьютерлер бул жагынан адамдарга караганда алда канча жакшы.
  • Бир нече ашпозчулар менен ашкана, мисалы, ресторанда - көп ядролуу компьютер.
  • Соода борборундагы фуд-кортто көптөгөн ресторандар - маалымат борбору

.NET куралдары

.NET башка көптөгөн нерселер сыяктуу жиптер менен иштөөдө жакшы. Ар бир жаңы версия менен, ал алар менен иштөө үчүн барган сайын жаңы куралдарды, OS жиптери боюнча абстракциянын жаңы катмарларын киргизет. Абстракцияларды куруу менен иштөөдө фреймворк иштеп чыгуучулар жогорку деңгээлдеги абстракцияны колдонууда бир же бир нече деңгээлге төмөндөө мүмкүнчүлүгүн калтырган ыкманы колдонушат. Көбүнчө бул зарыл эмес, чындыгында бул мылтык менен бутуңа атуу үчүн эшикти ачат, бирок кээде, сейрек учурларда, азыркы абстракция деңгээлинде чечилбеген маселени чечүүнүн жалгыз жолу болушу мүмкүн. .

Аспаптар деп мен алкак жана үчүнчү тараптын пакеттери тарабынан камсыз кылынган колдонмо программалоо интерфейстерин (API), ошондой эле көп жиптүү код менен байланышкан бардык көйгөйлөрдү издөөнү жөнөкөйлөтүүчү бүтүндөй программалык чечимдерди билдирет.

Жипти баштоо

Thread классы .NETте жиптер менен иштөө үчүн эң негизги класс болуп саналат. Конструктор эки делегаттын бирин кабыл алат:

  • ThreadStart — Параметрлер жок
  • ParametrizedThreadStart - типтеги объекттин бир параметри менен.

Делегат Start методун чакыргандан кийин жаңы түзүлгөн жипте аткарылат.Эгерде ParametrizedThreadStart түрүндөгү делегат конструкторго өткөрүлүп берилген болсо, анда объект Start методуна өтүшү керек. Бул механизм кайсы болбосун жергиликтүү маалыматты агымга өткөрүү үчүн керек. Белгилей кетчү нерсе, жипти түзүү кымбат операция, ал эми жиптин өзү оор объект, анткени ал стекке 1 МБ эстутумду бөлөт жана OS API менен өз ара аракеттенүүнү талап кылат.

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

ThreadPool классы бассейн түшүнүгүн билдирет. .NETте жип пулу бул инженериянын бир бөлүгү жана Microsoftтун иштеп чыгуучулары анын ар кандай сценарийлерде оптималдуу иштешине ынануу үчүн көп күч жумшашты.

Жалпы түшүнүк:

Тиркеме башталган учурдан тартып, ал фондо резервде бир нече жиптерди түзөт жана аларды колдонууга алуу мүмкүнчүлүгүн берет. Эгерде жиптер көп жана көп санда колдонулса, бассейн чалуучунун муктаждыктарын канааттандыруу үчүн кеңейет. Керектүү убакта бассейнде бош жиптер жок болгондо, ал жиптердин биринин кайтып келишин күтөт же жаңысын түзөт. Демек, жип пулу кээ бир кыска мөөнөттүү аракеттер үчүн эң сонун жана колдонмонун бүтүндөй иштеши боюнча кызмат катары иштеген операциялар үчүн начар ылайыктуу.

Бассейнден жипти колдонуу үчүн WaitCallback түрүндөгү делегатты кабыл алган QueueUserWorkItem ыкмасы бар, анын ParametrizedThreadStart менен бирдей кол тамгасы бар жана ага берилген параметр ошол эле функцияны аткарат.

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 терезесинде көрүүгө болот. Бул терезе тиркеме мүчүлүштүктөрдү жоюуда жана Break режиминде болгондо гана жип маалыматын көрсөтөт. Бул жерден сиз ар бир жиптин стек аттарын жана артыкчылыктарын ыңгайлуу көрө аласыз жана мүчүлүштүктөрдү оңдоону белгилүү бир жипке которсоңуз болот. Thread классынын Priority касиетин колдонуу менен сиз жиптин артыкчылыктуулугун орното аласыз, аны OC жана CLR процессордун убактысын жиптердин ортосунда бөлүштүрүүдө сунуш катары кабыл алышат.

.NET: Multithreading жана асинхрония менен иштөө үчүн куралдар. 1-бөлүк

Тапшырма параллелдүү китепкана

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

  • async - бул Тапшырма же жараксыздыкты кайтарган методдун өзгөрткүчү
  • жана күтүү - бөгөттөлбөгөн Тапшырманы күтүүчү оператор.

Дагы бир жолу: күтүү оператору, жалпы учурда (өзгөчө учурлар бар) учурдагы аткарылуучу жипти андан ары бошотот, ал эми Тапшырма өзүнүн аткарылышын аяктаганда жана жипти (чынында, контекстти айтуу туурараак болмок) , бирок бул тууралуу кийинчерээк) ыкманы мындан ары да аткарууну улантат. .NET ичинде, бул механизм кирешелүүлүктүн кайтарылышы сыяктуу эле ишке ашырылат, качан жазылган ыкма бүтүндөй класска айланганда, мамлекеттик машина болуп саналат жана бул мамлекеттерге жараша өзүнчө бөлүктөрдө аткарылышы мүмкүн. Ар бир кызыккан адам asyns/await аркылуу каалаган жөнөкөй кодду жаза алат, компиляцияны түзө алат жана JetBrains dotPeek аркылуу Compiler Generated Code иштетилген.

Тапшырманы ишке киргизүү жана колдонуу варианттарын карап көрөлү. Төмөндөгү код мисалында биз эч кандай пайдалуу болбогон жаңы тапшырманы түзөбүз (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 түрүндөгү пландоочу объект болуп саналат. Бул класс жана анын урпактары Тапшырмаларды жиптер боюнча бөлүштүрүү стратегияларын көзөмөлдөө үчүн иштелип чыккан; демейки боюнча, Тапшырма бассейнден туш келди жипте аткарылат.

Түзүлгөн тапшырмага күтүү оператору колдонулат, демек, андан кийин жазылган код, эгер бар болсо, күтүүгө чейинки код сыяктуу эле контекстте (көбүнчө бул бир жипте дегенди билдирет) аткарылат.

Метод асинхрондук жараксыз деп белгиленген, демек ал күтүү операторун колдоно алат, бирок чакыруу коду аткарылышын күтө албайт. Мындай өзгөчөлүк зарыл болсо, анда ыкма Тапшырма кайтарышы керек. Асинхрондук жараксыз деп белгиленген ыкмалар кеңири таралган: эреже катары, бул окуяларды иштеткичтер же өрт жана унутуу принцибинде иштеген башка ыкмалар. Эгер сиз аткаруунун аягына чейин күтүүгө мүмкүнчүлүк берип гана тим болбостон, натыйжаны да кайтарышыңыз керек болсо, анда 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
}

Биринчи мисалда биз Тапшырма чакыруучу жипти бөгөттөп бүткүчө күтөбүз; биз натыйжаны иштеп чыгууга ал мурунтан эле бар болгондо гана кайтабыз; ага чейин чакыруучу жип өз алдынча калат.

Экинчи вариантта биз ыкманын натыйжасы эсептелгенге чейин чакыруучу жипти бөгөттөйбүз. Бул жаман, анткени биз жипти, программанын ушундай баалуу ресурсун жөнөкөй бекерчилик менен ээлеп алганыбыз үчүн эмес, ошондой эле биз чакырган методдун коду күтүүнү камтыса, ал эми синхрондоштуруу контексти чакырып жаткан жипке кайра кайтууну талап кылса. күтө тур, анда биз туюкка учурайбыз : Чакыруу жип асинхрондук ыкманын натыйжасы эсептелип жатканда күтөт, асинхрондук ыкма чалуучу жипте өзүнүн аткарылышын улантуу үчүн бекер аракет кылат.

Бул ыкманын дагы бир кемчилиги каталарды чечүүнүн татаалдыгы болуп саналат. Чындыгында, асинхрондук коддогу каталарды асинхрондук/күтүүдөгү каталарды чечүү оңой - алар код синхрондуу болгон сыяктуу иштешет. Эгерде биз тапшырмага синхрондук күтүү эксорцизмин колдонсок, баштапкы өзгөчөлүк 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 ээлери операцияны өздөрү жокко чыгара алышпайт, бирок операциянын жокко чыгарылгандыгын текшере алышат. Бул үчүн буль касиети бар IsCancellationRequested жана ыкмасы ThrowIfCancelRequested. Акыркысы өзгөчө учурду жаратат TaskCancelledException эгерде Cancel ыкмасы тоту болуп жаткан CancellationToken инстанциясында чакырылса. Жана бул ыкманы мен колдонууну сунуштайм. Бул өзгөчө кырдаал операциясын кайсы учурда токтотсо болорун толук көзөмөлгө алуу аркылуу мурунку варианттарга караганда жакшыртылган.

Жипти токтотуунун эң катаал варианты - Win32 API TerminateThread функциясын чакыруу. Бул функцияны чакыргандан кийин CLRдин жүрүм-турумун алдын ала айтуу мүмкүн эмес. MSDNде бул функция жөнүндө төмөнкүлөр жазылган: "TerminateThread - бул эң өзгөчө учурларда гана колдонулушу керек болгон кооптуу функция. "

FromAsync ыкмасын колдонуу менен эски API'ни 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 ыкмаларына толгон.

TaskCompletionSource классын колдонуп, эски APIди Task Basedге айландырыңыз

Дагы бир маанилүү курал класс болуп саналат TaskCompletionSource. Функциялары, максаты жана иштөө принциби боюнча, ал мен жогоруда жазган ThreadPool классынын RegisterWaitForSingleObject ыкмасын бир аз эске салышы мүмкүн. Бул классты колдонуу менен сиз Tasks ичинде эски асинхрондук API'лерди оңой жана ыңгайлуу ороп алсаңыз болот.

Сиз мен бул максаттар үчүн арналган TaskFactory классынын FromAsync ыкмасы жөнүндө айтканмын деп айтасыз. Бул жерде биз Microsoft акыркы 15 жыл ичинде сунуш кылган .netтеги асинхрондук моделдердин өнүгүү тарыхын эстешибиз керек: Task-Based Asynchronous Pattern (TAP) алдында Асинхрондук программалоо үлгүсү (APP) болгон. ыкмалары жөнүндө болду баштооDoSomething кайтып келет IAsyncResult жана методдору EndАны кабыл алган DoSomething жана бул жылдардагы мурас үчүн FromAsync ыкмасы жөн гана идеалдуу, бирок убакыттын өтүшү менен ал Окуяга негизделген асинхрондук үлгү менен алмаштырылды (ЖАНА AP), асинхрондук операция аяктаганда окуя көтөрүлөт деп болжолдогон.

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

TaskCompletionSource Tips & Tricks

Эски API'лерди ороп коюу TaskCompletionSource аркылуу жасала турган нерсе эмес. Бул классты колдонуу жиптерди ээлебеген тапшырмаларда ар кандай APIлерди долбоорлоонун кызыктуу мүмкүнчүлүгүн ачат. Ал эми агым, биз эстегендей, кымбат ресурс жана алардын саны чектелген (негизинен RAM көлөмү менен). Бул чектөөгө, мисалы, татаал бизнес логикасы менен жүктөлгөн веб-тиркемени иштеп чыгуу менен оңой жетүүгө болот. Узак добуш берүү сыяктуу амалдарды ишке ашырууда мен айтып жаткан мүмкүнчүлүктөрдү карап көрөлү.

Кыскача айтканда, айла-амалдын маңызы мындай: сиз APIден анын тарабында болуп жаткан кээ бир окуялар жөнүндө маалымат алышыңыз керек, ал эми API кандайдыр бир себептерден улам окуяны билдире албайт, бирок абалды гана кайтара алат. Булардын мисалы катары HTTP үстүнө курулган бардык API'лер WebSocket мезгилине чейин же кандайдыр бир себептерден улам бул технологияны колдонуу мүмкүн эмес болуп калган. Кардар 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 - бул класс TaskScheduler жана анын туундулары. Мен жогоруда TPL темалар боюнча Тапшырмаларды бөлүштүрүү стратегияларын башкаруу мүмкүнчүлүгүнө ээ экенин айттым. Мындай стратегиялар TaskScheduler классынын урпактарында аныкталган. Сизге керек болгон дээрлик бардык стратегияларды китепканадан тапса болот. ParallelExtensionsExtras, Microsoft тарабынан иштелип чыккан, бирок .NETтин бир бөлүгү эмес, бирок Nuget пакети катары берилген. Алардын айрымдарына кыскача токтоло кетели:

  • CurrentThreadTaskScheduler — учурдагы жип боюнча тапшырмаларды аткарат
  • LimitedConcurrencyLevelTaskScheduler — конструктордо кабыл алынган N параметри боюнча бир убакта аткарылуучу тапшырмалардын санын чектейт
  • OrderedTaskScheduler — LimitedConcurrencyLevelTaskScheduler(1) катары аныкталган, ошондуктан тапшырмалар ырааттуу түрдө аткарылат.
  • WorkStealingTaskScheduler — аспаптар эмгек уурдоо тапшырманы бөлүштүрүүгө мамиле. Негизи бул өзүнчө ThreadPool. .NET ThreadPool бардык тиркемелер үчүн статикалык класс болуп саналат деген маселени чечет, бул программанын бир бөлүгүндө анын ашыкча жүктөлүшү же туура эмес колдонулушу экинчисинде терс таасирлерге алып келиши мүмкүн дегенди билдирет. Анын үстүнө, мындай кемчиликтердин себебин түшүнүү өтө кыйын. Ошол. ThreadPool колдонуу агрессивдүү жана күтүүсүз болушу мүмкүн болгон программанын бөлүктөрүндө өзүнчө WorkStealingTaskSchedulers колдонуу зарылчылыгы болушу мүмкүн.
  • QueuedTaskScheduler — приоритеттүү кезек эрежелери боюнча тапшырмаларды аткарууга мүмкүндүк берет
  • ThreadPerTaskScheduler — анда аткарылган ар бир Тапшырма үчүн өзүнчө жип түзөт. Аткаруу үчүн күтүүсүз көп убакыт талап кылынган тапшырмалар үчүн пайдалуу болушу мүмкүн.

Жакшы деталдары бар макала Microsoft блогунда TaskSchedulers жөнүндө.

Tasks менен байланышкан нерселердин мүчүлүштүктөрүн оңой оңдоо үчүн Visual Studio'нун Tasks терезеси бар. Бул терезеде сиз тапшырманын учурдагы абалын көрүп, коддун учурда аткарылып жаткан сабына өтсөңүз болот.

.NET: Multithreading жана асинхрония менен иштөө үчүн куралдар. 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 инструменттери Tasks болуп саналат.
  • C# async/await операторлору блоктолбогон күтүү концепциясын ишке ашырышат
  • TaskScheduler-ден алынган класстарды колдонуп, Тапшырмалардын жиптер боюнча бөлүштүрүлүшүн көзөмөлдөй аласыз
  • ValueTask түзүмү ысык жолдорду жана эс-трафигин оптималдаштырууда пайдалуу болушу мүмкүн
  • Visual Studio'нун Tasks and Threads терезелери көп агымдуу же асинхрондук кодду оңдоо үчүн пайдалуу көптөгөн маалыматты камсыз кылат.
  • PLinq - бул сонун курал, бирок ал маалымат булагы жөнүндө жетиштүү маалыматка ээ болбошу мүмкүн, бирок аны бөлүү механизми аркылуу оңдоого болот
  • Уландысы бар…

Source: www.habr.com

Комментарий кошуу