.NET: Олон урсгалтай болон асинхронтой ажиллах хэрэгслүүд. 1-р хэсэг

Би Хабрын тухай анхны нийтлэлийг нийтэлж байна, орчуулга нь корпорацид тавигдсан блог шуудан.

Энд, одоо үр дүнг хүлээхгүйгээр ямар нэг зүйлийг асинхрон хийх, эсвэл үүнийг гүйцэтгэж буй хэд хэдэн нэгжид том ажлыг хуваах хэрэгцээ нь компьютер гарч ирэхээс өмнө байсан. Тэдний гарч ирснээр энэ хэрэгцээ маш бодитой болсон. Одоо, 2019 онд би энэ нийтлэлийг 8 цөмт Intel Core процессортой зөөврийн компьютер дээр бичиж байна, зуу гаруй процесс зэрэгцээ ажиллаж байгаа, бүр илүү олон хэлхээтэй. Ойролцоох нь 8 цөмт процессортой, хэдэн жилийн өмнө худалдаж авсан бага зэрэг эвдэрсэн утас байна. Сэдвийн эх сурвалжууд 16 цөмт процессортой энэ жилийн шилдэг ухаалаг гар утсыг зохиогчид нь биширч буй нийтлэл, видео бичлэгээр дүүрэн байна. MS Azure нь 20 цөмт процессор, 128 TB RAM бүхий виртуал машиныг цагт 2 доллараас бага үнээр хангадаг. Харамсалтай нь утаснуудын харилцан үйлчлэлийг зохицуулах чадваргүйгээр хамгийн их хүчийг гаргаж, энэ хүчийг ашиглах боломжгүй юм.

Нэр томъёо

Үйл явц - OS объект, тусгаарлагдсан хаягийн орон зай нь урсгалуудыг агуулдаг.
Thread - үйлдлийн системийн объект, гүйцэтгэх хамгийн жижиг нэгж, процессын хэсэг, урсгалууд нь санах ой болон бусад нөөцийг процессын хүрээнд өөр хоорондоо хуваалцдаг.
Олон даалгавар - OS шинж чанар, хэд хэдэн процессыг нэгэн зэрэг ажиллуулах чадвар
Олон цөмт - процессорын шинж чанар, өгөгдөл боловсруулахад хэд хэдэн цөм ашиглах чадвар
Олон боловсруулалт - компьютерийн шинж чанар, физикийн хувьд хэд хэдэн процессортой нэгэн зэрэг ажиллах чадвар
Олон урсгалтай - үйл явцын шинж чанар, өгөгдөл боловсруулалтыг хэд хэдэн хэлхээнд хуваарилах чадвар.
Зэрэгцээ байдал - цаг хугацааны нэгжид хэд хэдэн үйлдлийг нэгэн зэрэг гүйцэтгэх
Асинхрон - энэ боловсруулалтыг дуусгахыг хүлээхгүйгээр үйлдлийг гүйцэтгэх; гүйцэтгэлийн үр дүнг дараа нь боловсруулж болно.

Метафор

Бүх тодорхойлолтууд тийм ч сайн биш бөгөөд зарим нь нэмэлт тайлбар шаарддаг тул албан ёсоор танилцуулсан нэр томъёонд өглөөний цай хийх тухай зүйрлэлийг нэмж оруулах болно. Энэ зүйрлэлээр өглөөний цайгаа хоол хийх нь үйл явц юм.

Өглөө өглөөний цай бэлдэж байхдаа би (CPU-ийн) Би гал тогооны өрөөнд ирлээ (Компьютер). Би 2 гартай (судалд). Гал тогооны өрөөнд хэд хэдэн төхөөрөмж байдаг (IO): зуух, данх, талх шарагч, хөргөгч. Би хий асаагаад хайруулын тавган дээр тавиад халаахыг нь хүлээлгүй тос хийнэ (асинхрон, Non-Blocking-IO-Wait), Би өндөгийг хөргөгчнөөс гаргаж аваад таваг болгон хувааж, дараа нь нэг гараараа цохив (Сэдэв №1), хоёрдугаарт (Сэдэв №2) хавтанг барих (Хуваалцсан нөөц). Одоо би данх асаахыг хүсч байна, гэхдээ надад гар хүрэхгүй байна (Thread өлсгөлөн) Энэ хугацаанд хайруулын таваг дулаарч (Үр дүнг боловсруулах) би ташуурдсан зүйлээ хийнэ. Би данх руу гараа сунган асаагаад дотор нь ус буцалж байгааг тэнэг харлаа (Блоклох-IO-Хүлээ), хэдийгээр энэ хугацаанд тэр омлет ташуурдсан таваг угааж болох байсан.

