.NET. Multithreading-ի և asynchrony-ի հետ աշխատելու գործիքներ: Մաս 1

Հրապարակում եմ Հաբրի հոդվածի բնօրինակը, որի թարգմանությունը տեղադրված է կորպորատիվում բլոգի գրառումը.

Ինչ-որ բան ասինխրոն կերպով անելու անհրաժեշտությունը, առանց արդյունքի սպասելու այստեղ և հիմա, կամ մեծ աշխատանքը բաժանելու այն մի քանի ստորաբաժանումների միջև, գոյություն ուներ մինչև համակարգիչների հայտնվելը: Նրանց գալուստով այս կարիքը շատ շոշափելի դարձավ։ Այժմ՝ 2019 թվականին, ես մուտքագրում եմ այս հոդվածը 8 միջուկանի Intel Core պրոցեսորով նոութբուքի վրա, որի վրա զուգահեռ աշխատում են հարյուրից ավելի գործընթացներ և նույնիսկ ավելի շատ թելեր։ Մոտակայքում կա մի քիչ մաշված հեռախոս, գնվել է մի քանի տարի առաջ, վրան ունի 8 միջուկանոց պրոցեսոր։ Թեմատիկ ռեսուրսները լի են հոդվածներով և տեսանյութերով, որտեղ դրանց հեղինակները հիանում են այս տարվա առաջատար սմարթֆոններով, որոնք ունեն 16 միջուկային պրոցեսորներ: MS Azure-ը տրամադրում է վիրտուալ մեքենա 20 միջուկային պրոցեսորով և 128 TB RAM-ով 2 դոլար/ժամից պակաս գնով: Ցավոք, անհնար է առավելագույնը հանել և օգտագործել այս ուժը՝ առանց թելերի փոխազդեցությունը կառավարելու հնարավորության:

Տերմինոլոգիա

Գործընթացը - OS օբյեկտ, մեկուսացված հասցեի տարածք, պարունակում է թելեր:
Թեմա - OS օբյեկտ, կատարման ամենափոքր միավորը, գործընթացի մի մասը, թելերը կիսում են հիշողությունը և այլ ռեսուրսները գործընթացի շրջանակներում:
Multitasking- ը - ՕՀ-ի հատկություն, մի քանի գործընթացներ միաժամանակ գործարկելու հնարավորություն
Բազմամիջուկ - պրոցեսորի հատկություն, տվյալների մշակման համար մի քանի միջուկներ օգտագործելու հնարավորություն
Բազմամշակում - համակարգչի հատկություն, մի քանի պրոցեսորների հետ միաժամանակ ֆիզիկապես աշխատելու ունակություն
Multithreading — գործընթացի հատկություն, տվյալների մշակումը մի քանի թելերի միջև բաշխելու ունակություն:
Զուգահեռություն - ժամանակի միավորի համար միաժամանակ մի քանի գործողություններ կատարելը ֆիզիկապես
Ասինխրոնիա — գործողության կատարում՝ առանց սպասելու այս մշակման ավարտին, կատարման արդյունքը կարող է մշակվել ավելի ուշ։

Փոխաբերություն

Ոչ բոլոր սահմանումները լավն են, և ոմանք լրացուցիչ բացատրության կարիք ունեն, այնպես որ ես կավելացնեմ նախաճաշ պատրաստելու փոխաբերություն պաշտոնապես ներկայացված տերմինաբանությանը: Այս փոխաբերությամբ նախաճաշ պատրաստելը գործընթաց է:

