Professional Documents
Culture Documents
Implementacioni Idiomi Asinhronog, Paralelnog I Višenitnog Programiranja U C#.NET-u
Implementacioni Idiomi Asinhronog, Paralelnog I Višenitnog Programiranja U C#.NET-u
Ментор: Кандидат:
др Саша Д. Лазаревић Ирена Савковић 2018/3713
_________________________________________
_________________________________________
________________________________________
Радна биографија кандидата
Ирена С. Савковић
Ужице, Република Србија
организованост
ЛИЧНЕ
одговорност
КАРАКТЕРИСТИКЕ
посвећеност
иницијативност
Август 2018
РАДНО ИСКУСТВО Infosys д.о.о. – Ужице, Република Србија
–
Програмер у тиму за развој на новим софтверским
технологијама
(стални радни однос)
Full-stack програмер у трочланом тиму за развој POS
система (.NET Core технологије – Entity Framework
Core, WPF Core, ASP.NET Core API, ASP.NET Core
MVC).
Фебруар 2018
Air Serbia а.д. – Београд, Република Србија
–
Практикант у IT одељењу за развој апликација Мај 2018
(понуда за запослење)
Учествовање у прикупљању и дефинисању
корисничких захтева, у креирању нових програма
учитавања, обраде података и креирању извештаја
(.NET платформа)
Октобар 2014
ОБРАЗОВАЊЕ Факултет организационих наука – Београд,
–
Република Србија
Јул 2018
Информациони системи и технологије – основне
студије (9,23)
2010 – 2014
Ужичка гимназија – Ужице, Република Србија
Потписивањем изјављујем:
Да је рад искључиво резултат мог сопственог истраживачког рада;
Да сам рад и мишљења других аутора које сам користио у овом раду назначио
или цитирао у складу са Упутством;
Да су сви радови и мишљења других аутора наведени у списку
литературе/референци који су саставни део овог рада и писани су у складу са
Упутством;
Да сам довио све дозволе за коришћење ауторског дела који се у
потпуносзи/целости уносе у предати рад и да сам то јасно навео;
Да сам свестан да је плагијат коришћење туђих радова у било ком облику (као
цитата, парафраза, слика, табела, дијаграма, дизајна, планова, фотографија,
филма, музике, формула, веб сајтова, компјутерских програма и сл.) без
навођења аутора или представљање туђих ауторских дела као мојих, кажњиво
по закону (Закон о ауторским и сродним правима, Службени гласник Републике
Србије, бр. 104/2009, 99/2011, 119/2012), као и других закона и одговарајућих
аката Универзитета у Београду и Факултета организационих наука;
Да сам свестан да плагијат укључује и представљање, употребу и
дустрибуирање рада предавача или других студената као сопствених;
Да сам свестан последица које код доказаног плагијата могу проузроковати на
предати завшни мастер рад и мој статус;
Да је електронска верзија завршног рада идентична штампаном примерку и
пристајем на његово објављивање под условима прописаним актима
Универзитета и Факултета.
Београд, __________
Потпис студента ______________
РЕЗИМЕ
Имплементациони узори или имплементациони идиоми су програмски узори налик дизајн
узорима, али на нижем нивоу апстракције. Описују примену уочене понављајуће конструкције
у конкретној технологији. У овом раду описан је значај примене имплементационих идиома и
представљена њихова општа структура. Обрађена је област конкурентног програмирања у
C#.NET-у и указано на потребу за применом конкурентности у савременом развоју софтвера.
Детаљно су обрађене основне технике конкурентног програмирања – асинхроно (са акцентом
на актуелном TAP асинхроном узору), паралелно и вишенитно програмирање у ужем смислу.
Објашњене су кључне разлике између њих, проблематика коју решавају, али и контраефекти
њихове неадекватне примене. У оквиру сваке технике конкурентности, пратећи општу
структуру узора, обрађени су имплементациони идиоми који су најчешће у употреби. Сваки
опис проблема, прате општа идеја решења, примери конкретног програмског решења и
коментар о условима, ограничењима и последицама примене конкретног имплементационог
идиома.
Кључне речи
имплементациони идиоми, конкурентно програмирање, асинхроно програмирање, TAP, Task,
паралелно програмирање, паралелизам, вишенитно програмирање.
ABSTRACT
Implementation patterns or implementation idioms are programming patterns alike design patterns,
but on lower abstraction level. They describe the application of repetitive constructions in a specific
technology. This paper describes the importance of implementation idioms usage and presents their
general structure. It elaborates the field of concurrency in C#.NET and indicates the need for its
application in modern software development. Moreover, it thoroughly specifies the basics of
concurrency techniques – asynchronous programming (with TAP asynchronous pattern highlighted),
parallel programming and multithreading. Additionally, this paper explains their differences and
issues they solve as well as the opposite effects of their inadequate utilization. For each of described
concurrency techniques, there are most common implementation idioms that follow
implementation idiom general structure. Each issue description is followed by general solution idea,
examples of solution code and comments containing conditions, limitations and consequences of
usage of the specific implementation idiom.
Keywords
implementation idioms, concurrency, asynchronous programming, TAP, Task, parallel programming,
parallelism, multithreading
Листа табела
Табела 1 Каректеристике типова повратних вредности синхроног и асинхроног програмирања у
C#.NET-у..............................................................................................................................................37
Садржај
1 Увод у програмске узоре.............................................................................................................1
1.1 Општа идеја програмског узора..........................................................................................1
1.2 Општа структура програмског узора...................................................................................1
1.3 Имплементациони узори....................................................................................................1
1.4 Исечци кôда..........................................................................................................................2
2 Конкурентност..............................................................................................................................3
2.1 Процесорско распоређивање нити....................................................................................3
2.2 Технике конкурентног програмирања................................................................................4
2.2.1 Асинхроно програмирање...........................................................................................6
2.2.1.1 Асинхрони узор заснован на Task-овима (Task-based Asynchronous Pattern –
TAP) 6
2.2.1.2 Асинхрони узор заснован на догађајима (Event-based Asynchronous Pattern –
EAP) 10
2.2.1.3 Модел асинхроног програмирања (Asynchronous Programming Model – APM) 10
2.2.2 Паралелно програмирање........................................................................................11
2.2.3 Вишенитно програмирање........................................................................................12
3 Имплементациони идиоми конкурентног програмирања.....................................................13
3.1 Имплементациони идиоми асинхроног програмирања.................................................13
ИИ-1 Избегавање оригиналног контекста при наставку...............................................14
ИИ-2 Паузирање одређени период времена................................................................15
ИИ-3 Чекање завршетка групе Task-ова.........................................................................17
ИИ-4 Чекање завршетка било ког Task-а из групе.........................................................19
ИИ-5 Обрада сваког Task-а из групе у редоследу извршавања...................................20
ИИ-6 Обрада грешака насталих у async Task методама................................................22
ИИ-7 Обрада грешака насталих у async void методама................................................24
ИИ-8 Креирање ValueTask-а............................................................................................26
ИИ-9 Употреба ValueTask-а.............................................................................................28
ИИ-10 Враћање завршених Task-ова................................................................................30
ИИ-11 Извештавање о напретку извршења.....................................................................32
ИИ-12 Покретање Task-ова у одређеном редоследу......................................................33
ИИ-13 Започињање дуготрајног Task-а............................................................................35
3.1.1 Асинхрони токови......................................................................................................36
ИИ-14 Креирање асинхроних токова...............................................................................38
ИИ-15 Употреба асинхроних токова.................................................................................40
ИИ-16 Употреба LINQ са асинхроним токовима..............................................................41
ИИ-17 Прекид асинхроних токова....................................................................................43
3.1.2 Прекид извршавања..................................................................................................45
ИИ-18 Изазивање прекида извршавања.........................................................................46
ИИ-19 Реаговање на захтев прекида периодичном провером......................................48
ИИ-20 Прекид услед истека времена...............................................................................49
ИИ-21 Прекид асинхроног извршавања..........................................................................50
3.1.3 Модуларно тестирање асинхроног извршавања.....................................................51
ИИ-22 Модуларно тестирање async Task метода............................................................53
ИИ-23 Модуларно тестирање async Task метода од чијег се извршења очекује неуспех
55
ИИ-24 Модуларно тестирање async void метода............................................................57
3.2 Имплементациони идиоми паралелног програмирања.................................................59
ИИ-25 Паралелна обрада података..................................................................................60
ИИ-26 Агрегација резултата паралелне обраде података..............................................62
ИИ-27 Паралелни LINQ......................................................................................................63
ИИ-28 Паралелно позивање већег броја метода............................................................64
ИИ-29 Прекид паралелног извршавања..........................................................................65
ИИ-30 Обрада грешака при паралелном извршавању...................................................66
3.3 Имплементациони идиоми вишенитног програмирања................................................67
ИИ-31 Синхронизација нити коришћењем типа Monitor...............................................69
ИИ-32 Синхронизација нити коришћењем кључне речи lock........................................71
ИИ-33 Означавање поља кључном речју volatile............................................................73
ИИ-34 Синхронизација нити коришћењем типа Interlocked..........................................74
ИИ-35 Употреба Timer-а....................................................................................................75
4 Закључак.....................................................................................................................................77
5 Литература.................................................................................................................................78
1 Увод у програмске узоре
Концепт узора није уско везан за област софтверског инжењерства. Он има темеље у
различитим дисциплинама изградње сложених система, каква је нпр. архитектура. Тек касније,
узори су нашли своје место и у области програмирања.
1. Назива узора – ефектног кратког назива који обухвата проблем, решење и последице и
на конкретан начин дефинише узор. Важно је да назив узора буде препознатљив ради
лакше комуникације;
2. Дефиниције проблема – опис проблематике, контекста у ком се јавља и, често, услова
који морају бити испуњени да би се узор могао применити;
3. Решења проблема – апстрактни опис решења проблема, без конкретне
имплементације, који приказује како се решење може применити у различитим
ситуацијама и контекстима;
4. Коментара – значајни за евалуирање примене узора. Описују утицај на систем у
погледу флексибилности, модуларности, надградивости итд. Омогућава анализу
предности и недостатака примене узора.
1
Због уске везаности за програмски језик, имплементациони идиоми нису разумљиви
програмерима других програмских језика и парадигми, иако је истина да су поједини
имплементациони идиоми прилично слични у различитим програмским језицима. Идиом се у
природним језицима дефинише као “коришћење и означавање израза који су природни за
изворног говорника”. Идиом у програмским језицима би се могао дефинисати аналогно, тако
да су изрази природни програмеру конкретног програмског језика.
2
2 Конкурентност
Конкурентно програмирање подразумева извршавање више програмских токова, односно
нити, истовремено. Програмска нит (thread) представља појединачни секвенцијални
контролни ток, јединствен низ инструкција које се извршавају. Велики број програма никада
неће захтевати више од једне нити (single-threaded programs). Међутим, далеко је већи број
оних других (multithreaded programs) којима је примена конкурентности круцијална за
успешност. Употреба програмских нити омогућава програму да буде респонзиван и ефикасан.
Савремени рачунарски системи имају већу способност обраде него икада, тако да је само на
програмерима да умање латентност везану за процесорску активност (processor-bound latency)
– да дељењем процесорски захтевних послова на вишеструке процесоре смање трајање
обраде. Такође, латентност везана за улазно-излазне активности (I/O-bound latency) треба да
буде минимизирана колико год је могуће, јер некада може да буде једнака извршењу
милиона инструкцијских циклуса, као када се нпр. преузимају подаци са удаљеног веб
сервера.
Конкурентност је, ипак, кључ лепо имплементираног софтвера. У прошлости је била много
захтевнија и тежа за писање, одржавање и отклањање грешака. Неретко су је програмери
избегавали. Данас је она доста прилагођенија широкој стручној заједници и у великој мери
олакшана за примену. У сваком случају, конкурентни кôд је императив савременог софтвера.
Ипак, покретање одређеног задатка на посебној нити треба да буде паметно процењено.
Пожељно је за релативно дуге задатке, а ствара контраефекат за велики број ситних задатака.
У случају бројних ситних послова, планирање и распоређивање нити надмашује трошак
једноставног покретања задатака синхроно.
3
2.1 Процесорско распоређивање нити
Процесор или централна процесорска јединица (central processing unit – CPU) је рачунарска
компонента која, заправо, извршава конкретан програм. Сваки рачунар има бар један
процесор, а неки од савремених и више њих. Већина савремених процесора је вишејезгарна,
при чему је свако језгро виртуелна процесорска јединица. Из тог разлога се процесорско језгро
повремено назива процесором.
Када дође време да се у процесору распореди нит, Windows мора да уради замену контекста.
Током замене контекста (context switch), Windows чува стање тренутне нити у интерни објекат
нити у оперативном систему, узима другу нит из колекције спремних нити и пратеће логике о
њиховом извршавању (threadpool-а програмских нити) и прослеђује процесору информацију о
стању нити из интерног објекта нити. Затим почиње да је извршава. Уколико се распоређује
нит са другог процеса, утрошак је још израженији јер се адресни простор празни.
Нит се онда извршава током једног квантума нити. Квантум (quantum) представља временски
период током ког процесор извршава одређену програмску нит. То је вишеструка вредност
clock interval-а (clock interval износи око 15 милисекунди на савременим вишепроцесорским
системима). Када се кôд враћа са врха стека, улази у стање чекања или је квантум времена
истекао, распоређивач узима другу спремну нит. То може бити иста или друга нит, у
зависности од такмичења за процесоре. Нит може ући у стање чекања ако се заблокира
чекајући на I/O или намерно позивајући Thread.Sleep. Windows Server има већи квантум нити
него десктоп верзија Windows-а. То значи да се нити извршавају дужи временски период пре
замене контекста.
Уколико се програм састоји од задатака дужих од квантума извршавања нити, онда је употреба
нити оправдана и прихватљива. Међутим, уколико кôд садржи више задатака за које се
очекује да вероватно кратко трају, тј. трају краће од квантума нити, коришћење нити је
неефикасно, зато што ће програм потрошити значајно више времена на замену контекста, него
што би потрошио синхроним извршавањем.
Вишенитно програмирање у ужем смислу је техника конкурентности која користи више нити
извршавања програма. Означава, дословно, више нити. Вишенитно програмирање свакако
није превазиђено, али старомодне технике креирања нити на нижем нивоу свакако јесу и није
им место у савременим апликацијама. Данас постоје много моћније и ефикасније технике
вишенитног програмирања на вишем нивоу апстракције. Директно креирање Thread-а је
превазиђена техника. Са друге стране, вишенитно програмирање живи кроз threadpool, који
сам распоређује задатке и прилагођава се захтевима. Такође, threadpool омогућава другу
технику конкурентног програмирања – паралелно програмирање.
4
Паралелно програмирање је форма вишенитног програмирања и представља обављање
велике количине посла његовим дељењем на више нити које се истовремено, конкурентно
извршавају. Паралелно процесирање користи вишенитност да максимизира употребу
процесорских језгара. Ако већ постоји потреба да се велики посао обави, корисно је да сва
језгра уместо само једног буду ангажована у тој активности.
5
2.2.1 Асинхроно програмирање
Асинхроно програмирање је кључна техника која обезбеђује респонзивност програма са
графичким корисничким интерфејсом (захтевање података са мреже, приступање бази
података или читање/писање у систему датотека) и скалабилност серверских апликација
(способност одговора на већи број захтева). О томе је било говора у уводном поглављу 2
Конкурентност. Савремене апликације значајно много користе рад са системом датотека и
мрежом. Све те активности подразумевано при свом одвијању блокирају графички интерфејс,
мада је све више оних које подржавају асинхроно извршавање.
Сваки од њих ће бити у некој мери обрађен у наставку. Савремени узор асинхроног
програмирања је TAP. У његов развој се активно улаже, док су преостала два застарела и не
препоручују се за примену у савременом развоју софтвера. Самим тим разумевању
асинхроног узора заснованог на Task-овима и обради његових карактеристика биће посвећено
највише пажње.
Како је креирање Thread-а релативно “скупа” операција, јер захтева повлачење велике
количине меморије, значајно је ефикасније препустити то задужење threadpool-у. Он је
намењен да обавља посао распоређивања нити, додељивања асинхроних активности
одређеној нити, извршавања активности, а онда и да испланира поновну употребу постојеће
нити за неку наредну активност, уместо ручног уништавања нити и креирања нове које бисмо
ми извели.
6
Task је објекат који енкапсулира кôд намењен за асинхроно извршавање. Личи на делегат, али
је суштинска разлика та да је делегат синхрони, а Task асинхрони. Када се извршава делегат
написан као ламбда израз, контрола се у позивајући кôд пребацује тек када делегат буде
извршен. Са друге стране, када се иницира Task, контрола се готово одмах враћа на кôд који је
покренуо асинхроно извршавање, док за то време асинхрони посао траје.
Кључне речи које се користе су async и await. Додавањем async у декларацију методе
омогућава се функционалност await команди у оквиру методе. Такође, сигнализира се
компајлеру да генерише state machine за ту методу, којим се прати асинхроно извршавање и
наставља ток извршавања када позадинска активност буде комплетно извршена, слично
функционисању yield return-а. Додавањем await команде врши се повраћај контроле
позиваоцу асинхроне методе. Од тренутка иницирања асинхроног извршавања графички
интерфејс је респонзиван, иако се позвана активност истовремено одвија.
7
2.2.1.1.4 Начин функционисања асинхроне методе
Асинхрона метода се извршава као скуп синхроних сегмената. Ти сегменти подељени су await
командама. Асинхрона метода почиње да се извршава синхроно, баш као и свака синхрона
метода, све до прве await команде.
Првом await командом започиње асинхроно await чекање извршења аргумента и ако
операција из аргумента није завршена враћа се недовршен Task. Кад се у неком тренутку у
будућности операција буде завршила, асинхрона метода ће наставити да се извршава.
Први сегмент асинхроне методе се свакако извршава на нити која је методу позвала, а на
остале сегменте се може утицати. Када се догоди први await у оквиру асинхроне методе,
бележи се стање контекста. Метода, подразумевано, наставља да се извршава у оквиру тог
контекста. Ако се то догађа на нити интерфејса, онда је у питању UI контекст. У већини других
случајева то је threadpool контекст.
8
се Task извршава на другој нити, његов статус може бити промењен у било ком тренутку за
време или након читања вредности атрибута статуса. Такође, одмах након позива Task.Run
никада не можемо знати тачно стање Task-а. Оно зависи од комбинације оперативног система,
његове оптерећености и других фактора. Може се догодити да одмах буде распоређен на
извршавање, али и да, са друге стране, буде одложен чекајући на расположивост додатних
ресурса. Може се догодити и да до тренутка повратка контроле на позивајући кôд, асинхрони
задатак буде већ завршен.
До застоја ће доћи ако се таква синхрона метода позива са нпр. UI контекста, јер он подржава
само једну нит у једном тренутку. Застој се дешава зато што синхрона метода асинхрону
методу не чека асинхроно, већ синхроно. Синхроно чекање блокира UI нит, тако да повратак са
извршења асинхроне методе на изворни контекст није могуће, јер је он већ заузет синхроним
чекањем. Ово се може избећи или претварањем синхроне методе у асинхрону или
извршавањем асинхроне методе на другом контексту.
9
2.2.1.2 Асинхрони узор заснован на догађајима (Event-based Asynchronous Pattern – EAP)
Овај узор је представљен у оквиру .NET Framework 2.0. Не препоручује се за развој савременог
софтвера, јер за њега више није обезбеђена подршка. Према њему, дефинишу се парови
методе и пратећег догађаја. Метода се обично завршава суфиксом Async, а повратна вредност
јој је void, по чему се најлакше разликује од TAP метода које је могуће асинхроно сачекати.
Евентуално, иницира догађај са суфиксом Completed. Параметри тог догађаја би требало да
потичу од AsyncCompletedEventArgs.
Ипак, постоји и начин да се асинхроно сачека извршење постојеће EAP методе. Коришћењем
TaskCompletionSource<T> може се окружити EAP изведба и прилагодити TAP узору асинхроног
извршавања.
APM узор омогућава позивајућем кôду да га извршава или као синхрони или као асинхрони.
Након позива Begin методе, програм може да настави извршавање на позивајућој нити, док се
асинхрона операција извршава на другој нити. За сваки позив Begin методе, програм треба да
позове и End методу како би преузео резултате извршавања.
Као и код EAP узора, постоји могућност асинхроног чекања постојећих некомпатибилних
метода. APM се у TAP најчешће може конвертовати коришћењем Task.Factory.FromAsync. У
случајевима када то није могуће, може се употребити TaskCompletionSource<T>.
10
2.2.2 Паралелно програмирање
Савремени рачунари, углавном, поседују већи број процесорских језгара што омогућава
извршавање више програмских нити истовремено. Када је већ доступна оваква предност,
штета је програмирати извршавање на појединачној програмској нити. Уместо тога, уводи се
паралелизација кôда, како би се посао распоредио на више процесора.
Са појавом .NET Framework 4, постао је доступан Task Parallel Library (TPL) сет алата за примену
конкурентности кроз паралелизацију кôда. Ова библиотека динамички прилагођава степен
конкурентности, како би најефикасније искористила могућности расположивих процесора.
Бави се поделом посла у сегменте, распоређивањем нити, обезбеђивањем могућности
прекида извршавања, управљањем стањем и другим аспектима конкурентности на нижем
нивоу апстракције.
Међутим, није сваки кôд подобан паралелизацији. При паралелизацији кôда, важно је бити
сигуран да бенефити паралелизма неће бити изгубљени. То значи да посао који се обавља у
оквиру једне итерације мора бити значајно већи од трајања временског периода који се улаже
у синхронизацију дељене вредности. Други трошак до ког може да дође је цена извршавања
делегата у свакој итерацији. Оба проблема могу бити решена коришћењем класе
System.Collections.Concurrent.Partitioner . Она врши трансформацију распона у колекцију
Tuple-ова који дефинишу распон над којим се у оквиру оригиналне колекције итерира.
11
2.2.3 Вишенитно програмирање
Вишенитно програмирање у ужем смислу је конкурентно програмирање на нижем нивоу
апстракције. Како би тема била јаснија, најпре је потребно разјаснити појмове процеса и нити.
Процес је један програм у извршавању, док је нит основна јединица којој оперативни систем
додељује процесорско време. Свака нит има приоритет извршавања и одржава сет података,
тзв. контекст нити (thread context), које систем користи при смени нити, када се једна нит
паузира, а друга наставља са извршавањем – регистре и стек. Све нити једног процеса деле
исти виртуелни адресни простор, што значи да нит може да изврши било који део к ôда,
укључујући и делове тренутно коришћене од стране друге нити.
Застој је ситуација када обе нити покушавају да заузму ресурс који је друга нит већ закључала.
Самим тим оне не могу ни да напредују, јер су у константном стању чекања на ослобађање оне
друге. Такво стање резултује потпуним застојем извршења. Многе методе вишенитног
програмирања омогућавају истек времена како би се детектовао застој.
Трка за приступ ресурсу је недостатак који долази до изражаја када излаз извршавања зависи
од тога која је од више нити прва стигла до дељеног блока. Такво понашање је последица
произвољног тренутка у ком се врши замена контекста у оперативном систему. Покретањем
програма већи број пута, долази се до различитог, потпуно непредвидивог резултата.
12
3 Имплементациони идиоми конкурентног програмирања
Имплементациони идиоми ће у наставку бити обрађени по сегментима у оквиру којих се
примењују. Сваки идиом ће пратити уобичајену структуру узора описану у поглављу 1.2 Општа
структура програмског узора, а то су четири елемента – конкретан и препознатљив назив
идиома, дефиниција проблема са условима који морају бити испуњени, решење проблема и
коментар о последицама примене идиома.
13
ИИ-1 Избегавање оригиналног контекста при наставку
Проблем
Решење
Коментар
14
ИИ-2 Паузирање одређени период времена
Проблем
Потребно је асинхроно сачекати одређени период времена. Ово је честа потреба при
модуларном тестирању, поновним покушајима, и једноставном истицању времена.
Решење
Тип Task садржи статичку методу Delay која враћа Task који се доврши након одређеног
времена. Овај пример приказује Task који ће се извршити асинхроно након три секунде
асинхроног чекања.
await Task.Delay(sledeceCekanje);
Task.Delay се користи и као стратегија једноставног истека времена. Иако је препоручени
начин имплементације истека времена CancellationTokenSource, слично је могуће урадити и
на следећи начин. Креирају се два Task-а. Један који асинхроно извршава жељену операцију, а
други који асинхроно чека бесконачан период времена. Њему се проследи CancellationToken
којим је дефинисано колико дуго ће се чекати на одзив. У следећем примеру то су три секунде.
Чека се на завршетак бар једног од та два Task -а. Уколико се у оквиру три секунде не преузме
одговор, десиће се истек времена. Међутим, разлика у односу на прави механизам истека
времена који омогућава CancellationTokenSource је да ће резултат у овом случају свакако
15
бити преузет и након истека, јер операција није отказана. Како би се операција заиста и
прекинула, потребно је да се CancellationToken проследи директно у методу преузимања
одговора. Када то није могуће, овакво решење је сасвим задовољавајуће.
if (completedTask == timeoutTask)
{
return null;
}
Коментар
16
ИИ-3 Чекање завршетка групе Task-ова
Проблем
Решење
Статичка метода WhenAll класе Task омогућава чекање завршетка групе Task-ова.
Функционише тако што враћа Task који се завршава када се заврши сваки од Task-ова из
прослеђене групе.
Коментар
Приликом извршавања групе Task-ова може се догодити да се бар у оквиру једног од њих
догоди грешка. Када се извршење Task-а асинхроно чека, грешка ће бити регуларно смештена
на Task који се враћа.
17
Када се чека извршење групе, уколико се асинхроно чека Task.WhenAll са бар једном грешком,
биће бачена само прва ухваћена грешка.
try
{
await Task.WhenAll(task1, task2);
Међутим, уколико нас занимају све грешке које су се догодиле на групи која се извршавала,
оне су смештене у Task-у који враћа Task.WhenAll у атрибуту Exception.
try
{
await allTasks;
}
18
ИИ-4 Чекање завршетка било ког Task-а из групе
Проблем
Некада је потребно да се сачека извршење било ког од Task-ова из прослеђене групе. Овај
проблем се најчешће јавља када се врши више независних операција истовремено и битно
нам је да одреагујемо чим се заврши прва асинхрона операција, која год она била.
Решење
Статичка метода WhenAny класе Task прима као улазне параметре групу Task-ова, а враћа Task
који се завршава када се заврши бар један од прослеђених Task-ова из групе. Необично је то
да је резултат враћеног (“спољашњег”) Task-а, заправо (“унутрашњи”) Task који се први
извршио.
Коментар
Такозвани “спољашњи” Task који враћа метода WhenAny увек ће се завршити успешно. Чак и
када “унутрашњи” Task има грешку она неће бити прослеђена на “спољашњи” Task. Да бисмо
се уверили да није било грешака на Task-у који се први извршио, потребно је да га асинхроно
сачекамо.
Када се изврши неки од Task-ова из прослеђене групе, треба да одлучимо шта ће се догодити
са онима који се још нису извршили. Уколико их ни не чекамо нити отказујемо, они остају
напуштени. То значи да ће њихови резултати, као и могуће грешке које су настале током
њиховог извршења, бити игнорисани. Некада постоји потреба да се ти остали Task-ови након
извршења првог откажу, како не би непотребно трошили ресурсе. Није препоручљиво да се у
оквиру групе Task-ова смести и Task.Delay који би дефинисао истек времена, јер не би у
суштини прекинуо извршење. Боље решење је коришћење Cancellation стратегије.
19
ИИ-5 Обрада сваког Task-а из групе у редоследу извршавања
Проблем
Потребно је да се за сваки Task из групе изврши одређена обрада одмах након што Task буде
завршен. Уколико се извршава група Task-ова, важно је да се не обрађују редоследом којим су
посложени у групи, већ редоследом којим се завршавају.
Решење
20
async Task ObradiRedosledomZavrsetka()
{
Task<int> taskA = OdloziIVratiVrednost(2);
Task<int> taskB = OdloziIVratiVrednost(3);
Task<int> taskC = OdloziIVratiVrednost(1);
Коментар
Разлика у односу на почетни кôд је што се обрада Task-ова врши конкурентно, а не обрађује се
један у једном тренутку. На тај начин је постигнуто да се Task обради одмах након што се
заврши, без обзира на свој редослед у групи која се извршава.
21
ИИ-6 Обрада грешака насталих у async Task методама
Проблем
Потребно је обезбедити обраду грешака које се догоде при извршавању асинхроних метода.
Решење
Када се у оквиру async Task методе догоди грешка, она се смешта на Task који је повратна
вредност те методе.
Грешку је могуће ухватити и обрадити помоћу try-catch блока, као када се обрађују грешке
синхроног кôда.
try
{
await task;
Коментар
Најчешће је потребно да кôд који позива извршавање асинхроног кôда добије повратну
информацију о насталим грешкама. Када се асинхроно сачека неуспешни Task, прва ухваћена
грешка ће бити поново бачена. Притом ће оригинални stack trace бити правилно задржан и
остати погодан за тумачење.
22
Уколико повратни Task садржи више грешака, а битно је да све буду обрађене, као што је
случај код извршавања Task.WhenAll групе Task-ова, користан је имплементациони идиом
ИИ-3 Чекање завршетка групе Task-ова.
23
ИИ-7 Обрада грешака насталих у async void методама
Проблем
За разлику од async Task метода, async void методе немају као повратну вредност Task на
ком би биле смештене настале грешке. Међутим, потребно је да грешке, ипак, буду обрађене.
Решење
Не постоји добро решење за такву ситуацију. Ако је икако могуће, пожељно је да се async void
промени у async Task. Некада то није могуће, као у случају имплементације ICommand која
мора да врати void. Препорука је да се тада креира асинхрона верзија Execute методе која
враћа Task.
Постоји и други начин за решавање овог проблема. Када async void метода баци грешку, та
грешка је онда бачена у оквиру SynchronizationContext-а који је био активан када је почело
извршавање async void методе. Уколико извршно окружење омогућава
SynchronizationContext, онда оно често омогућава и обраду грешака глобално, на вишем
нивоу. Нпр. WPF пружа Application.DispatcherUnhandledException. Постоје одређене
24
библиотеке које омогућавају управљање SynchronizationContext-ом или имитирање
SynchronizationContext-а код апликација које немају уграђен, као што је случај код конзолних
апликација.
Коментар
Разлог зашто се препоручује употреба async Task метода пре него async void метода је
олакшано тестирање. Чак и креирање варијанти void метода заснованих на Task повратној
вредности у некој мери пружа могућност тестирања.
25
ИИ-8 Креирање ValueTask-а
Проблем
Решење
Опште правило је да се као повратна вредност асинхроне методе користи Task<T>. Када
асинхрона метода значајно дуго траје, трошак креирања Task<T> повратне вредности је
занемарљив. Међутим, некада је асинхрона операција која се иницира краткотрајна и
моментално враћа резултат. У тим случајевима не желимо Task<T> као повратну вредност
асинхроне методе, јер нема много смисла када се асинхрона активност одмах завршава. Тада
нам је потребан тип који је сличан типу Task и који се може асинхроно сачекати, а притом не
захтевати трошак креирања непотребне целокупне Task<T> повратне вредности.
Често је асинхрона метода одмах спремна да врати ValueTask<T> резултат. У том случају,
може се извршити оптимизација, коришћењем конструктора ValueTask<T>. У супротном
наставља се са асинхроном методом само ако је потребно .
26
public class MyAsyncDisposable : IAsyncDisposable
{
private Func<Task> _disposeLogic;
Коментар
Препоручљиво је да већина метода које пишемо враћа Task<T>, јер таква пракса ствара мање
замки при употреби таквих метода него што је случај са ValueTask<T> повратном вредношћу.
Уколико се имплементира неки интерфејс који користи ValueTask<T> и ValueTask, онда је
најчешће могуће користити async и await. Међутим, када сами имплементирамо методе са
таквим повратним вредностима, потребна је нешто напреднија имплементација.
27
ИИ-9 Употреба ValueTask-а
Проблем
Решење
Такође, могуће је и асинхроно чекање након извршења конкурентне операције, као што се
ради са Task<T>.
Једно од ограничења ValueTask-а је што може бити асинхроно сачекан само једном. У
претходним примерима то ограничење није прекршено.
28
async Task UpotrebaMetodeAsync()
{
Један ValueTask<T> могуће је само једном конвертовати у Task<T>. Ако се врши конверзија, то
се најчешће изврши одмах, а након тога се ValueTask<T> потпуно игнорише. Важно је
напоменути да над једним ValueTask<T> није могуће обавити и await и позвати AsTask.
Када се ради са ValueTask<T>, најчешће се ValueTask<T> или одмах асинхроно чека или
конвертује у Task<T>, а затим са њим ради као са било којим Task<T>.
Коментар
Комплекснија употреба ValueTask<T> захтева посебан опрез, зато што овај тип нема гаранције
какве има Task<T>. При синхроном враћању резултата, Task<T>, за разлику од ValueTask<T>,
блокира позивајућу нит док операција не буде извршена. Код ValueTask<T> је важно обратити
пажњу на то да се синхроно резултат може вратити само једном и то након што је ValueTask<T>
извршен, као и да се тај ValueTask више не може асинхроно чекати или конвертовати у
Task<T>. Како би се спречили бројни проблеми који могу да настану, савет је да се, када се
ради са ValueTask<T>, ValueTask<T> или одмах асинхроно чека или конвертује у Task<T>.
29
ИИ-10 Враћање завршених Task-ова
Проблем
Решење
Употребом Task.FromResult можемо креирати и вратити нови Task<Т> који је већ завршен са
одређеном вредношћу.
interface IAsinhroniInterfejs
{
Task<int> VratiVrednostAsync();
}
interface IAsinhroniInterfejs
{
Task VratiVrednostAsync();
}
30
interface IAsinhroniInterfejs
{
Task UradiAsync();
}
Коментар
31
ИИ-11 Извештавање о напретку извршења
Проблем
Решење
while (!done)
{
//...
progress?.Report(percentageComplete);
}
}
Коментар
IProgress<T> параметар, по конвеницији, може да буде null, јер методи која асинхрону
методу позива можда није потребно праћење напретка. Због тога је важна провера улазног
параметра који означава напредак.
Ако је Progress<T> креиран на UI нити, он при конструкцији бележи UI контекст и у оквиру тог
контекста реагује на свој callback. То значи да се, тада, интерфејс може ажурирати преко
Progress<T> callback-а, чак и када се Progress<T>.Report позива из асинхроне методе са друге
нити.
32
IProgress<T> и прекидање извршења кôда, нису резервисани само за асинхрони кôд. Требало
би да се користе и за синхроне методе које се дуго извршавају.
33
ИИ-12 Покретање Task-ова у одређеном редоследу
Проблем
Постоје примарни Task-ови који треба да се заврше пре него што секундарни Task-ови буду
извршени.
Решење
Тип Task има доступну методу ContinueWith која извршава пратећи Task примарног тек
завршеног Task-а. Она омогућава надовезивање Task-а који треба да се изврши асинхроно
одмах након завршетка оригиналног Task-а, тј. пружа уланчавање између претходника и
последице. То је корисно када неки задаци морају да прате одређени редослед извршавања, а
неки други не.
Уколико се ContinueWith позове више пута над оригиналним Task-ом, толико последичних
Task-ова ће бити истовремено асинхроно покренуто над претходником. Редослед њиховог
извршења у време компајлирања није одређен, тако да редослед њиховог извршења може
варирати од једног до другог покретања апликације.
У следећем примеру извршавање почиње исписом почетка, који се без изузетка први
извршава, а наставља се пратећим исписом задатка A. Задаци B и C су последични задаци
задатка A и истовремено се асинхроно покрећу након завршетка задатка A. Редослед исписа
задатка B и задатка C може варирати из горе описаног разлога. Последњи ће се догодити испис
краја извршења јер он асинхроно чека завршетак групе задатака B и C.
Коментар
34
велики број варијанти са различитим параметрима којима се контролише понашање. Неке
варијанте примају TaskContinuationOptions вредности које се могу комбиновати логичким OR
оператором.
На тај начин можемо се претплатити на ослушкиваче догађаја оригиналног Task-а који прате
његов статус завршености и у зависности од тога да ли се извршио на предвиђен или
непредвиђен начин, покрећу одговарајући последични Task. Ова могућност је нарочито
корисна када је оригинални Task такав да га само иницирамо, уланчамо са потенцијалним
последичним задацима и, “заборавимо” на њега, тј. не позивамо се на њега поново.
try
{
await resultTask;
35
ИИ-13 Започињање дуготрајног Task-а
Проблем
Решење
Коментар
36
3.1.1 Асинхрони токови
Асинхрони токови (asynchronous streams) су начин да се асинхроно прихвати и обради више
објеката. Они су засновани на асинхроном енумерационом типу IAsyncEnumerable<T>. То је
асинхрона верзија енумерационог типа и омогућава преузимање објеката асинхроно, једног
по једног, на захтев, а затим и њихову асинхрону обраду. У наставку ће равноправно бити
коришћени изрази асинхрони ток и асинхрона колекција.
37
Тип повратне Повратна
Карактеристике Push/Pull
вредности вредност
T Појединачна Синхроно враћање појединачне вредности.
Синхроно
Task<T> Појединачна Погодно само за враћање појединачне вредности. Након што се Task<T> заврши, враћа
се једнострука T вредност. Чак и када је T колекција, вредност се враћа само једном.
Task<IEnumerable<T>> Вишеструка Иако је повратна вредност вишеструка и враћа се асинхроно, враћа се тек када цела
колекција буде спремна за враћање.
IAsyncEnumerable<T> Вишеструка Асинхроно враћање колекције вредности. Разликује се у односу на IEnumerable<T> у
томе што не блокира UI нит приликом узимања сваког објекта из колекције.
Асинхроно
38
ИИ-14 Креирање асинхроних токова
Проблем
Потребно је вратити више вредности (нпр. IEnumerable<T>) од којих свака захтева додатну
асинхрону обраду.
Решење
У наредном примеру се асинхроно враћа колекција укупно 10 вредности од којих свака пре
израчунавања свог квадрата захтева додатну асинхрону обраду.
39
//Kreiraj rezultate za ovu stranicu
foreach (string value in valuesOnThisPage)
{
yield return value;
}
Коментар
Уобичајени узор везан за асинхроне токове је да је веома малом броју објеката потребна
асинхрона обрада. Већина итерација се одвија синхроно. У претходном примеру се асинхрона
обрада одвија једном на сваких 10 елемената, када је потребно асинхроно преузети следећу
страницу резултата.
40
ИИ-15 Употреба асинхроних токова
Проблем
Решење
Употреба асинхроног тока се ради комбиновањем await и foreach петље. Када метода
VratiKvadrateVrednostiAsync врати асинхрону колекцију, креира се асинхрони енумератор.
Разлика таквог енумератора у односу на уобичајени је у томе што прелазак на следећи
елемент колекције може бити асинхрони, тј. догодити се тек када следећи елемент пристигне.
Уколико следећи елемент пристигне, извршиће се тело методе, а уколико је крај пролазака
кроз асинхрону колекцију, изаћи ће се из петље. При обради сваког елемента могућа је и
одређена асинхрона обрада елемента. У том случају, на следећи елемент ће се прећи тек када
тело петље за тај елемент буде извршено.
Коментар
Иако је могуће и природно користити await foreach за пролазак кроз асинхрону колекцију,
доступна је и исцрпна библиотека са LINQ операторима који се могу користити у ту сврху. Као
што је већ напоменуто, доступно је и избегавање оригиналног контекста, али и прекидање
обраде асинхроних токова прослеђивањем CancellationToken-а.
41
ИИ-16 Употреба LINQ са асинхроним токовима
Проблем
Решење
Као што IEnumerable<T> има подршку за LINQ над објектима, тако и IAsyncEnumerable<T> има
подршку доступну у оквиру NuGet пакета System.Linq.Async. Доступни су Where, Select,
SelectMany, Join и бројни други оператори. Већина LINQ оператора, као што је WhereAwait који
ће бити обрађен у наставку, сада прихвата и асинхроне делегате.
Међутим, питање је како користити Where оператор када је Where израз асинхрон. Конкретно,
како користити овај оператор када за сваки елемент треба асинхроно проверити, нпр. у бази,
да ли треба да буде уврштен у резултат.
Одговор је да се Where оператор не може користити са асинхроним изразима, зато што од свог
делегата очекује тренутан, синхрони одговор. За овакав случај погодан је WhereAwait оператор.
42
async Task VratiVrednostiLINQAsync()
{
IAsyncEnumerable<int> values = UsporavajucaKolekcija().WhereAwait(async
{
await Task.Delay(10); //asinhrona provera da li da element bude uvrsten
return value % 2 == 0;
});
Коментар
LINQ методе за асинхроне токове могу бити корисне и за уобичајене колекције. Уколико над
неком колекцијом желимо да користимо WhereAwait, или сличан оператор који подржава
асинхроне делегате, над колекцијом ћемо позвати ToAsyncEnumerable() и тиме је претворити у
асинхрону. Над асинхроном колекцијом онда можемо користити такве операторе.
Async оператори извлаче из колекције одређену вредност или обављају неку израчунавајућу
операцију над објектима колекције и враћају скаларну вредност коју треба асинхроно
сачекати. Пример таквог оператора је CountAsync који враћа појединачну нумеричку вредност
броја објеката колекције који задовољавају одређени услов.
43
ИИ-17 Прекид асинхроних токова
Проблем
Решење
44
async Task PrekiniSpoljaAsync()
{
using var cts = new CancellationTokenSource(500);
CancellationToken token = cts.Token;
Коментар
45
3.1.2 Прекид извршавања
Подршка за прекид извршавања уведена је у .NET 4.0 оквиру. Исцрпна је и добро
имплементирана. Омогућава да прекид извршавања буде захтеван из спољног кôда, али
никако насилно изазван над кôдом. Такође, уколико кôд није написан тако да подржи прекид,
прекид над њим неће бити могућ.
Прекид извршавања је нека врста сигнала, при чему постоје извор и прималац сигнала. Извор
је типа CancellationTokenSource, а прималац CancellationToken.
Метода која подржава прекид прима CancellationToken као улазни параметар, како би
позивајућем кôду назначила подржану могућност прекида. Уколико метода не захтева прекид,
потребно је или подесити подразумевану вредност улазног токена на default или обезбедити
и варијанту методе која не прима токен као параметар.
CancellationToken.None који је
прослеђен у претходном примеру је еквивалент
подразумеване вредности CancellationToken-а и означава токен који никада не може бити
прекинут.
46
ИИ-18 Изазивање прекида извршавања
Проблем
Решење
У наставку је, најпре, приказан пример асинхроне методе која подржава прекид, а затим и
асинхроне методе у оквиру које се тај прекид изазива.
Када се прекид изазове могућа су три исхода извршавања методе у оквиру које је прекид
изазван. Први исход може бити у потпуности успешно извршење, уколико се читава
функционалност извршила пре него што је дошло до прекида из спољног кôда. Други могући
исход је прекид извршавања услед захтеваног прекида. Такође, може се догодити и трећи
исход у ком се није догодило ништа везано за ручно изазивање прекида, већ се извршавање
прекинуло услед неочекиване грешке пре изазивања прекида.
//izazvan prekid
cts.Cancel();
47
catch (OperationCanceledException ex)
{
//izvrsavanje prekinuto na zahtev, pre kraja izvrsavanja
}
catch (Exception ex)
CancellationTokenSource је могуће прекинути само једном. Уколико је поново потребан након
што је прекид захтеван, неопходно је креирати нову инстанцу ове класе и помоћу ње захтевати
прекид.
Коментар
48
ИИ-19 Реаговање на захтев прекида периодичном провером
Проблем
Решење
Када у кôду имамо петљу која врши одређену обраду, онда не постоји асинхрона метода на
нижем нивоу којој се може проследити CancellationToken. Уколико, ипак, морамо да
прекинемо њено извршавање на захтев позивајућег кôда, постоји начин да реагујемо на
прекид. Периодичном провером улазног CancellationToken-а, утврђујемо да ли је од
претходног проласка кроз петљу до тренутног – изазван прекид.
if (i % 1000 == 0)
Коментар
49
ИИ-20 Прекид услед истека времена
Проблем
Решење
Постоје два уобичајена начина изазивања прекида услед истека времена, коришћењем типа
CancellationTokenSource. Најчешћи начин је прослеђивање бројача времена као параметра
CancellationTokenSource-а.
Коментар
Приказани начини изазивања прекида су ефикасни и лаки за примену. Ипак, важно је обратити
пажњу на то да кôд који се прекида, подржава прекиде. Није могуће прекинути кôд који није
прекидан.
50
ИИ-21 Прекид асинхроног извршавања
Проблем
Решење
У наставку је пример асинхроне методе која асинхроно чека другу асинхрону методу, а притом
јој прослеђује CancellationToken, како би у потпуности подржала реаговање на прекид
извршавања.
Коментар
Неке методе, ипак, не подржавају прекиде. То није добро, јер се никада не зна која ће метода
са вишег нивоа позвати такву методу, а желети подржаност прекида извршења. Пожељно је да
прекиди буду подржани увек када је то могуће.
Уколико прекиди нису подржани, потребно је импровизовати прекид. Један начин је да позив
такве методе окружимо методом која подржава прекид. Уколико не желимо да радимо на тај
начин, можемо једноставно да “прекинемо” асинхроно извршавање игнорисањем резултата
извршења.
51
3.1.3 Модуларно тестирање асинхроног извршавања
Како бисмо обезбедили да извршавање програма протиче на предвиђен начин, креирамо и
покрећемо тестове. Постоје бројни типови тестова софтвера – интеграцијски тестови, веб
тестови, тестови оптерећења итд. Модуларно тестирање (unit testing) извршавања представља
парадигму тестирања засновану на уситњавању функционалности на мале целине понашања
погодне за тестирање. Оне представљају модуларне јединице (units). Модуларни тестови
тестирају појединачне компоненте и методе и треба да тестирају само кôд који је под
контролом програмера, али не и инфрастуктурне проблеме какви су рад са базом података,
системом датотека или мрежом.
52
У процесу тестирања асинхроног извршавања, важно је праћење Task-а који асинхрона метода
враћа. Њиме се враћа информација о завршетку Task-а, резултатима и потенцијално насталим
грешкама. Због тога, неопходно је асинхроно сачекати извршење методе која се тестира. У
супротном, тест би можда прошао као успешан, игноришући грешке настале у оквиру
асинхроне методе. Дакле, мора се обезбедити да тест покаже неуспех, онда када је кôд који је
под тестом неуспешан. Већина савремених оквира за модуларно тестирање подржава
асинхроне модуларне тестове са Task-ом као повратним типом. Асинхрона метода која је под
тестом, а није асинхроно сачекана изазваће упозорење компајлера са ознаком CS4014, којим
се препоручује коришћење await кључне речи за употребу враћеног Task-а. Када се тестна
метода прилагоди асинхроном чекању и потпис јој треба, природно, променити у async Task.
53
ИИ-22 Модуларно тестирање async Task метода
Проблем
Решење
Већина савремених оквира за аутоматско тестирање, попут MSTest, NUnit и xUnit оквира,
подржава модуларно тестирање async Task метода. Они препознају када је повратна
вредност методе која се тестира типа Task и чекају на завршетак Task-а пре него што тест
прогласе успешним или неуспешним. Уколико је оквир за тестирање такав да не препознаје
async Task методе, потребно је омогућити неку врсту чекања асинхроне операције која се
тестира. Један начин је коришћење GetAwaiter().GetResult(). Могуће је и коришћење Wait()
методе. Битан недостатак првог начина је што избегава AggregateException у ком би биле
смештене потенцијално настале грешке.
Коментар
Како тестирање асинхроних метода може да буде компликовано, често је корисно најпре
тестирати одговарајуће синхроно понашање.
54
class SinhroniNeuspeh : MojInterfejs
{
Изазивање асинхроног понашања може се постићи помоћу Task.Yield, што је нарочито
корисно при модуларном тестирању.
55
ИИ-23 Модуларно тестирање async Task метода од чијег се извршења очекује
неуспех
Проблем
Потребно је модуларно тестирати async Task методу од које се очекује специфичан неуспех.
Решење
MSTest омогућава коришћење атрибута ExpectedException, где се дефинише тип грешке која
се очекује. Ипак, ово је лоше решење, јер таква грешка може настати у било којој методи која
се позива из теста, а не само у конкретној од које очекујемо неуспех.
//nije preporučljivo
[TestMethod]
Боље решење је оно које проверава да ли у конкретном делу кôда настаје грешка, а не у тесту
у целини. Већина савремених оквира за тестирање омогућава тестирање неуспеха конкретног
дела кôда помоћу неке врсте позива Assert.ThrowsAsync<TException> какав је доступан у
оквиру xUnit-а. Уколико не постоји таква подршка, функционалност је потребно написати
самостално. У наставку је пример ThrowsAsync-а доступног у xUnit-у.
//preporučljivo
[Fact]
public async Task Divide_WhenDenominatorIsZero_ThrowsDivideByZero()
{
Аналогна метода коју пружа MSTest је Assert.ThrowsExceptionAsync<TException>.
//preporučljivo
[TestMethod]
public async Task Divide_WhenDenominatorIsZero_ThrowsDivideByZero_Specific()
{
Важно је истаћи да је неопходно асинхроно чекање ThrowsAsync методе. await је тај који у
оквиру тестне методе прослеђује откривену грешку. Уколико се изостави асинхроно чекање,
тест ће се показати успешним какво год понашање методе било.
56
Коментар
Успешан сценарио је оно што ће сви тестирати пре пуштања софтвера на тржиште, међутим
неочекивано понашање апликације настаје због граничних случајева који нису покривени. Тако
да је тестирање обраде грешака једнако важно колико и тестирање предвиђеног понашања.
57
ИИ-24 Модуларно тестирање async void метода
Проблем
Решење
Пре него што се уопште покуша са решавањем овог тешко решивог проблема, савет је избећи
га у потпуности. Када год је могуће async void треба заменити са async Task. Метода некада
због архитектуралних захтева, ипак, мора да буде async void. Пример такве ситуације је када
метода мора да имплементира одговарајућу async void методу интерфејса који
имплементира.
У случају обавезне async void методе, прибегава се решењу са две написане методе. Једна од
њих је изворна async void метода која задовољава имплементацију интерфејса. Она притом
асинхроно чека резултат извршавања друге, async Task методе, у оквиру које је смештена
читава логика. Новокреирана метода је погодна за тестирање, а обавезна структура ничим није
нарушена.
[TestMethod]
58
Коментар
59
3.2 Имплементациони идиоми паралелног програмирања
У овом поглављу ће бити обрађени имплементациони идиоми којима се постиже
паралелизација обраде података. Аналогно секвенцијалном извршавању петље која итерира
кроз колекцију, при паралелизацији се користе методе Parallel.For и Parallel.ForEach. У
паралелним операцијама колекције се деле на сегменте који се, затим, извршавају
конкурентно, на више нити.
Такође, биће представљена и библиотека Parallel LINQ (PLINQ) која представља проширење
функционалности LINQ-а подршком за паралелну обраду. PLINQ је доста једноставнији за
употребу од класе Parallel, док она са друге стране има своје предности.
60
ИИ-25 Паралелна обрада података
Проблем
Решење
Тип Parallel садржи методу ForEach која је намењена управо за овај проблем. У наредном
примеру приказана је метода која врши паралелну ротацију матрица, прослеђених као
параметар, за одређени број степени.
Код прекидања паралелне петље разликују се два појма – заустављање петље које се изазива
унутар петље и прекид петље који се изазива из спољног кôда.
Следећи пример приказује раније заустављање извршавања петље, унутар петље, услед
наиласка на елемент који није валидан. За заустављање је коришћена метода Stop над
ParallelLoopState-ом. Након њеног позива ниједна итерација се не извршава.
Слична методи Stop је метода Break којом се спречавају сви будући позиви тела петље, док
позиви који су већ започети настављају са извршавањем. То значи да чак и извршавање
елемената иза невалидног елемента, може бити већ започето пре тренутка у ком је утврђена
невалидност елемента и заустављање петље.
61
void RotirajMatrice(IEnumerable<Matrix> matrices,
float degrees,
CancellationToken token)
Како би дељено стање између различитих нити на којима се извршавају паралелне операције
било конзистентно, мора се користити неки од механизама закључавања. У наставку је пример
неоптималне, али ефективне употребе закључавања због дељене вредности која представља
број невалидних елемената колекције, а која се мења са различитих нити.
Коментар
62
ИИ-26 Агрегација резултата паралелне обраде података
Проблем
Потребно је агрегирати, тј. сабрати резултате паралелне обраде података. Пример сабирања је
одређивање суме или просечне вредности резултата.
Решење
Класа Parallel омогућава агрегацију кроз концепт локалних вредности. То су променљиве које
постоје локално у оквиру паралелне петље. Њима тело методе приступа директно, без
синхронизације. Када се локални резултати агрегирају, користи се делегат localFinally који
захтева синхронизацију приступа променљивој која означава коначни резултат. У наставку је
пример паралелног збира.
Parallel.ForEach(source: values,
localInit: () => 0,
body: (item, state, localValue) => localValue + item,
localFinally: localValue =>
{
Коментар
Ипак, Parallel LINQ (PLINQ) има доступан природнији механизам агрегације резултата
паралелне обраде. Поред општег механизма PLINQ агрегације, могуће је користити и готове
агрегатне операторе какав је Sum. У већини случејева PLINQ библиотека пружа знатно
интуитивнији, краћи и једноставнији кôд у односу на Parallel класу.
63
ИИ-27 Паралелни LINQ
Проблем
Потребно је паралелном обрадом једне колекције креирати другу колекцију или агрегирани
резултат обраде колекције.
Решење
Коментар
PLINQ садржи бројне операторе, заступљене и у LINQ библиотеци. Неки од њих су оператор
пројектовања Select и Where оператор филтрирања. Могућа је једноставна агрегација
резултата паралелног извршавања коришћењем агрегатних функција какве су Sum, Average и
Aggregate.
Мада је PLINQ значајно једноставнији од класе Parallel, она је, ипак, погоднија за друге
процесе у систему. То је нарочито значајно на серверским уређајима.
64
ИИ-28 Паралелно позивање већег броја метода
Проблем
Решење
У класи Parallel постоји метода Invoke која је креирана да испуни овакве захтеве. Инициране
операције се притом извршавају симултано, тако да се може очекивати да целокупна обрада
траје колико и најдужа операција иницирана у оквиру Parallel.Invoke.
Коментар
Уколико акцију треба иницирати над сваким улазним елементом, уместо Parallel.Invoke
препоручује се употреба Parallel.ForEach. Такође, ако свака од акција креира излазни
резултат, саветује се коришћење оператора из PLINQ библиотеке.
65
ИИ-29 Прекид паралелног извршавања
Проблем
Решење
Коментар
Подршка прекида је важна са аспекта корисничког искуства. Чак и временски кратка операција
значајно користи процесор. Висока употреба процесора се негативно одражава на корисничко
искуство, јер је корисник примећује. Стога, при паралелним израчунавањима и другим
процесорски захтевним операцијама колико год кратко оне трајале, препоручљиво је
подржати прекиде извршавања.
66
ИИ-30 Обрада грешака при паралелном извршавању
Проблем
Решење
У оквиру паралелне петље потенцијално је могуће да при извршавању сваке итерације настане
грешка. Свака од насталих грешака биће смештена у један AggregateException – јединствену
агрегирану грешку. Таква грешка ће бити ухваћена у catch блоку. Свакој од грешака се, затим,
приступа из InnerExceptions атрибута агрегиране грешке.
Коментар
67
3.3 Имплементациони идиоми вишенитног програмирања
Област вишенитног програмирања на коју посебно треба обрадити пажњу при обради
имплементационих идиома је област синхронизације програмских нити. Синхронизација
обезбеђује да извршавање протиче без опасности од трке за приступ (race conditions) и застоја
(deadlocks) и штити дељени ресурс од доспевања у невалидно стање.
Inkrementiraj();
t.Wait();
return count;
}
68
private void Dekrementiraj()
{
for (int i = 0; i < total; i++)
69
ИИ-31 Синхронизација нити коришћењем типа Monitor
Проблем
Потребно је помоћу типа Monitor обезбедити симултани приступ податку дељеном између
нити.
Решење
Помоћу класе Monitor може се блокирати приступ друге нити, пре него што прва нит изађе из
дељеног сегмента кôда. Заштићени сегмент кôда се означава позивима статичких метода
Monitor.Enter() и Monitor.Exit(), који се међусобно препознају преко заједничког објекта
који се методама прослеђује. Важно је да заштићени блок буде окружен try-finally блоком.
Тиме сузбијамо могуће блокирање свих осталих нити ако се на тренутно закључаној догоди
грешка и, због тога, никада не стигне до Monitor.Exit().
Inkrementiraj();
t.Wait();
return count;
}
try
{
Monitor.Enter(sync, ref lockTaken);
count++;
}
finally
{
if (lockTaken)
70
private void Dekrementiraj()
{
for (int i = 0; i < total; i++)
{
bool lockTaken = false;
try
{
Monitor.Enter(sync, ref lockTaken);
count--;
}
finally
{
if (lockTaken)
Коментар
Доступна је и метода Monitor.Pulse() која омогућава нити да уђе у ред чекања на дељени
податак и да тиме назначи да је она та која је следећа у редоследу извршавања. Ово је
стандардни механизам синхронизације у складу са узором Проблема произвођача и
потрошача (Producer/Consumer Pattern), по ком нема употребе податка док траје проиводња
податка.
71
ИИ-32 Синхронизација нити коришћењем кључне речи lock
Проблем
Потребно је помоћу кључне речи lock обезбедити симултани приступ податку дељеном
између нити.
Решење
Како коришћење Monitor-а за синхронизацију може да буде позивано веома често у кôду, а
блок try-finally лако може да буде изостављен, доступна је lock кључна реч која омогућава
закључавање нити по истом узору синхронизације.
Inkrementiraj();
t.Wait();
return count;
}
72
Коментар
73
ИИ-33 Означавање поља кључном речју volatile
Проблем
Решење
Један од начина решавања овог проблема је означавање поља кључном речју volatile која
означава да је поље нестално, променљиво и да је осетљиво на промене од стране хардвера,
оперативног система или друге нити.
Коментар
Ипак, са volatile се мора бити опрезан. Уколико се употреба ове кључне речи не познаје
добро, препорука је да се користи lock механизам закључавања.
74
ИИ-34 Синхронизација нити коришћењем типа Interlocked
Проблем
Решење
Неколико линија кôда којима се проверава да ли је дозвољен улазак у методу која мења
вредност неке променљиве може непотребно трошити програмерско време. Синхронизација
употребом типа Interlocked прати Узор поређења и размене (Compare/Exchange Pattern).
Уместо ручног програмирања кôда који окружује сегмент осетљив на промене, у оквиру типа
Interlocked расположив је аналогни механизам Interlocked.CompareExchange.
У примеру који следи, првом параметру методе се нова вредност додељује само онда када је
нова вредност једнака вредности трећег параметра методе.
Коментар
Иако једноставне, Interlocked методе могу да имплементирају значајно снажне концепте, као
што су структуре података ослобођене закључавања. Ипак, такве структуре представљају
понављање операције све док не протекне исправно. Вишеструки позиви таквих метода могу
смањити ефикасност у односу на ону каква би била једноставним закључавањем дељеног
сегмента.
75
ИИ-35 Употреба Timer-а
Проблем
Решење
.NET омогућава две врсте мерача времена (timers) намењених за вишенитно окружење. То су
System.Threading.Timer и System.Timers.Timer. Основна разлика је та да прва од две класе на
threadpool нити извршава појединачни делегат у одређеним временским интервалима, док
други тип иницира догађај у задатим временским интервалима, такође, на threadpool нити.
void Pokreni()
{
var timerState = new TimerState { Counter = 0 };
timer.Dispose();
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}: done.");
}
76
Коментар
77
4 Закључак
78
5 Литература
Beck, K. (2007). Implementation Patterns. Addison-Wesley Professional.
Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable
Object-Oriented Software. Addison-Wesley Professional.
Hilyard, J., & Teilhet, S. (2015). C# 6.0 Cookbook: Solutions For C# Developers (4th изд.). O'Reilly
Media, Inc.
Microsoft. (2016, Јун 20). Async in depth. Преузето Октобар 18, 2020 са Microsoft Docs:
https://docs.microsoft.com/en-us/dotnet/standard/async-in-depth
Microsoft. (2016, Јун 20). Async Overview. Преузето Октобар 18, 2020 са Microsoft Docs:
https://docs.microsoft.com/en-us/dotnet/standard/async
Microsoft. (2017, Март 30). Asynchronous Programming Model (APM). Преузето Октобар 18, 2020
са Microsoft Docs: https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-
programming-patterns/asynchronous-programming-model-apm
Microsoft. (2017, Март 30). Synchronizing data for multithreading. Преузето Октобар 14, 2020 са
Microsoft Docs: https://docs.microsoft.com/en-
us/dotnet/standard/threading/synchronizing-data-for-multithreading
Microsoft. (2017, Март 30). Task Parallel Library (TPL). Преузето Октобар 14, 2020 са Microsoft
Docs: https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/task-
parallel-library-tpl
Microsoft. (2018, Октобар 16). Asynchronous programming patterns. Преузето Октобар 18, 2020 са
Microsoft Docs: https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-
programming-patterns/
Microsoft. (2018, Јул 23). Event-based Asynchronous Pattern (EAP). Преузето Октобар 18, 2020 са
Microsoft Docs: https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-
programming-patterns/event-based-asynchronous-pattern-eap
Microsoft. (2018, Септембар 12). Parallel Programming in .NET. Преузето Октобар 14, 2020 са
Microsoft Docs: https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/
Microsoft. (2018, Новембар 18). Threads and Threading. Преузето Октобар 14, 2020 са Microsoft
Docs: https://docs.microsoft.com/en-us/dotnet/standard/threading/threads-and-threading
Microsoft. (2018, Јул 3). Timers. Преузето Октобар 14, 2020 са Microsoft Docs:
https://docs.microsoft.com/en-us/dotnet/standard/threading/timers
Microsoft. (2018, Јул 28). Unit testing best practices with .NET Core and .NET Standard. Преузето
Октобар 16, 2020 са Microsoft Docs: https://docs.microsoft.com/en-
us/dotnet/core/testing/unit-testing-best-practices
79
Microsoft. (2019, Фебруар 26). Task-based asynchronous pattern. Преузето Септембар 13, 2020 са
Microsoft Docs: https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-
programming-patterns/task-based-asynchronous-pattern-tap
Microsoft. (2019, Август 7). Unit test basics. Преузето Октобар 16, 2020 са Microsoft Docs:
https://docs.microsoft.com/en-us/visualstudio/test/unit-test-basics?view=vs-2019
Microsoft. (2019, Мај 14). Walkthrough: Create and run unit tests for managed code. Преузето
Октобар 16, 2020 са Microsoft Docs: https://docs.microsoft.com/en-
us/visualstudio/test/walkthrough-creating-and-running-unit-tests-for-managed-code?
view=vs-2019
Microsoft. (2020, Мај 20). Asynchronous programming. Преузето Септембар 13, 2020 са Microsoft
Docs: https://docs.microsoft.com/en-us/dotnet/csharp/async
Microsoft. (2020, Јун 4). Asynchronous programming with async and await. Преузето Септембар
13, 2020 са Microsoft Docs: https://docs.microsoft.com/en-us/dotnet/csharp/programming-
guide/concepts/async/
Microsoft. (2020, Март 30). Data Parallelism (Task Parallel Library). Преузето Октобар 14, 2020 са
Microsoft Docs: Data Parallelism (Task Parallel Library)
Microsoft. (2020, Април 7). Get started with unit testing. Преузето Октобар 16, 2020 са Microsoft
Docs: https://docs.microsoft.com/en-us/visualstudio/test/getting-started-with-unit-testing?
view=vs-2019
Microsoft. (2020, Мај 18). Unit testing in .NET Core and .NET Standard. Преузето Октобар 16, 2020
са Microsoft Docs: https://docs.microsoft.com/en-us/dotnet/core/testing/?pivots=mstest
Microsoft, & Cleary, S. (2015, Јул 2). Async Programming : Unit Testing Asynchronous Code.
Преузето Октобар 17, 2020 са Microsoft Docs: https://docs.microsoft.com/en-
us/archive/msdn-magazine/2014/november/async-programming-unit-testing-
asynchronous-code
Strauss, D. (2017). C#7 and .NET Core Cookbook (2nd изд.). Packt Publishing.
Why writing idiomatic code is important? (.). Преузето Септембар 13, 2020 са
https://medium.com/@abyu/why-writing-idiomatic-code-is-important-f7e46c799c26
80