Би 2 гараараа омлет чанаж байсан, надад илүү байхгүй, гэхдээ тэр үед омлетыг ташуурдах мөчид омлет ташуурдах, таваг барих, хайруулын таваг халаах гэсэн 3 үйлдлийг нэг дор хийсэн. CPU нь компьютерийн хамгийн хурдан хэсэг бөгөөд IO нь ихэнхдээ бүх зүйл удааширдаг тул IO-ээс өгөгдөл хүлээн авахдаа CPU-г ямар нэгэн зүйлээр эзлэх нь үр дүнтэй шийдэл болдог.

Метафорыг үргэлжлүүлэх нь:

  • Хэрэв би омлет бэлтгэх явцад хувцсаа солихыг оролдох байсан бол энэ нь олон төрлийн ажил хийх жишээ болно. Нэг чухал нюанс: компьютерууд хүмүүсээс хамаагүй дээр байдаг.
  • Хэд хэдэн тогоочтой гал тогоо, жишээлбэл ресторанд - олон цөмт компьютер.
  • Худалдааны төв дэх хоолны талбай дахь олон ресторан - дата төв

.NET хэрэгслүүд

.NET нь бусад олон зүйлтэй адил thread-тэй ажиллахдаа сайн. Шинэ хувилбар бүрээр тэдэнтэй ажиллах илүү олон шинэ хэрэгслүүд, үйлдлийн систем дэх хийсвэрлэлийн шинэ давхаргуудыг танилцуулж байна. Хийсвэрлэлийн бүтээн байгуулалттай ажиллахдаа фреймворк хөгжүүлэгчид өндөр түвшний хийсвэрлэлийг ашиглахдаа нэг буюу түүнээс дээш түвшинд доош орох боломжийг үлдээх аргыг ашигладаг. Ихэнхдээ энэ нь шаардлагагүй, үнэндээ энэ нь буугаар хөлөө буудах үүд хаалгыг нээж өгдөг, гэхдээ заримдаа, ховор тохиолдолд энэ нь одоогийн хийсвэрлэлийн түвшинд шийдэгдээгүй асуудлыг шийдэх цорын ганц арга зам байж болох юм. .

Багаж хэрэгсэл гэж би хүрээ болон гуравдагч талын багцаар хангагдсан хэрэглээний програмчлалын интерфейс (API), олон урсгалтай кодтой холбоотой аливаа асуудлыг хайх ажлыг хялбаршуулдаг бүхэл бүтэн програм хангамжийн шийдлүүдийг хэлж байна.

Сэдэв эхлүүлж байна

Thread класс нь thread-тэй ажиллахад зориулагдсан .NET дэх хамгийн энгийн анги юм. Зохион байгуулагч хоёр төлөөлөгчийн аль нэгийг хүлээн авна:

  • ThreadStart - Ямар ч параметр байхгүй
  • ParametrizedThreadStart - төрлийн объектын нэг параметртэй.

Төлөөлөгч нь Start аргыг дуудсаны дараа шинээр үүсгэсэн хэлхээнд гүйцэтгэгдэх болно.Хэрэв ParametrizedThreadStart төрлийн төлөөлөгчийг бүтээгч рүү дамжуулсан бол Start арга руу объект дамжуулагдах ёстой. Энэ механизм нь орон нутгийн аливаа мэдээллийг урсгал руу шилжүүлэхэд шаардлагатай. Thread үүсгэх нь үнэтэй үйлдэл бөгөөд утас нь өөрөө хүнд объект гэдгийг тэмдэглэх нь зүйтэй, учир нь энэ нь ядаж стек дээр 1МБ санах ойг хуваарилж, OS API-тай харилцах шаардлагатай болдог.

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

ThreadPool анги нь усан сан гэсэн ойлголтыг илэрхийлдэг. NET-д thread-ийн сан нь инженерчлэлийн нэг хэсэг бөгөөд Microsoft-ын хөгжүүлэгчид үүнийг олон янзын хувилбаруудад оновчтой ажиллуулахын тулд маш их хүчин чармайлт гаргасан.

Ерөнхий ойлголт:

Аппликейшн ажиллаж эхэлснээс хойш арын дэвсгэр дээр хэд хэдэн хэлхээ үүсгэж, тэдгээрийг ашиглах боломжийг олгодог. Хэрэв утаснууд байнга, олон тоогоор ашиглагдаж байвал сан дуудагчийн хэрэгцээг хангахын тулд өргөжиж байна. Цөөрөмд зөв цагт чөлөөт утас байхгүй бол аль нэг хэлхээ буцаж ирэхийг хүлээх эсвэл шинээр үүсгэх болно. Үүнээс үзэхэд урсгалын сан нь зарим богино хугацааны үйлдлүүдэд тохиромжтой бөгөөд програмын бүх үйл ажиллагааны туршид үйлчилгээ хэлбэрээр ажилладаг үйлдлүүдэд тохиромжгүй байдаг.