Առավոտյան նախաճաշ պատրաստելիս ես (CPUԵս գալիս եմ խոհանոց (Համակարգիչ) Ես ունեմ 2 ձեռք (Cores) Խոհանոցում կան մի շարք սարքեր (IO): ջեռոց, թեյնիկ, տոստեր, սառնարան: Միացնում եմ գազը, վրան տապակ եմ դնում ու մեջը ձեթ լցնում առանց սպասելու, որ տաքանա (asynchronously, Non-Blocking-IO-Wait), ձվերը հանում եմ սառնարանից և ջարդում ափսեի մեջ, հետո մի ձեռքով հարում եմ (Թեմա թիվ 1), և երկրորդը (Թեմա թիվ 2) պահելով ափսեը (Համօգտագործվող ռեսուրս): Հիմա ես կուզենայի միացնել թեյնիկը, բայց ձեռքերս քիչ են (Թելային սովամահություն) Այս ընթացքում տաքանում է տապակը (Արդյունքի մշակում), որի մեջ լցնում եմ հարածս։ Ես ձեռքս մեկնում եմ դեպի թեյնիկը և միացնում այն ​​և հիմարորեն նայում եմ, թե ինչպես է ջուրը եռում դրա մեջ (Արգելափակում-IO-Սպասեք), չնայած այս ընթացքում նա կարող էր լվանալ այն ափսեն, որտեղ հարել էր ձվածեղը։

Ես ձվածեղ եմ պատրաստել ընդամենը 2 ձեռքով, իսկ ավելին չունեմ, բայց միևնույն ժամանակ ձվածեղը հարելու պահին տեղի է ունեցել միանգամից 3 գործողություն՝ ձվածեղը հարել, ափսեը պահել, տապակը տաքացնել։ CPU-ն համակարգչի ամենաարագ մասն է, IO-ն այն է, ինչն ամենից հաճախ ամեն ինչ դանդաղում է, ուստի հաճախ արդյունավետ լուծում է IO-ից տվյալներ ստանալիս պրոցեսորը ինչ-որ բանով զբաղեցնելը:

Շարունակելով փոխաբերությունը.

  • Եթե ​​ձվածեղ պատրաստելու ընթացքում ես էլ փորձեի հագուստ փոխել, սա կլիներ բազմաբնույթ առաջադրանքների օրինակ։ Կարևոր նրբերանգ. համակարգիչներն այս հարցում շատ ավելի լավն են, քան մարդիկ:
  • Խոհանոց մի քանի խոհարարներով, օրինակ ռեստորանում՝ բազմաբնույթ համակարգիչ։
  • Բազմաթիվ ռեստորաններ առևտրի կենտրոնում գտնվող սննդի դատարանում՝ տվյալների կենտրոն

.NET գործիքներ

.NET-ը լավ է աշխատում թելերի հետ, ինչպես շատ այլ բաների դեպքում: Յուրաքանչյուր նոր տարբերակով այն ավելի ու ավելի շատ նոր գործիքներ է ներմուծում դրանց հետ աշխատելու համար, աբստրակցիայի նոր շերտեր ՕՀ թելերի վրա: Աբստրակցիաների կառուցման հետ աշխատելիս շրջանակային մշակողները օգտագործում են այնպիսի մոտեցում, որը հնարավորություն է տալիս բարձր մակարդակի աբստրակցիա օգտագործելիս մեկ կամ մի քանի մակարդակ ներքև իջնել: Ամենից հաճախ դա անհրաժեշտ չէ, իրականում դա դուռ է բացում որսորդական հրացանով ոտքդ կրակելու համար, բայց երբեմն, հազվադեպ դեպքերում, դա կարող է լինել միակ ճանապարհը լուծելու խնդիրը, որը չի լուծվում ներկայիս վերացական մակարդակում: .

Գործիքներ ասելով նկատի ունեմ և՛ կիրառական ծրագրավորման միջերեսները (API), որոնք տրամադրվում են շրջանակի և երրորդ կողմի փաթեթների կողմից, ինչպես նաև ամբողջական ծրագրային լուծումներ, որոնք պարզեցնում են բազմաշերտ կոդի հետ կապված ցանկացած խնդիրների որոնումը:

Թեմա սկսելը

Thread դասը .NET-ում թելերի հետ աշխատելու ամենահիմնական դասն է: Կառուցողն ընդունում է երկու պատվիրակներից մեկին.

  • ThreadStart — Պարամետրեր չկան
  • ParametrizedThreadStart - տիպի օբյեկտի մեկ պարամետրով:

Պատվիրակը կկատարվի նոր ստեղծված շղթայում Start մեթոդը կանչելուց հետո: Եթե ParametrizedThreadStart տիպի պատվիրակ փոխանցվել է կոնստրուկտորին, ապա օբյեկտը պետք է փոխանցվի Start մեթոդին: Այս մեխանիզմն անհրաժեշտ է ցանկացած տեղական տեղեկատվություն հոսքին փոխանցելու համար: Հարկ է նշել, որ թեմա ստեղծելը թանկ գործողություն է, և թելն ինքնին ծանր առարկա է, թեկուզ այն պատճառով, որ այն հատկացնում է 1 ՄԲ հիշողություն ստեկի վրա և պահանջում է փոխազդեցություն OS API-ի հետ:

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

ThreadPool դասը ներկայացնում է լողավազանի հասկացությունը: .NET-ում thread pool-ը ինժեներական մի մասն է, և Microsoft-ի մշակողները մեծ ջանքեր են գործադրել, որպեսզի համոզվեն, որ այն օպտիմալ կերպով աշխատում է տարբեր սցենարների դեպքում:

Ընդհանուր հայեցակարգ.

Ծրագրի մեկնարկի պահից այն ստեղծում է մի քանի թելեր ռեզերվում ֆոնին և հնարավորություն է տալիս դրանք օգտագործելու: Եթե ​​թելերն օգտագործվում են հաճախակի և մեծ քանակությամբ, ապա լողավազանը ընդլայնվում է զանգահարողի կարիքները բավարարելու համար: Երբ ճիշտ ժամանակին լողավազանում ազատ թելեր չկան, այն կամ կսպասի, որ թելերից մեկը վերադառնա, կամ կստեղծի նորը: Սրանից հետևում է, որ թելերի լողավազանը հիանալի է որոշ կարճաժամկետ գործողությունների համար և վատ է հարմար այն գործողությունների համար, որոնք գործում են որպես ծառայություններ հավելվածի ողջ գործունեության ընթացքում:

Լողավազանից շարանը օգտագործելու համար կա QueueUserWorkItem մեթոդ, որն ընդունում է WaitCallback տիպի պատվիրակ, որն ունի նույն ստորագրությունը, ինչ ParametrizedThreadStart-ը, և դրան փոխանցված պարամետրը կատարում է նույն գործառույթը:

ThreadPool.QueueUserWorkItem(...);

Քիչ հայտնի thread pool մեթոդը RegisterWaitForSingleObject օգտագործվում է չարգելափակող IO գործողություններ կազմակերպելու համար: Այս մեթոդին փոխանցված պատվիրակը կկանչվի, երբ մեթոդին փոխանցված WaitHandle-ը «Ազատված է»:

ThreadPool.RegisterWaitForSingleObject(...)

.NET-ն ունի շղթայի ժամանակաչափ և այն տարբերվում է WinForms/WPF ժամանակաչափերից նրանով, որ դրա մշակիչը կկանչվի լողավազանից վերցված թեմայի վրա:

System.Threading.Timer

Կա նաև բավականին էկզոտիկ եղանակ՝ պատվիրակին կատարման համար լողավազանից թեմա ուղարկելու համար՝ BeginInvoke մեթոդը:

DelegateInstance.BeginInvoke

Ես կցանկանայի համառոտ անդրադառնալ այն ֆունկցիայի վրա, որին կարելի է անվանել վերը նշված մեթոդներից շատերը՝ CreateThread Kernel32.dll Win32 API-ից: Արտաքին մեթոդների մեխանիզմի շնորհիվ այս ֆունկցիան անվանելու միջոց կա։ Ես նման կոչ եմ տեսել միայն մեկ անգամ՝ ժառանգական կոդի սարսափելի օրինակում, և հենց դա արած հեղինակի մոտիվացիան դեռևս առեղծված է մնում ինձ համար։

Kernel32.dll CreateThread

Թեմաների դիտում և վրիպազերծում

Ձեր կողմից ստեղծված թեմաները, բոլոր երրորդ կողմի բաղադրիչները և .NET լողավազանը կարող են դիտվել Visual Studio-ի Threads պատուհանում: Այս պատուհանը կցուցադրի շղթայի մասին տեղեկատվությունը միայն այն ժամանակ, երբ հավելվածը վրիպազերծման և ընդմիջման ռեժիմում է: Այստեղ դուք կարող եք հեշտությամբ դիտել յուրաքանչյուր շղթայի կույտերի անվանումները և առաջնահերթությունները, ինչպես նաև վրիպազերծումը միացնել որոշակի շղթայի: Օգտագործելով Thread դասի Priority հատկությունը, դուք կարող եք սահմանել շարանի առաջնահերթությունը, որը OC-ն և CLR-ը կընկալեն որպես առաջարկություն պրոցեսորի ժամանակը թելերի միջև բաժանելիս:

.NET. Multithreading-ի և asynchrony-ի հետ աշխատելու գործիքներ: Մաս 1

Առաջադրանք զուգահեռ գրադարան

Առաջադրանքների զուգահեռ գրադարանը (TPL) ներդրվել է .NET 4.0-ում: Այժմ դա ստանդարտ և հիմնական գործիք է ասինխրոնիայի հետ աշխատելու համար: Ցանկացած ծածկագիր, որն օգտագործում է ավելի հին մոտեցում, համարվում է ժառանգություն: TPL-ի հիմնական միավորը System.Threading.Tasks անվանատարածքից Task դասն է: Առաջադրանքը աբստրակցիա է թելի վրա: C# լեզվի նոր տարբերակով մենք ստացանք Tasks-ի հետ աշխատելու էլեգանտ տարբերակ՝ async/wait օպերատորներ: Այս հասկացությունները հնարավորություն տվեցին գրել ասինխրոն կոդ այնպես, կարծես այն պարզ և համաժամանակյա լիներ, ինչը հնարավորություն տվեց նույնիսկ այն մարդկանց, ովքեր քիչ են հասկանում թելերի ներքին աշխատանքը, գրել հավելվածներ, որոնք օգտագործում են դրանք, հավելվածներ, որոնք չեն սառչում երկար գործողություններ կատարելիս: Async/wait-ի օգտագործումը մեկ կամ նույնիսկ մի քանի հոդվածի թեմա է, բայց ես կփորձեմ դրա էությունը ստանալ մի քանի նախադասությամբ.

  • async-ը Task կամ void վերադարձնող մեթոդի փոփոխիչ է
  • and await-ը չարգելափակող Task-ի սպասող օպերատոր է:

Եվս մեկ անգամ. սպասման օպերատորը, ընդհանուր դեպքում (կան բացառություններ), կթողարկի կատարման ընթացիկ շարանը հետագա, և երբ առաջադրանքը ավարտի իր կատարումը, և շարանը (իրականում, ավելի ճիշտ կլինի ասել համատեքստը. , բայց դրա մասին ավելի ուշ) կշարունակի մեթոդի հետագա գործարկումը: .NET-ի ներսում այս մեխանիզմն իրականացվում է այնպես, ինչպես եկամտաբերությունը, երբ գրված մեթոդը վերածվում է մի ամբողջ դասի, որը պետական ​​մեքենա է և կարող է իրականացվել առանձին մասերով՝ կախված այս վիճակներից։ Յուրաքանչյուր հետաքրքրված կարող է գրել ցանկացած պարզ կոդ՝ օգտագործելով asynс/await, հավաքել և դիտել ժողովը՝ օգտագործելով 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 վիճակի ստուգումներով: Եթե ​​ստուգումներ չկան, ապա CancellationTokenSource օբյեկտի վրա կանչված Cancel մեթոդը կկարողանա դադարեցնել առաջադրանքի կատարումը միայն այն սկսելուց առաջ:

Վերջին պարամետրը TaskScheduler տիպի ժամանակացույցի օբյեկտ է: Այս դասը և նրա սերունդները նախագծված են՝ վերահսկելու առաջադրանքները շղթաների միջև բաշխելու ռազմավարությունները; լռելյայնորեն, առաջադրանքը կկատարվի պատահական շղթայի վրա՝ լողավազանից:

Սպասման օպերատորը կիրառվում է ստեղծված Task-ի վրա, ինչը նշանակում է, որ դրանից հետո գրված կոդը, եթե այդպիսին կա, կկատարվի նույն համատեքստում (հաճախ դա նշանակում է նույն շղթայի վրա), ինչ կոդն է սպասել:

Մեթոդը նշված է որպես async void, ինչը նշանակում է, որ այն կարող է օգտագործել սպասման օպերատորը, սակայն զանգի կոդը չի կարողանա սպասել կատարմանը: Եթե ​​նման հատկանիշն անհրաժեշտ է, ապա մեթոդը պետք է վերադարձնի Task-ը: 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-ն օգտագործելիս շատ հեշտ է կարգավորել. դրանք վարվում են նույն կերպ, կարծես կոդը համաժամանակյա լիներ: Մինչդեռ, եթե մենք կիրառենք համաժամանակյա սպասողական էկզորցիզմ առաջադրանքի վրա, սկզբնական բացառությունը վերածվում է AggregateException-ի, այսինքն. Բացառությունը կարգավորելու համար դուք պետք է ուսումնասիրեք InnerException տեսակը և ինքներդ գրեք if շղթա մեկ catch բլոկի ներսում կամ օգտագործեք catch երբ կառուցում եք, C# աշխարհում ավելի ծանոթ catch բլոկների շղթայի փոխարեն:

Երրորդ և վերջին օրինակները նույնպես վատ են նշվում նույն պատճառով և պարունակում են նույն խնդիրները:

WhenAny և WhenAll մեթոդները չափազանց հարմար են առաջադրանքների խմբին սպասելու համար, դրանք փաթեթավորում են առաջադրանքների խումբը մեկի մեջ, որը կաշխատի կա՛մ խմբից առաջադրանքն առաջին անգամ գործարկելու դեպքում, կա՛մ երբ բոլորն ավարտեն իրենց կատարումը:

Թելերի դադարեցում

Տարբեր պատճառներով կարող է անհրաժեշտ լինել դադարեցնել հոսքը սկսվելուց հետո: Դա անելու մի շարք եղանակներ կան: Thread դասը ունի երկու համապատասխան անվանմամբ մեթոդ. Անջատել и ընդհատել. Առաջինը խորհուրդ չի տրվում օգտագործել, քանի որ ցանկացած պատահական պահի զանգահարելուց հետո, ցանկացած հրահանգի մշակման ժամանակ բացառություն կկատարվի ThreadAbortedException. Դուք չեք ակնկալում, որ նման բացառություն կստեղծվի ցանկացած ամբողջ թվով փոփոխական ավելացնելիս, այնպես չէ՞: Իսկ այս մեթոդը կիրառելիս սա շատ իրական իրավիճակ է։ Եթե ​​Ձեզ անհրաժեշտ է կանխել CLR-ն կոդի որոշակի հատվածում նման բացառություն ստեղծելուց, կարող եք այն փաթեթավորել զանգերի մեջ: Thread.BeginCriticalRegion, Thread.EndCriticalRegion. Վերջնական բլոկում գրված ցանկացած կոդ փաթաթված է նման զանգերի մեջ: Այդ իսկ պատճառով, շրջանակային կոդի խորքում դուք կարող եք գտնել բլոկներ դատարկ փորձով, բայց ոչ վերջապես դատարկ: Microsoft-ն այնքան է հուսահատեցնում այս մեթոդը, որ նրանք չեն ներառել այն .net core-ում:

Ընդհատման մեթոդն ավելի կանխատեսելի է աշխատում: Այն կարող է ընդհատել շարանը բացառությամբ ThreadInterruptedException միայն այն պահերին, երբ շարանը սպասողական վիճակում է։ Այն մտնում է այս վիճակում՝ կախված WaitHandle-ին սպասելիս, արգելափակելիս կամ Thread.Sleep-ին զանգահարելուց հետո:

Վերը նկարագրված երկու տարբերակներն էլ վատն են իրենց անկանխատեսելիության պատճառով: Լուծումը կառուցվածքի օգտագործումն է Չեղարկման նշան և դաս CancellationTokenSource. Բանն այն է, որ ստեղծվում է CancellationTokenSource դասի օրինակ, և միայն նա, ում պատկանում է այն, կարող է դադարեցնել գործողությունը՝ զանգահարելով մեթոդը: վերացնել. Միայն CancellationToken-ն է փոխանցվում բուն գործողությանը: CancellationToken-ի սեփականատերերը չեն կարող իրենք չեղարկել գործողությունը, այլ կարող են միայն ստուգել՝ արդյոք գործողությունը չեղարկվել է: Դրա համար կա բուլյան հատկություն Չեղարկում է պահանջվում և մեթոդ ThrowIfCancel Requested. Վերջինս բացառություն կգցի TaskCancelledException եթե Cancel մեթոդը կանչվել է CancellationToken օրինակի վրա, որը թութակ է ստացել: Եվ սա այն մեթոդն է, որը ես խորհուրդ եմ տալիս օգտագործել: Սա բարելավում է նախորդ տարբերակների համեմատ՝ ձեռք բերելով լիակատար վերահսկողություն, թե որ պահին կարող է ընդհատվել բացառության գործողությունը:

Թելը դադարեցնելու ամենադաժան տարբերակը Win32 API TerminateThread ֆունկցիան կանչելն է: Այս ֆունկցիան կանչելուց հետո CLR-ի պահվածքը կարող է անկանխատեսելի լինել: MSDN-ում այս ֆունկցիայի մասին գրված է հետևյալը. «TerminateThread-ը վտանգավոր գործառույթ է, որը պետք է օգտագործվի միայն ամենածայրահեղ դեպքերում: «

Հնացած API-ի վերափոխում առաջադրանքի վրա հիմնված՝ օգտագործելով FromAsync մեթոդը

Եթե ​​ձեզ բախտ է վիճակվել աշխատել մի նախագծի վրա, որը սկսվել է Tasks-ի ներդրումից հետո և դադարել է հանգիստ սարսափ պատճառել ծրագրավորողների մեծամասնության համար, ապա ստիպված չեք լինի գործ ունենալ շատ հին API-ների, ինչպես երրորդ կողմի, այնպես էլ ձեր թիմի հետ: նախկինում խոշտանգել է. Բարեբախտաբար, .NET Framework թիմը հոգ էր տանում մեր մասին, չնայած, երևի թե, նպատակը մեր մասին հոգ տանելն էր: Ինչ էլ որ լինի, .NET-ն ունի մի շարք գործիքներ՝ հին ասինխրոն ծրագրավորման մոտեցումներով գրված նորին առանց ցավի փոխակերպելու։ Դրանցից մեկը TaskFactory-ի FromAsync մեթոդն է։ Ստորև բերված կոդի օրինակում ես այս մեթոդի միջոցով փաթաթում եմ WebRequest դասի հին async մեթոդները Task-ում:

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. Գործառույթների, նպատակի և գործողության սկզբունքի առումով այն կարող է ինչ-որ չափով հիշեցնել ThreadPool դասի RegisterWaitForSingleObject մեթոդը, որի մասին ես գրել եմ վերևում։ Օգտագործելով այս դասը՝ դուք կարող եք հեշտությամբ և հարմար կերպով փաթաթել հին ասինխրոն API-ները Tasks-ում:

Դուք կասեք, որ ես արդեն խոսել եմ այս նպատակների համար նախատեսված TaskFactory դասի FromAsync մեթոդի մասին։ Այստեղ մենք ստիպված կլինենք հիշել .net-ում ասինխրոն մոդելների զարգացման ողջ պատմությունը, որն առաջարկել է Microsoft-ը վերջին 15 տարիների ընթացքում. մինչև առաջադրանքների վրա հիմնված ասինխրոն օրինակը (TAP) կար Asynchronous Programming Pattern (APP), որը: մեթոդների մասին էր ՍկսելDoSomething վերադառնում է IAsyncԱրդյունք և մեթոդներ վերջDoSomething, որն ընդունում է այն և այս տարիների ժառանգության համար FromAsync մեթոդը պարզապես կատարյալ է, բայց ժամանակի ընթացքում այն ​​փոխարինվեց իրադարձությունների վրա հիմնված ասինխրոն ձևով (EAP), որը ենթադրում էր, որ իրադարձությունը կբարձրացվի, երբ ասինխրոն գործողությունն ավարտվի:

TaskCompletionSource-ը կատարյալ է իրադարձության մոդելի շուրջ ստեղծված Tasks-ն ու ժառանգական 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 խորհուրդներ և հնարքներ

Հին 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 մեթոդը պետք է վերադարձնի արդեն ավարտված Task-ը: Եթե ​​սա ամենատարածված դեպքն է, ապա կարող եք մտածել ValueTask-ի օգտագործման մասին:

Երբ մենք ստանում ենք հաղորդագրության հարցում, մենք ստեղծում և տեղադրում ենք TaskCompletionSource բառարանում, այնուհետև սպասում ենք, թե ինչ կլինի առաջինը. նշված ժամանակային միջակայքը լրանում է կամ հաղորդագրություն է ստացվում:

ValueTask. ինչու և ինչպես

Async/wait օպերատորները, ինչպես yield return օպերատորը, մեթոդից ստեղծում են վիճակի մեքենա, և սա նոր օբյեկտի ստեղծումն է, որը գրեթե միշտ կարևոր չէ, բայց հազվադեպ դեպքերում կարող է խնդիր առաջացնել: Այս դեպքը կարող է լինել իսկապես հաճախակի կոչվող մեթոդ, մենք խոսում ենք վայրկյանում տասնյակ և հարյուր հազարավոր զանգերի մասին։ Եթե ​​նման մեթոդը գրված է այնպես, որ շատ դեպքերում այն ​​վերադարձնում է արդյունք՝ շրջանցելով սպասման բոլոր մեթոդները, ապա .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));
}