Усан сангаас урсгалыг ашиглахын тулд ParametrizedThreadStart-тай ижил гарын үсэг бүхий WaitCallback төрлийн төлөөлөгчийг хүлээн авдаг QueueUserWorkItem арга байдаг бөгөөд түүнд дамжуулсан параметр нь ижил үүргийг гүйцэтгэдэг.

ThreadPool.QueueUserWorkItem(...);

Бага мэддэг урсгалын сангийн арга RegisterWaitForSingleObject нь блоклохгүй IO үйлдлүүдийг зохион байгуулахад ашиглагддаг. Арга руу дамжуулсан WaitHandle нь "Суралцсан" үед энэ аргад шилжүүлсэн төлөөлөгч дуудагдах болно.

ThreadPool.RegisterWaitForSingleObject(...)

.NET нь урсгал таймертай бөгөөд WinForms/WPF таймераас ялгаатай нь түүний зохицуулагчийг сангаас авсан thread дээр дууддаг.

System.Threading.Timer

Төлөөлөгчийг усан сангаас утас руу илгээх нэлээд чамин арга байдаг - BeginInvoke арга.

DelegateInstance.BeginInvoke

Би дээрх олон аргуудыг нэрлэж болох функцийн талаар товчхон дурдмаар байна - Kernel32.dll Win32 API-аас CreateThread. Экстерн аргын механизмын ачаар энэ функцийг дуудах арга бий. Өв залгамжлалын кодын аймшигт жишээн дээр би ийм дуудлагыг ганцхан удаа харсан бөгөөд яг үүнийг хийсэн зохиолчийн сэдэл миний хувьд нууц хэвээр байна.

Kernel32.dll CreateThread

Threads харах ба дибаг хийх

Таны үүсгэсэн Threads, бүх гуравдагч талын бүрэлдэхүүн хэсгүүд болон .NET сан зэргийг Visual Studio-ийн Threads цонхноос харж болно. Энэ цонх нь зөвхөн програм дибаг хийж, Break горимд байх үед урсгалын мэдээллийг харуулах болно. Эндээс та хэлхээ бүрийн стекийн нэр, тэргүүлэх чиглэлийг хялбархан харж, дибаг хийхийг тодорхой хэлхээ рүү шилжүүлэх боломжтой. Thread классын Priority шинж чанарыг ашиглан та урсгалын тэргүүлэх чиглэлийг тохируулж болох бөгөөд OC болон CLR нь процессорын цагийг thread хооронд хуваахдаа зөвлөмж болгон хүлээн авах болно.

.NET: Олон урсгалтай болон асинхронтой ажиллах хэрэгслүүд. 1-р хэсэг

Даалгаврын зэрэгцээ номын сан

Task Parallel Library (TPL) .NET 4.0-д нэвтэрсэн. Одоо энэ нь асинхронтой ажиллах стандарт, гол хэрэгсэл юм. Хуучин аргыг ашигладаг аливаа кодыг хуучин гэж үзнэ. TPL-ийн үндсэн нэгж нь System.Threading.Tasks нэрийн талбараас Task анги юм. Даалгавар гэдэг нь thread дээрх хийсвэрлэл юм. C# хэлний шинэ хувилбараар бид Tasks - async/await operators-тэй ажиллах гоёмсог аргатай болсон. Эдгээр ойлголтууд нь асинхрон кодыг энгийн бөгөөд синхрон мэт бичих боломжийг олгосон бөгөөд энэ нь утаснуудын дотоод ажиллагааны талаар бага ойлголттой хүмүүст ч тэдгээрийг ашигладаг програмууд, урт үйлдэл хийх үед хөлддөггүй програмуудыг бичих боломжтой болгосон. Async/wait ашиглах нь нэг юм уу хэд хэдэн нийтлэлд зориулагдсан сэдэв боловч би цөөн хэдэн өгүүлбэрээр түүний гол санааг олж мэдэхийг хичээх болно:

  • async нь Task эсвэл void буцаах аргын хувиргагч юм
  • болон await нь блоклохгүй Task хүлээж оператор юм.

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

Даалгаврыг эхлүүлэх, ашиглах сонголтуудыг харцгаая. Доорх кодын жишээнд бид ямар ч ашиггүй шинэ даалгавар үүсгэдэг (Thread.Sleep(10000)), гэхдээ бодит амьдрал дээр энэ нь нарийн төвөгтэй CPU-ийн эрчимтэй ажил байх ёстой.

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 төрлийн төлөвлөгч объект юм. Энэ анги болон түүний удамшлууд нь даалгавруудыг урсгалуудаар тараах стратегийг удирдахад зориулагдсан бөгөөд анхдагч байдлаар, даалгаврыг сангаас санамсаргүй хэлхээгээр гүйцэтгэх болно.

Үүсгэсэн даалгаварт await операторыг ашигладаг бөгөөд энэ нь хэрэв байгаа бол түүний дараа бичигдсэн код нь хүлээхээс өмнөх кодтой ижил контекст (ихэвчлэн нэг хэлхээ дээр) хийгдэнэ гэсэн үг юм.

Арга нь async void гэж тэмдэглэгдсэн бөгөөд энэ нь await операторыг ашиглах боломжтой гэсэн үг боловч дуудлагын код гүйцэтгэлийг хүлээх боломжгүй болно. Хэрэв ийм функц шаардлагатай бол арга нь Task-ыг буцаана. Асинхронгүй хүчингүй гэж тэмдэглэсэн аргууд нь нэлээд түгээмэл байдаг: дүрмээр бол эдгээр нь үйл явдал зохицуулагч эсвэл гал болон мартах зарчим дээр ажилладаг бусад аргууд юм. Хэрэв та гүйцэтгэлийн төгсгөл хүртэл хүлээх боломжийг олгохоос гадна үр дүнг нь буцаах шаардлагатай бол Task ашиглах хэрэгтэй.

StartNew аргын буцаасан Task дээр болон бусад аль ч дээр та 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 chain өөрөө бичих эсвэл барихдаа catch-г ашиглах хэрэгтэй болно.

Гурав дахь болон эцсийн жишээнүүд нь ижил шалтгаанаар муу гэж тэмдэглэгдсэн бөгөөд ижил асуудлуудыг агуулна.

WhenAny болон WhenAll аргууд нь бүлэг даалгавруудыг хүлээхэд нэн тохиромжтой бөгөөд тэдгээр нь бүлгийн даалгавруудыг нэг болгон нэгтгэдэг бөгөөд энэ нь бүлгийн даалгавар анх идэвхжсэн үед эсвэл бүгд гүйцэтгэлээ дуусгасны дараа ажиллах болно.

Утаснуудыг зогсоох

Янз бүрийн шалтгааны улмаас урсгалыг эхлүүлсний дараа зогсоох шаардлагатай байж болно. Үүнийг хийх хэд хэдэн арга байдаг. Thread анги нь зохих нэртэй хоёр аргатай: Үр хөндөлт и тасалдал. Эхнийх нь хэрэглэхийг зөвлөдөггүй, учир нь ямар ч санамсаргүй мөчид дуудсаны дараа, аливаа зааврыг боловсруулах явцад онцгой тохиолдол гарах болно ThreadAbortedException. Бүхэл тоон хувьсагчийг нэмэгдүүлэхэд ийм онцгой тохиолдол гарна гэж та бодохгүй байна, тийм ээ? Мөн энэ аргыг ашиглах үед энэ нь маш бодит нөхцөл юм. Хэрэв та кодын тодорхой хэсэгт CLR-ийг ийм үл хамаарах зүйл үүсгэхээс урьдчилан сэргийлэх шаардлагатай бол үүнийг дуудлагад оруулах боломжтой. Thread.BeginCriticalRegion, Thread.EndCriticalRegion. Finally блокт бичигдсэн аливаа код ийм дуудлагад ороогдоно. Ийм учраас хүрээ кодын гүнээс та хоосон оролдлого бүхий блокуудыг олох боломжтой, гэхдээ эцэст нь хоосон биш. Майкрософт энэ аргыг маш ихээр хориглодог тул тэд үүнийг .net core-д оруулаагүй болно.

Тасалдлын арга нь илүү урьдчилан таамаглах боломжтой ажилладаг. Энэ нь үл хамаарах зүйлээр утсыг тасалдуулж болно ThreadInterruptedException зөвхөн утас хүлээгдэж буй үед л. Энэ нь WaitHandle-г хүлээх, түгжих эсвэл Thread.Sleep-г дуудсаны дараа унжиж байх үед энэ төлөвт ордог.