Եկեք մանրամասն նայենք կոդի այս հատվածին. եթե քեշում արժեք կա, մենք ստեղծում ենք կառուցվածք, հակառակ դեպքում իրական առաջադրանքը կփաթաթվի իմաստալից: Զանգող կոդը չի հետաքրքրում, թե որ ճանապարհով է այս կոդը կատարվել.

Task Schedulers. առաջադրանքների մեկնարկի ռազմավարությունների կառավարում

Հաջորդ API-ն, որը ես կցանկանայի դիտարկել, դասն է Առաջադրանքների ժամանակացույց և դրա ածանցյալները: Ես արդեն նշեցի վերևում, որ TPL-ն հնարավորություն ունի կառավարելու Tasks-ը թեմաներով բաշխելու ռազմավարությունները: Նման ռազմավարությունները սահմանված են TaskScheduler դասի հետնորդներում: Գրեթե ցանկացած ռազմավարություն, որը ձեզ կարող է անհրաժեշտ լինել, կարելի է գտնել գրադարանում: ParallelExtensionsExtras, մշակված Microsoft-ի կողմից, բայց ոչ .NET-ի մաս, այլ մատակարարվում է որպես Nuget փաթեթ: Համառոտ նայենք դրանցից մի քանիսին.

  • CurrentThreadTaskScheduler — կատարում է առաջադրանքներ ընթացիկ շղթայի վրա
  • LimitedConcurrencyLevelTaskScheduler - սահմանափակում է միաժամանակ կատարվող առաջադրանքների քանակը N պարամետրով, որն ընդունված է կոնստրուկտորում
  • OrderedTaskScheduler — սահմանվում է որպես LimitedConcurrencyLevelTaskScheduler(1), ուստի առաջադրանքները կկատարվեն հաջորդաբար:
  • WorkStealingTaskScheduler - իրականացնում է աշխատանք-գողություն առաջադրանքների բաշխման մոտեցում: Ըստ էության, դա առանձին ThreadPool է: Լուծում է այն խնդիրը, որ .NET ThreadPool-ում ստատիկ դաս է, մեկը բոլոր հավելվածների համար, ինչը նշանակում է, որ ծրագրի մի մասում դրա ծանրաբեռնվածությունը կամ սխալ օգտագործումը կարող է հանգեցնել կողմնակի ազդեցությունների մյուս մասում: Ավելին, չափազանց դժվար է հասկանալ նման թերությունների պատճառը։ Դա. Ծրագրի այն մասերում, որտեղ ThreadPool-ի օգտագործումը կարող է լինել ագրեսիվ և անկանխատեսելի, կարող է անհրաժեշտություն առաջանալ օգտագործել առանձին WorkStealingTaskSchedulers:
  • QueuedTaskScheduler — թույլ է տալիս կատարել առաջադրանքներ ըստ առաջնահերթ հերթի կանոնների
  • ThreadPerTaskScheduler — ստեղծում է առանձին թեմա յուրաքանչյուր առաջադրանքի համար, որը կատարվում է դրա վրա: Կարող է օգտակար լինել առաջադրանքների համար, որոնք անկանխատեսելիորեն երկար ժամանակ են պահանջում:

Լավ մանրամասն կա հոդված TaskScheduler-ի մասին microsoft բլոգում:

Tasks-ի հետ կապված ամեն ինչի հարմար կարգաբերման համար Visual Studio-ն ունի Tasks պատուհան: Այս պատուհանում դուք կարող եք տեսնել առաջադրանքի ընթացիկ վիճակը և անցնել ընթացիկ կոդի գործող տողին:

.NET. Multithreading-ի և asynchrony-ի հետ աշխատելու գործիքներ: Մաս 1

PLinq և Parallel դասը

Բացի Tasks-ից և դրանց մասին ասված ամեն ինչից, .NET-ում կա ևս երկու հետաքրքիր գործիք՝ PLinq (Linq2Parallel) և Parallel դասը։ Առաջինը խոստանում է Linq-ի բոլոր գործողությունների զուգահեռ իրականացում բազմաթիվ թելերի վրա: Թելերի քանակը կարելի է կարգավորել WithDegreeOfParallelism ընդլայնման մեթոդի միջոցով: Ցավոք, ամենից հաճախ PLinq-ն իր լռելյայն ռեժիմում չունի բավարար տեղեկատվություն ձեր տվյալների աղբյուրի ներքին մասերի մասին, որպեսզի ապահովի զգալի արագություն, մյուս կողմից՝ փորձելու արժեքը շատ ցածր է. պարզապես անհրաժեշտ է նախապես զանգահարել AsParallel մեթոդը: Linq մեթոդների շղթան և կատարման թեստերը: Ավելին, հնարավոր է PLinq-ին լրացուցիչ տեղեկատվություն փոխանցել ձեր տվյալների աղբյուրի բնույթի մասին՝ օգտագործելով Partitions մեխանիզմը: Դուք կարող եք կարդալ ավելին այստեղ и այստեղ.

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 կառուցվածքը կարող է օգտակար լինել թեժ ուղիների և հիշողության տրաֆիկի օպտիմալացման համար
  • Visual Studio-ի Tasks և Threads պատուհանները տրամադրում են շատ տեղեկություններ, որոնք օգտակար են բազմաշերտ կամ ասինխրոն կոդի վրիպազերծման համար:
  • PLinq-ը հիանալի գործիք է, բայց այն կարող է բավարար տեղեկատվություն չունենալ ձեր տվյալների աղբյուրի մասին, բայց դա կարող է շտկվել բաժանման մեխանիզմի միջոցով:
  • Շարունակելի…

Source: www.habr.com

Добавить комментарий