Дээр дурдсан хоёр сонголт хоёулаа таамаглах боломжгүй тул муу байна. Үүний шийдэл нь бүтцийг ашиглах явдал юм CancellationToken болон анги CancellationTokenSource. Гол нь: CancellationTokenSource ангийн жишээ үүсгэгдсэн бөгөөд зөвхөн үүнийг эзэмшдэг хүн л аргыг дуудаж үйл ажиллагааг зогсоож чадна. Болих. Зөвхөн CancellationToken нь үйлдэлд өөрөө дамждаг. CancellationToken эзэмшигчид өөрсдөө үйлдлийг цуцлах боломжгүй, гэхдээ зөвхөн үйлдлийг цуцалсан эсэхийг шалгах боломжтой. Үүнд зориулсан Boolean шинж чанар байдаг CancellationRequested ба арга ThrowIfCancelRequested. Сүүлийнх нь онцгой тохиолдол гаргах болно TaskCancelledException хэрэв Cancel аргыг тоть болж байгаа CancellationToken жишээн дээр дуудсан бол. Мөн энэ бол миний ашиглахыг санал болгож буй арга юм. Энэ нь ямар үед үл хамаарах үйлдлийг зогсоож болохыг бүрэн хянах замаар өмнөх сонголтуудаас сайжруулсан юм.

Сэдвийг зогсоох хамгийн харгис сонголт бол Win32 API TerminateThread функцийг дуудах явдал юм. Энэ функцийг дуудсаны дараа CLR-ийн үйлдлийг урьдчилан таамаглах аргагүй байж болно. MSDN дээр энэ функцийн талаар дараахь зүйлийг бичсэн болно. “TerminateThread нь аюултай функц бөгөөд зөвхөн онцгой тохиолдолд л ашиглах ёстой. "

FromAsync аргыг ашиглан хуучин API-г Task Based болгон хөрвүүлэх

Хэрэв та Tasks-ыг нэвтрүүлсний дараа эхлүүлсэн төсөл дээр ажиллах хангалттай азтай бол ихэнх хөгжүүлэгчдэд чимээгүйхэн айдас төрүүлэхээ больсон бол та гуравдагч этгээдийн болон танай багийн аль алинд нь олон хуучин API-уудтай ажиллах шаардлагагүй болно. өмнө нь эрүүдэн шүүж байсан. Аз болоход .NET Framework-ийн баг биднийг халамжилдаг байсан ч зорилго нь өөрсдийгөө халамжлах байсан байж магадгүй юм. Гэсэн хэдий ч .NET нь хуучин асинхрон програмчлалын арга барилаар бичигдсэн кодыг шинэ код руу өвдөлтгүй хөрвүүлэх олон хэрэгсэлтэй. Тэдний нэг нь TaskFactory-ийн FromAsync арга юм. Доорх кодын жишээн дээр би WebRequest ангийн хуучин синхрончлолын аргуудыг энэ аргыг ашиглан Task-д оруулав.

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 дэх асинхрон загваруудын хөгжлийн түүхийг бүхэлд нь санах хэрэгтэй болно: Даалгаварт суурилсан асинхрон загвар (TAP) -аас өмнө асинхрон програмчлалын загвар (APP) байсан. аргуудын тухай байсан ЭхлэхDoSomething буцаж байна IAsyncResult болон аргууд ТөгсгөлDoSomething үүнийг хүлээн зөвшөөрч, эдгээр жилүүдийн өв залгамжлалын хувьд FromAsync арга нь төгс төгөлдөр боловч цаг хугацаа өнгөрөхөд үүнийг Үйл явдалд суурилсан асинхрон загвараар сольсон (EAP), асинхрон ажиллагаа дуусахад үйл явдал гарна гэж үзсэн.

TaskCompletionSource нь үйл явдлын загварт баригдсан Tasks болон хуучин API-уудыг багцлахад төгс төгөлдөр юм. Үүний ажлын мөн чанар нь дараах байдалтай байна: энэ ангийн объект нь Task төрлийн нийтийн өмчтэй бөгөөд түүний төлөвийг TaskCompletionSource ангийн SetResult, SetException гэх мэт аргуудаар удирдаж болно. Хүлээж буй операторыг энэ даалгаварт ашигласан газруудад TaskCompletionSource-д ашигласан аргаас хамааран энэ нь биелэх эсвэл бүтэлгүйтэх болно. Хэрэв энэ нь тодорхойгүй хэвээр байгаа бол зарим хуучин EAP API-г TaskCompletionSource ашиглан Task-д ороосон байгаа энэ кодын жишээг харцгаая: үйл явдал эхлэхэд Даалгаврыг Дууссан төлөв рүү шилжүүлэх ба хүлээх операторыг ашигласан аргыг харцгаая. Энэ даалгавар нь объектыг хүлээн авснаар гүйцэтгэлээ үргэлжлүүлнэ үр дүн.

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

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

    result completionSource.Task;
}

Даалгаврыг гүйцээх Эх сурвалжийн зөвлөмж ба заль мэх

Хуучин API-г боох нь TaskCompletionSource ашиглан хийж болох бүх зүйл биш юм. Энэ ангийг ашиглах нь thread-үүдийг эзэлдэггүй Tasks дээр төрөл бүрийн API дизайн хийх сонирхолтой боломжийг нээж өгдөг. Мөн урсгал нь бидний санаж байгаагаар үнэтэй нөөц бөгөөд тэдгээрийн тоо хязгаарлагдмал байдаг (гол төлөв RAM-ийн хэмжээгээр). Жишээлбэл, бизнесийн нарийн төвөгтэй логик бүхий ачаалагдсан вэб програмыг хөгжүүлснээр энэ хязгаарлалтыг хялбархан хийж болно. Урт санал хураалт гэх мэт мэхийг хэрэгжүүлэхдээ миний яриад байгаа боломжуудыг авч үзье.

Товчхондоо, заль мэхний мөн чанар нь энэ юм: та API-аас түүний талд болж буй зарим үйл явдлын талаар мэдээлэл авах шаардлагатай байдаг бол API нь ямар нэг шалтгааны улмаас үйл явдлыг мэдээлэх боломжгүй, харин зөвхөн төлөвийг буцааж өгөх боломжтой. Эдгээрийн нэг жишээ бол WebSocket-ээс өмнө эсвэл ямар нэг шалтгаанаар энэ технологийг ашиглах боломжгүй үед HTTP дээр бүтээгдсэн бүх API юм. Үйлчлүүлэгч HTTP серверээс асууж болно. HTTP сервер өөрөө үйлчлүүлэгчтэй холбоо тогтоож чадахгүй. Энгийн шийдэл бол таймер ашиглан серверт санал асуулга хийх боловч энэ нь сервер дээр нэмэлт ачаалал үүсгэж, TimerInterval / 2 дундажаар нэмэлт саатал үүсгэдэг. Үүнийг даван туулахын тулд Long Polling хэмээх заль мэхийг зохион бүтээсэн бөгөөд энэ нь таймераас хариу өгөхийг хойшлуулдаг. Хугацаа дуусах эсвэл үйл явдал гарах хүртэл сервер. Хэрэв үйл явдал болсон бол түүнийг боловсруулж, үгүй ​​бол хүсэлтийг дахин илгээнэ.

while(!eventOccures && !timeoutExceeded)  {

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

Гэвч үйл явдлыг хүлээж буй үйлчлүүлэгчдийн тоо нэмэгдэнгүүт ийм шийдэл аймшигтай болох нь батлагдна, учир нь... Ийм үйлчлүүлэгч бүр үйл явдлыг хүлээж буй бүхэл бүтэн хэлхээг эзэлдэг. Тийм ээ, үйл явдал эхлэхэд бид 1 мс-ийн нэмэлт саатал авдаг, ихэнхдээ энэ нь тийм ч чухал биш боловч яагаад програм хангамжийг байж болохоос дордуулдаг вэ? Хэрэв бид Thread.Sleep(1)-ийг устгавал дэмий хоосон циклээр эргэлддэг процессорын нэг цөмийг 100% сул ачаалах болно. TaskCompletionSource-ийг ашигласнаар та энэ кодыг хялбархан дахин хийж, дээр дурдсан бүх асуудлыг шийдэж чадна:

class LongPollingApi {

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

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

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

    public void SendMessage(int userId, Msg m) {

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

Энэ код нь үйлдвэрлэлд бэлэн биш, харин зүгээр л демо юм. Үүнийг бодит тохиолдлуудад ашиглахын тулд та дор хаяж хэн ч үүнийг хүлээж байгаагүй цагт мессеж ирэх нөхцөл байдлыг зохицуулах хэрэгтэй: энэ тохиолдолд AsseptMessageAsync арга нь аль хэдийн дууссан ажлыг буцаах ёстой. Хэрэв энэ нь хамгийн түгээмэл тохиолдол бол та ValueTask ашиглах талаар бодож болно.

Бид мессежийн хүсэлтийг хүлээн авснаар толь бичигт TaskCompletionSource үүсгэж байрлуулж, дараа нь юу болохыг хүлээнэ үү: заасан хугацааны интервал дуусах эсвэл мессеж хүлээн авах.

ValueTask: яагаад, яаж

Async/await операторууд нь өгөөж буцаах операторын нэгэн адил аргаас төлөвийн машин үүсгэдэг бөгөөд энэ нь шинэ объект үүсгэх явдал бөгөөд энэ нь бараг үргэлж чухал биш боловч ховор тохиолдолд асуудал үүсгэж болно. Энэ тохиолдол нь маш олон удаа дуудагддаг арга байж болох юм, бид секундэд хэдэн арван, хэдэн зуун мянган дуудлагын тухай ярьж байна. Хэрэв ийм аргыг ихэнх тохиолдолд хүлээх бүх аргуудыг алгасаж үр дүнг буцаадаг байдлаар бичсэн бол .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));
}

Энэ кодын хэсгийг нарийвчлан авч үзье: хэрэв кэшэд утга байгаа бол бид бүтцийг бий болгодог, эс тэгвээс бодит даалгавар нь утга учиртай байх болно. Дуудлага хийх код нь энэ кодыг аль замд гүйцэтгэсэн нь хамаагүй: C# синтакс талаас нь авч үзвэл ValueTask нь энэ тохиолдолд ердийн Task-тай адил ажиллах болно.

TaskSchedulers: даалгавар эхлүүлэх стратегийг удирдах

Миний авч үзэхийг хүсч буй дараагийн API бол анги юм Task Scheduler ба түүний деривативууд. TPL нь даалгавруудыг урсгалаар тараах стратегийг удирдах чадвартай гэдгийг би дээр дурдсан. Ийм стратеги нь TaskScheduler ангийн удамд тодорхойлогддог. Танд хэрэгтэй байж болох бараг бүх стратегийг номын сангаас олж болно. Parallel Extensions Нэмэлтүүд, Microsoft-аас боловсруулсан боловч .NET-ийн нэг хэсэг биш, харин Nuget багц хэлбэрээр нийлүүлсэн. Тэдгээрийн заримыг товчхон авч үзье:

  • CurrentThreadTaskScheduler — одоогийн урсгал дээр даалгавруудыг гүйцэтгэдэг
  • ХязгаарлагдмалConcurrencyLevelTaskScheduler — нэгэн зэрэг гүйцэтгэсэн даалгаврын тоог бүтээгчид хүлээн зөвшөөрсөн N параметрээр хязгаарладаг.
  • Ordered Task Scheduler — нь LimitedConcurrencyLevelTaskScheduler(1) гэж тодорхойлогдсон тул даалгавруудыг дараалан гүйцэтгэх болно.
  • WorkStealingTaskScheduler - хэрэглүүр ажлын хулгай ажил хуваарилах хандлага. Үндсэндээ энэ нь тусдаа ThreadPool юм. .NET ThreadPool нь бүх хэрэглээнд зориулагдсан статик анги байдаг тул програмын нэг хэсэгт хэт ачаалал өгөх, буруу ашиглах нь нөгөө хэсэгт сөрөг үр дагаварт хүргэж болзошгүй гэсэн асуудлыг шийддэг. Түүнээс гадна ийм согогийн шалтгааныг ойлгоход туйлын хэцүү байдаг. Тэр. ThreadPool ашиглах нь түрэмгий, урьдчилан таамаглах аргагүй байж болох програмын хэсгүүдэд тусдаа WorkStealingTaskSchedulers ашиглах шаардлагатай байж магадгүй юм.
  • QueuedTaskScheduler — тэргүүлэх дарааллын дүрмийн дагуу даалгавруудыг гүйцэтгэх боломжийг танд олгоно
  • ThreadPerTaskScheduler — үүн дээр хийгдэж буй ажил бүрийн хувьд тусдаа хэлхээ үүсгэдэг. Урьдчилан таамаглахын аргагүй удаан хугацаа шаардсан ажлуудад хэрэгтэй байж болно.

Сайхан нарийвчилсан байна нийтлэл Microsoft блог дээрх TaskSchedulers-ийн тухай.

Tasks-тай холбоотой бүх зүйлийг дибаг хийхэд тохиромжтой Visual Studio нь Tasks цонхтой. Энэ цонхонд та даалгаврын одоогийн төлөвийг харж, одоо ажиллаж байгаа кодын мөр рүү шилжих боломжтой.

.NET: Олон урсгалтай болон асинхронтой ажиллах хэрэгслүүд. 1-р хэсэг

PLinq ба зэрэгцээ анги

Даалгаврууд болон тэдгээрийн талаар хэлсэн бүх зүйлээс гадна .NET-д өөр хоёр сонирхолтой хэрэгсэл бий: PLinq (Linq2Parallel) болон Parallel анги. Эхнийх нь олон урсгал дээр бүх Linq үйлдлийг зэрэгцүүлэн гүйцэтгэхийг амлаж байна. Thread-ийн тоог WithDegreeOfParallelism өргөтгөлийн аргыг ашиглан тохируулж болно. Харамсалтай нь ихэвчлэн PLinq нь анхдагч горимдоо хурдыг мэдэгдэхүйц нэмэгдүүлэхийн тулд таны мэдээллийн эх сурвалжийн дотоод байдлын талаар хангалттай мэдээлэлгүй байдаг, нөгөө талаас оролдох зардал маш бага байдаг: та зүгээр л AsParallel аргыг дуудах хэрэгтэй. Linq аргуудын гинжин хэлхээ ба гүйцэтгэлийн тестийг ажиллуул. Түүнчлэн хуваалтын механизмыг ашиглан өгөгдлийн эх сурвалжийн мөн чанарын талаарх нэмэлт мэдээллийг PLinq-д дамжуулах боломжтой. Та илүү ихийг уншиж болно энд и энд.

Parallel static класс нь Foreach цуглуулгыг зэрэгцээ давталт хийх, For давталтыг гүйцэтгэх, олон төлөөлөгчдийг зэрэгцүүлэн Invoke гүйцэтгэх аргуудыг хангадаг. Тооцоолол дуусах хүртэл одоогийн хэлхээний гүйцэтгэл зогсох болно. Сүүлийн аргумент болгон ParallelOptions-ийг дамжуулснаар хэлхээний тоог тохируулж болно. Та мөн сонголтуудыг ашиглан TaskScheduler болон CancellationToken-г зааж өгч болно.

үр дүн нь

Тайлангийнхаа материал, түүний дараа ажиллаж байхдаа цуглуулсан мэдээлэлдээ тулгуурлан энэхүү нийтлэлийг бичиж эхлэхдээ ийм их юм болно гэж төсөөлөөгүй. Одоо, миний энэ нийтлэлийг бичиж байгаа текст засварлагч 15-р хуудас дууссан гэж зэмлэн хэлэхэд би завсрын үр дүнг нэгтгэн дүгнэх болно. Бусад заль мэх, API, харааны хэрэгсэл, бэрхшээлийг дараагийн өгүүллээр авч үзэх болно.

Дүгнэлт:

  • Орчин үеийн компьютеруудын нөөцийг ашиглахын тулд утас, асинхрон, параллелизмтэй ажиллах хэрэгслүүдийг мэдэх хэрэгтэй.
  • .NET-д эдгээр зорилгоор олон төрлийн хэрэгслүүд байдаг
  • Тэд бүгд нэг дор гарч ирээгүй тул та ихэвчлэн хуучин API-г олох боломжтой, гэхдээ хуучин API-г их хүчин чармайлтгүйгээр хөрвүүлэх арга замууд байдаг.
  • .NET дээр thread-уудтай ажиллах нь Thread болон ThreadPool классуудаар илэрхийлэгддэг
  • Thread.Abort, Thread.Interrupt, Win32 API TerminateThread аргууд нь аюултай тул хэрэглэхийг зөвлөдөггүй. Үүний оронд CancellationToken механизмыг ашиглах нь дээр
  • Урсгал бол үнэ цэнэтэй нөөц бөгөөд нийлүүлэлт нь хязгаарлагдмал. Утаснууд үйл явдлыг хүлээж завгүй байгаа нөхцөл байдлаас зайлсхийх хэрэгтэй. Үүний тулд TaskCompletionSource классыг ашиглах нь тохиромжтой
  • Зэрэгцээ болон асинхронтой ажиллах хамгийн хүчирхэг, дэвшилтэт .NET хэрэгслүүд бол Tasks юм.
  • c# async/await операторууд нь блоклохгүй хүлээх ойлголтыг хэрэгжүүлдэг
  • Та TaskScheduler-аас үүсэлтэй ангиудыг ашиглан урсгалуудын хооронд даалгаврын хуваарилалтыг хянах боломжтой
  • ValueTask бүтэц нь халуун зам болон санах ойн урсгалыг оновчтой болгоход тустай байж болно
  • Visual Studio-ийн Tasks and Threads цонхнууд нь олон урсгалтай эсвэл асинхрон кодыг дибаг хийхэд хэрэгтэй олон мэдээллийг өгдөг.
  • PLinq бол гайхалтай хэрэгсэл боловч таны өгөгдлийн эх сурвалжийн талаар хангалттай мэдээлэлгүй байж болох ч үүнийг хуваах механизм ашиглан засч болно.
  • Үргэлжлэл бий…

Эх сурвалж: www.habr.com

сэтгэгдэл нэмэх