Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1of 89

УНИВЕРЗИТЕТ У БЕОГРАДУ

ФАКУЛТЕТ ОРГАНИЗАЦИОНИХ НАУКА

ЗАВРШНИ МАСТЕР РАД

Тема: Имплементациони идиоми асинхроног,


паралелног и вишенитног програмирања у C#.NET-у

Ментор: Кандидат:
др Саша Д. Лазаревић Ирена Савковић 2018/3713

Београд, 2020. године


Сагласност чланова комисије за одбрану
Попуњавају чланови Комисије за одбрану:

Комисија која је прегледала рад


кандидата САВКОВИЋ (СЛОБОДАН) ИРЕНА
под насловом ИМПЛЕМЕНТАЦИОНИ ИДИОМИ АСИНХРОНОГ, ПАРАЛЕЛНОГ И ВИШЕНИТНОГ
ПРОГРАМИРАЊА У C#.NET-У и одобрила одбрану:

Ментор: др Саша Д. Лазаревић, ванредни професор

_________________________________________

Члан: др Илија Антовић, доцент

_________________________________________

Члан: др Божидар Раденковић, редовни професор

________________________________________
Радна биографија кандидата

Ирена С. Савковић
Ужице, Република Србија

 организованост
ЛИЧНЕ
 одговорност
КАРАКТЕРИСТИКЕ
 посвећеност
 иницијативност

ВЕШТИНЕ  Microsoft .NET платформа


СПОСОБНОСТИ  Релационе базе података, SQL
ИНТЕРЕСОВАЊА  Пословни информациони системи

Август 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)

ЗАВРШНИ РАД: Развој софтверског система за


евиденцију испорука курирске службе употребом
.NET технологија (10)

2010 – 2014
Ужичка гимназија – Ужице, Република Србија

 Српски језик – матерњи језик


ПОЗНАВАЊЕ
 Енглески језик – компетентна употреба језика
ЈЕЗИКА
 Грчки језик – самостална употреба језика
 Француски и италијански језик – елементарна употреба језика

 Ђак генерације – 2010


НАГРАДЕ  Вукова диплома – 2010, 2014
 Стипендија за младе таленте – 2010, 2011
 Стипендија за студенте – 2015, 2016, 2017
Изјава о академској честитости
Попуњава кандидат:

Савковић, Слободан, Ирена 2018/3713


Презиме, име једног родитеља, име Број индекса

Студијски програм: Софтверско инжењерство и рачунарске науке


Модул:

Аутор завршног рада под насловом: Имплементациони идиоми асинхроног, паралелног и


вишенитног програмирања у C#.NET-у
Чија је израда одобрена на Седници Већа студијских програма мастер академских студија
одржаној: 24.08.2020.

Потписивањем изјављујем:
 Да је рад искључиво резултат мог сопственог истраживачког рада;
 Да сам рад и мишљења других аутора које сам користио у овом раду назначио
или цитирао у складу са Упутством;
 Да су сви радови и мишљења других аутора наведени у списку
литературе/референци који су саставни део овог рада и писани су у складу са
Упутством;
 Да сам довио све дозволе за коришћење ауторског дела који се у
потпуносзи/целости уносе у предати рад и да сам то јасно навео;
 Да сам свестан да је плагијат коришћење туђих радова у било ком облику (као
цитата, парафраза, слика, табела, дијаграма, дизајна, планова, фотографија,
филма, музике, формула, веб сајтова, компјутерских програма и сл.) без
навођења аутора или представљање туђих ауторских дела као мојих, кажњиво
по закону (Закон о ауторским и сродним правима, Службени гласник Републике
Србије, бр. 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.1 Општа идеја програмског узора


Узор (pattern) представља једноставно и елегантно решење специфичног проблема. Таква
решења се увек развијају и усавршавају временом и никада не настају као почетна решења.
Редизајном и реаранжирањем кôда долази се до одређених концепата који кôд чине
модуларним, флексибилнијим, разумљивијим и погодним поновној употреби. Идеја узора је
да омогуће програмерима да своје умеће унапређују већ утемељеним знањима својих
претходника, без потребе да сами истражују и дођу до неког решења које вероватно није
ефикасно колико би могло бити. Сваки од узора настао је као потреба, након што је уочено на
који начин би се оптимално решио мање или више захтеван проблем из праксе. Већина
проблема са којима се програмер може сусрести је вероватно већ решена и категорисана као
неки од узора. Узори на највишем нивоу апстракције нису везани за програмску парадигму.

1.2 Општа структура програмског узора


У општем случају, узор се састоји од четири елемента:

1. Назива узора – ефектног кратког назива који обухвата проблем, решење и последице и
на конкретан начин дефинише узор. Важно је да назив узора буде препознатљив ради
лакше комуникације;
2. Дефиниције проблема – опис проблематике, контекста у ком се јавља и, често, услова
који морају бити испуњени да би се узор могао применити;
3. Решења проблема – апстрактни опис решења проблема, без конкретне
имплементације, који приказује како се решење може применити у различитим
ситуацијама и контекстима;
4. Коментара – значајни за евалуирање примене узора. Описују утицај на систем у
погледу флексибилности, модуларности, надградивости итд. Омогућава анализу
предности и недостатака примене узора.

1.3 Имплементациони узори


Имплементациони узори или имплементациони идиоми су програмски узори налик дизајн
узорима, али на нижем нивоу апстракције. Они описују примену уочене понављајуће
конструкције у конкретној технологији. Уско су везани за одређену програмску парадигму
(императивна, функционална, објектно-оријентисана парадигма итд.), појединачни
програмски језик, оквир или библиотеку. Представља израз одређеног задатка, алгоритма или
структуре који није обавезно уграђен у програмски језик.

Имплементациони узор је скуп фрагмената кôда повезаних ради остваривања одређеног


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

1
Због уске везаности за програмски језик, имплементациони идиоми нису разумљиви
програмерима других програмских језика и парадигми, иако је истина да су поједини
имплементациони идиоми прилично слични у различитим програмским језицима. Идиом се у
природним језицима дефинише као “коришћење и означавање израза који су природни за
изворног говорника”. Идиом у програмским језицима би се могао дефинисати аналогно, тако
да су изрази природни програмеру конкретног програмског језика.

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


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

1.4 Исечци кôда


Исечци кôда (code snippets) су узори на најнижем нивоу апстракције. То су унапред спремни
делови кôда које је могуће брзо уградити, најчешће командом десног клика, скупа тастера на
тастатури или из менија. Они чине писање кôда бржим, лакшим и поузданијим. Омогућавају
избегавање куцања рутинског, понављајућег кôда. Скоро сви савремени текстуални едитори
поседују алате за креирање исечака кôда. Најчешће су то команде за генерисање костура
петљи, if-else услова, try-catch блокова, конструктора, атрибута итд. Visual Studio омогућава
две врсте исечака C# кôда – експанзивне исечке (expansion snippets) и обухватајуће исечке кôда
(surround-with snippets). Експанзивни исечци кôда мењају пречицу која је откуцана исечком
који се креира. Пример таквог исечка је tryf за креирање try-finally блока. Друга групација
исечака су они који селектовани кôд обухватају у изабрани исечак. Пример је команда if.
Исечци кôда могу имати параметре који се при креирању морају заменити како би се написао
жељени кôд. Пример параметра за замену је почетни подразумевани параметар true у if
премиси услова који се генерише if командом. Visual Studio омогућава програмерима да и
сами креирају своје исечке кôда, увезу их у окружење и такве користе. Исечци кôда могу бити
прости или сложени колико је потребно.

2
2 Конкурентност
Конкурентно програмирање подразумева извршавање више програмских токова, односно
нити, истовремено. Програмска нит (thread) представља појединачни секвенцијални
контролни ток, јединствен низ инструкција које се извршавају. Велики број програма никада
неће захтевати више од једне нити (single-threaded programs). Међутим, далеко је већи број
оних других (multithreaded programs) којима је примена конкурентности круцијална за
успешност. Употреба програмских нити омогућава програму да буде респонзиван и ефикасан.

Савремени рачунарски системи имају већу способност обраде него икада, тако да је само на
програмерима да умање латентност везану за процесорску активност (processor-bound latency)
– да дељењем процесорски захтевних послова на вишеструке процесоре смање трајање
обраде. Такође, латентност везана за улазно-излазне активности (I/O-bound latency) треба да
буде минимизирана колико год је могуће, јер некада може да буде једнака извршењу
милиона инструкцијских циклуса, као када се нпр. преузимају подаци са удаљеног веб
сервера.

Вишенитно програмирање је неопходно у апликацијама које имају сложену логику обраде


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

Могућност да се постигне истовремено извршавање више задатака је ослобађајућа, али је и


тачка у којој више немамо потпуну контролу над програмом какву бисмо имали
програмирајући синхроно. Тачке на које треба обратити посебну пажњу су вишеструки приступ
дељеним подацима (race conditions) и потенцијална међузависност која може довести до
застоја (deadlock).

Конкурентност је, ипак, кључ лепо имплементираног софтвера. У прошлости је била много
захтевнија и тежа за писање, одржавање и отклањање грешака. Неретко су је програмери
избегавали. Данас је она доста прилагођенија широкој стручној заједници и у великој мери
олакшана за примену. У сваком случају, конкурентни кôд је императив савременог софтвера.

Међу кључним циљевима конкурентног програмирања је стављање целокупне расположиве


процесорске снаге у употребу, како би се кориснику система резултати испоручивали брзо и
ефикасно, без нарушавања корисничког искуства. Недопустиво је да се графички интерфејс
“заледи” када буде покренута нека процесорски захтевна активност. Такође, циљ је смањити
временско трајање процесорски захтевне обраде дељењем посла на сегменте који се обрађују
паралелно на више програмских нити и трају укупно онолико колико траје обрада временски
најзахтевнијег сегмента.

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

3
2.1 Процесорско распоређивање нити
Процесор или централна процесорска јединица (central processing unit – CPU) је рачунарска
компонента која, заправо, извршава конкретан програм. Сваки рачунар има бар један
процесор, а неки од савремених и више њих. Већина савремених процесора је вишејезгарна,
при чему је свако језгро виртуелна процесорска јединица. Из тог разлога се процесорско језгро
повремено назива процесором.

Програм у извршавању назива се процес (process), а процесима управља оперативни систем.


Сваки процесор у једном тренутку може да извршава само једну нит. У наставку је описан
начин на који Windows оперативни систем врши управљање нитима.

Када дође време да се у процесору распореди нит, Windows мора да уради замену контекста.
Током замене контекста (context switch), Windows чува стање тренутне нити у интерни објекат
нити у оперативном систему, узима другу нит из колекције спремних нити и пратеће логике о
њиховом извршавању (threadpool-а програмских нити) и прослеђује процесору информацију о
стању нити из интерног објекта нити. Затим почиње да је извршава. Уколико се распоређује
нит са другог процеса, утрошак је још израженији јер се адресни простор празни.

Нит се онда извршава током једног квантума нити. Квантум (quantum) представља временски
период током ког процесор извршава одређену програмску нит. То је вишеструка вредност
clock interval-а (clock interval износи око 15 милисекунди на савременим вишепроцесорским
системима). Када се кôд враћа са врха стека, улази у стање чекања или је квантум времена
истекао, распоређивач узима другу спремну нит. То може бити иста или друга нит, у
зависности од такмичења за процесоре. Нит може ући у стање чекања ако се заблокира
чекајући на I/O или намерно позивајући Thread.Sleep. Windows Server има већи квантум нити
него десктоп верзија Windows-а. То значи да се нити извршавају дужи временски период пре
замене контекста.

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

2.2 Технике конкурентног програмирања


Конкурентно програмирање је вишенитно програмирање у ширем смислу. Конкурентност
било које врсте представља обављање више задатака истовремено. Често се конкурентност
поистовећује са вишенитним програмирањем, иако је вишенитно програмирање само једна од
техника конкурентности.

Вишенитно програмирање у ужем смислу је техника конкурентности која користи више нити
извршавања програма. Означава, дословно, више нити. Вишенитно програмирање свакако
није превазиђено, али старомодне технике креирања нити на нижем нивоу свакако јесу и није
им место у савременим апликацијама. Данас постоје много моћније и ефикасније технике
вишенитног програмирања на вишем нивоу апстракције. Директно креирање Thread-а је
превазиђена техника. Са друге стране, вишенитно програмирање живи кроз threadpool, који
сам распоређује задатке и прилагођава се захтевима. Такође, threadpool омогућава другу
технику конкурентног програмирања – паралелно програмирање.

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

Асинхроно програмирање је облик конкурентности који користи futures и callbacks како би


избегао непотребне нити. Идеја асинхроног програмирања се базира на томе да ће се нека
операција која започиње, завршити у неком тренутку у будућности. Заснована је на принципу
“обећања”. Када се операција заврши, она обавештава свој future, или покреће callback или
догађај којим обавештава апликацију о свом завршетку. За то време, нит која је покренула
операцију била је слободна за извршавање других задатака. Савремени језици подржавају
async и await команде којима је асинхроно програмирање значајно олакшано.

Реактивно програмирање је блиско асинхроном програмирању, с тим што се не заснива на


асинхроним операцијама него на асинхроним догађајима. Апликација у овом случају реагује
на низ догађаја.

Сваки од облика конкурентности налази своју примену у различитим аспектима савремених


апликација. У наставку ће бити детаљно обрађене кључне технике конкурентности и
имплементациони идиоми који олакшавају решавање конкретних практичних проблема
конкурентности.

5
2.2.1 Асинхроно програмирање
Асинхроно програмирање је кључна техника која обезбеђује респонзивност програма са
графичким корисничким интерфејсом (захтевање података са мреже, приступање бази
података или читање/писање у систему датотека) и скалабилност серверских апликација
(способност одговора на већи број захтева). О томе је било говора у уводном поглављу 2
Конкурентност. Савремене апликације значајно много користе рад са системом датотека и
мрежом. Све те активности подразумевано при свом одвијању блокирају графички интерфејс,
мада је све више оних које подржавају асинхроно извршавање.

При примени асинхроног програмирања важно је правилно идентификовати задатке везане за


улазно-излазне активности (I/O-bound) и, са друге стране, процесорски захтевне операције
(CPU-bound). Уколико се погрешно препозна тип проблема, поједине конструкције се могу
погрешно употребити и значајно негативно утицати на перформансе. Дакле, уколико наш кôд
чека на нешто, као на пример податке из базе података, посао је везан за улазно-излазне
активности. Ако је, ипак, потребно извршити неко “скупо” израчунавање, посао зависи од
искоришћености процесорских могућности.

.NET омогућава три узора асинхроног извршавања:

 Асинхрони узор заснован на Task-овима (Task-based Asynchronous Pattern – TAP)


 Асинхрони узор заснован на догађајима (Event-based Asynchronous Pattern – EAP)
 Модел асинхроног програмирања (Asynchronous Programming Model – APM)

Сваки од њих ће бити у некој мери обрађен у наставку. Савремени узор асинхроног
програмирања је TAP. У његов развој се активно улаже, док су преостала два застарела и не
препоручују се за примену у савременом развоју софтвера. Самим тим разумевању
асинхроног узора заснованог на Task-овима и обради његових карактеристика биће посвећено
највише пажње.

Како је креирање Thread-а релативно “скупа” операција, јер захтева повлачење велике
количине меморије, значајно је ефикасније препустити то задужење threadpool-у. Он је
намењен да обавља посао распоређивања нити, додељивања асинхроних активности
одређеној нити, извршавања активности, а онда и да испланира поновну употребу постојеће
нити за неку наредну активност, уместо ручног уништавања нити и креирања нове које бисмо
ми извели.

Из тог разлога је уведена апстракција вишенитног програмирања на вишем нивоу. Креирањем


новог Task-а, сигнализира се распоређивачу послова (task scheduler) да постоји асинхрони
посао који је потребно обавити. Многе су опције суштинске реализације на располагању.
Можда ће threadpool одлучити да је ефикасније покренути Task касније, можда након
неколико завршених послова који се у том тренутку извршавају, или га распоредити на
одређени процесор. Такође, процениће да ли за њега треба креирати нову нит или, ипак,
искористити постојећу која је испунила свој задатак.

2.2.1.1 Асинхрони узор заснован на Task-овима (Task-based Asynchronous Pattern – TAP)


Task-based Asynchronous Pattern (TAP) је препоручени асинхрони узор у савременом развоју.
Уведен је са појавом .Net Framework 4. Заснива се на System.Threading.Tasks.Task и
System.Threading.Tasks.Task<TResult> типовима.

6
Task је објекат који енкапсулира кôд намењен за асинхроно извршавање. Личи на делегат, али
је суштинска разлика та да је делегат синхрони, а Task асинхрони. Када се извршава делегат
написан као ламбда израз, контрола се у позивајући кôд пребацује тек када делегат буде
извршен. Са друге стране, када се иницира Task, контрола се готово одмах враћа на кôд који је
покренуо асинхроно извршавање, док за то време асинхрони посао траје.

TAP подразумева јединствену методу која обухвата и иницирање и завршетак асинхроне


операције. Тај концепт је у супротности са Асинхроним моделом програмирања (APM) и
Асинхроним узором заснованим на догађајима (EAP).

Кључне речи које се користе су async и await. Додавањем async у декларацију методе
омогућава се функционалност await команди у оквиру методе. Такође, сигнализира се
компајлеру да генерише state machine за ту методу, којим се прати асинхроно извршавање и
наставља ток извршавања када позадинска активност буде комплетно извршена, слично
функционисању yield return-а. Додавањем await команде врши се повраћај контроле
позиваоцу асинхроне методе. Од тренутка иницирања асинхроног извршавања графички
интерфејс је респонзиван, иако се позвана активност истовремено одвија.

Важно је истаћи да су Task-ови апстракција асинхроног обављања посла, а не апстракција


вишенитности. Подразумевано, Task-ови се извршавају на тренутној нити и делегирају посао
оперативном систему. Опционо се може захтевати њихово извршење на одвојеној нити
позивом Task.Run.

2.2.1.1.1 Систем именовања


Конвенција за именовање асинхроне операције је да се на назив операције дода суфикс Async.
Уколико класа већ садржи операцију са суфиксом Async, препорука је да се на нову операцију
дода суфикс TaskAsync, а ако постоји и са таквим суфиксом, да се надовеже са GetTaskAsync.
Ако метода само започиње асинхрону операцију, али не враћа вредност коју је могуће чекати,
њен назив би требало да почиње са Begin, Start или неким другим интуитивним префиксом.
Методе које јасно индикују сврху методе не морају пратити конвенцију именовања. Такве
методе се често зову комбинаторима. Пример за такве методе су WhenAll и WhenAny.

2.2.1.1.2 Улазни параметри асинхроне методе


Улазни параметри асинхроне методе би требало да се подударају са одговарајућим
параметрима синхроне методе и требало би да буду прослеђени у истом редоследу. Међутим
out и ref параметри су изузетак. Њих треба избегавати у потпуности. Све што би се на овај
начин прослеђивало, боље је вратити кроз TResult као саставни део неке сложене структуре
какав је нпр. Tuple. Такође, препорука је да се, чак и ако одговарајућа синхрона метода не
поседује CancellationToken као улазни параметар, он подржи као улазни параметар
асинхроне методе.

2.2.1.1.3 Повратна вредност асинхроне методе


Асинхрона метода никада не треба да враћа void, осим у случају да се пише асинхрони
обрађивач догађаја. Треба да врати било шта што је типа везаног за Task. Повратна вредност
може бити Task<TResult> ако враћа вредност или Task ако не враћа вредност. Може бити и
ValueTask, односно IAsyncEnumerable<T> или IAsyncEnumerator<T> за враћање више вредности
у колекцији. Ти типови који су сродни Task-у фукционишу као futures. Оне обавештавају кôд
који их је позвао да се асинхрона метода завршила.

7
2.2.1.1.4 Начин функционисања асинхроне методе
Асинхрона метода се извршава као скуп синхроних сегмената. Ти сегменти подељени су await
командама. Асинхрона метода почиње да се извршава синхроно, баш као и свака синхрона
метода, све до прве await команде.

Количина синхроног посла (валидирања улазних параметара и иницирања асинхроне


операције) мора бити сведена на минимум како би се асинхрона метода извршила брзо и
вратила контролу на синхрони ток. Разлог за то је што асинхрона метода може бити позивана
са графичког интерфејса и сложеним синхроним делом посла нарушити респонзивност
интерфејса. Са друге стране, уколико се позива више асинхроних метода, позив сваке следеће
одлагаће се сложеним синхроним послом претходно позиваних метода.

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

Први сегмент асинхроне методе се свакако извршава на нити која је методу позвала, а на
остале сегменте се може утицати. Када се догоди први await у оквиру асинхроне методе,
бележи се стање контекста. Метода, подразумевано, наставља да се извршава у оквиру тог
контекста. Ако се то догађа на нити интерфејса, онда је у питању UI контекст. У већини других
случајева то је threadpool контекст.

Ако не желимо извршавање сегмента на оригиналном контексту, можемо асинхроно сачекати


резултат методе ConfigureAwait са вредношћу false параметра continueOnCapturedContext.
Тада ће се сегмент са нити интерфејса извршити на threadpool нити. У наставку је пример.

Извршење сегмента на UI контексту:

Извршење сегмента на threadpool контексту:

2.2.1.1.5 Креирање инстанце Task-а


Задатак који служи за извршавање захтевне обрачунске операције, који представља кôд који
CPU заправо треба да изврши покреће се са Task.Run или Task.Factory.StartNew ако је
потребно да се изврши на одређеном распоређивачу. Остали задаци представљају
“обавештење”. Такви су, углавном, и I/O задаци. Они се крерају са
TaskCompletionSource<TResult>.

2.2.1.1.6 Статус Task-а


Task класа омогућава енумерацију вредности животног циклуса Task-а. Task-ови који су
креирани помоћу јавних конструктора класе Task сматрају се хладним Task-овима (cold tasks),
јер су подразумевано у TaskStatus.Created нераспоређеном стању и биће распоређени тек
кад над њиховом инстанцом експлицитно буде покренут Start. Сви остали започињу циклус са
статусом другачијим од TaskStatus.Created. Уколико је Task завршен пре времена, асинхрона
метода враћа Task са статусом TaskStatus.Canceled. Овај статус спада у коначне статусе, као и
TaskStatus.Faulted и TaskStatus.RanToCompletion . Остале могуће вредности статуса су
WaitingForActivation, WaitingToRun, Running и WaitingForChildrenToComplete. Ипак, уколико

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

2.2.1.1.7 Обрада грешака у извршавању асинхроне методе


Када се у извршавању асинхроне методе догоди грешка, она ће бити смештена у Task који је
повратна вредност методе. Ако се тај Task затим асинхроно сачека, грешка ће бити преузета из
Task-а и бити поново бачена у оквиру асинхроне методе која је Task са грешком чекала. Затим
се грешка уобичајено хвата и обрађује, при чему има изворни stack trace. Пример је у наставку.

async Task PokusajNestoAsync()


{
try
{
await MogucaGreskaAsync();
}
Уколико се изостави асинхроно чекање, неће се догодити окончање процеса. То је обезбеђено
од стране распоређивача задатака који окружује делегат обрађивачем необрађених догађаја.
На тај начин се грешка неће испољити видљиво, иако до ње дође. Ипак, то је збуњујућа
ситуација за корисника, јер није свестан да се апликација налази у неконзистентном стању и да
од тог тренутка можда константно причињава штету.

2.2.1.1.8 Спречавање застоја


Употребом async декларације свуда кроз кôд, спречава се могућност настанка застоја
(deadlock). Свака асинхрона операција треба и да се асинхроно сачека, тј. да се асинхроно
сачека Task који она враћа. Чекањем завршетка асинхроне операције у синхроној методи
помоћу синхроног чекања са Task.Wait(), Task<TResult>.Result или
GetAwaiter().GetResult() сможе доћи до застоја.

До застоја ће доћи ако се таква синхрона метода позива са нпр. UI контекста, јер он подржава
само једну нит у једном тренутку. Застој се дешава зато што синхрона метода асинхрону
методу не чека асинхроно, већ синхроно. Синхроно чекање блокира UI нит, тако да повратак са
извршења асинхроне методе на изворни контекст није могуће, јер је он већ заузет синхроним
чекањем. Ово се може избећи или претварањем синхроне методе у асинхрону или
извршавањем асинхроне методе на другом контексту.

Детаљније о начину на који оперативни систем изводи функционисање асинхроног


извршавања може се прочитати у Microsoft-овој документацији у темељном чланку Async in
depth.

9
2.2.1.2 Асинхрони узор заснован на догађајима (Event-based Asynchronous Pattern – EAP)
Овај узор је представљен у оквиру .NET Framework 2.0. Не препоручује се за развој савременог
софтвера, јер за њега више није обезбеђена подршка. Према њему, дефинишу се парови
методе и пратећег догађаја. Метода се обично завршава суфиксом Async, а повратна вредност
јој је void, по чему се најлакше разликује од TAP метода које је могуће асинхроно сачекати.
Евентуално, иницира догађај са суфиксом Completed. Параметри тог догађаја би требало да
потичу од AsyncCompletedEventArgs.

Ипак, постоји и начин да се асинхроно сачека извршење постојеће EAP методе. Коришћењем
TaskCompletionSource<T> може се окружити EAP изведба и прилагодити TAP узору асинхроног
извршавања.

2.2.1.3 Модел асинхроног програмирања (Asynchronous Programming Model – APM)


Модел асинхроног програмирања је познат и под називом IAsyncResult pattern. Такође је
застарео узор асинхроног програмирања. По заступљености је први после TAP асинхроног
узора. Основна карактеристика по којој је овај узор препознатљив је операција
имплементирана паром метода са префиксима Begin и End. Притом, Begin метода прима
уобичајене параметре, као и додатне AsyncCallback и object параметре, a враћа IAsyncResult.
End метода као улазни параметар има само IAsyncResult параметар, а враћа резултујућу
вредност уколико постоји.

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-ова који дефинишу распон над којим се у оквиру оригиналне колекције итерира.

Иако TPL поједностављује вишенитно програмирање и заобилази проблеме конкурентности на


нижем нивоу, потребно је разумевање основних концепата какви су застоји, закључавање нити
и опасност од трке за приступ дељеним подацима.

11
2.2.3 Вишенитно програмирање
Вишенитно програмирање у ужем смислу је конкурентно програмирање на нижем нивоу
апстракције. Како би тема била јаснија, најпре је потребно разјаснити појмове процеса и нити.
Процес је један програм у извршавању, док је нит основна јединица којој оперативни систем
додељује процесорско време. Свака нит има приоритет извршавања и одржава сет података,
тзв. контекст нити (thread context), које систем користи при смени нити, када се једна нит
паузира, а друга наставља са извршавањем – регистре и стек. Све нити једног процеса деле
исти виртуелни адресни простор, што значи да нит може да изврши било који део к ôда,
укључујући и делове тренутно коришћене од стране друге нити.

Док вишенитно програмирање решава проблеме протока и респонзивности, узрокује и нове


проблеме – опасност од трке за приступ (race conditions) и застоја (deadlocks). Већи број нити
може захтевати дељени ресурс. Како дељени ресурс не би био доведен у невалидно стање,
потребно му је осигурати симултани приступ. Кôд који се извршава без синхронизације над
дељеном променљивом резултује трком за приступ променљивој, а затим и потенцијалним
нарушавањем интегритета дељеног податка.

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

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

Важно је напоменути и да локалне променљиве, најчешће, не захтевају синхронизацију. Разлог


за то је чињеница да се чувају на стеку, а свака нит има своју логички стек. Подразумевано,
локалне променљиве се не деле између позива метода, а такође ни између нити. Наравно, то
не значи да за њих не важе проблеми конкурентног приступа податку. Дешава се да нпр.
паралелна петља дели променљиву између итерација и излаже је опасности од трке за пруступ
и неинтегритета крајње вредности променљиве.

Кôд или податак синхронизован за симултани приступ назива се thread-safe, односно


безбедним за вишенитни приступ. Са друге стране, цена синхронизације је губитак на
перформансама.

12
3 Имплементациони идиоми конкурентног програмирања
Имплементациони идиоми ће у наставку бити обрађени по сегментима у оквиру којих се
примењују. Сваки идиом ће пратити уобичајену структуру узора описану у поглављу 1.2 Општа
структура програмског узора, а то су четири елемента – конкретан и препознатљив назив
идиома, дефиниција проблема са условима који морају бити испуњени, решење проблема и
коментар о последицама примене идиома.

3.1 Имплементациони идиоми асинхроног програмирања


У овом одељку, тема су имплементациони идиоми природно асинхроних операција које
започну једном и заврше се једном. Такви су HTTP захтеви, команде над базом података и
позиви веб сервиса.

У наставку ће бити детаљно обрађени имплементациони идиоми основних механизама рада


са await и async командама асинхроног програмирања, рада са групом Task-ова, обрадом
грешака и нешто специфичнијим функционалностима какво је праћење напретка асинхроног
извршавања.

Посебан одељак намењен је обради имплементационих идиома рада са асинхроним


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

Детаљно ће бити разрађен концепт прекидања извршавања изазивањем прекида из спољног


кôда, као и реаговања на изазивање прекида. Такође, покривена је и област прекидања
извршавања услед истека дозвољеног времена.

Последња подобласт, али не и мање важна, је област модуларног тестирања асинхроног


извршавања применом различитих оквира за тестирање.

13
ИИ-1 Избегавање оригиналног контекста при наставку

Проблем

Када се након await команде изврши асинхрона метода, подразумевано ће се вратити на


оригинални контекст. Проблем може да настане када је оригинални контекст UI контекст и
када велики број асинхроних операција наставља своје извршавање у оквиру њега.

Решење

Како би се избегло враћање на оригинални контекст, асинхроно се чека резултат


ConfigureAwait-а коме је прослеђена false вредност параметра continueOnCapturedContext .

async Task NastaviNaKontekstuAsync()


{
await Task.Delay(TimeSpan.FromSeconds(1));
}

Коментар

Потребно је проценити које асинхроне операције због међузависности са елементима


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

14
ИИ-2 Паузирање одређени период времена

Проблем

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

Решење

Тип Task садржи статичку методу Delay која враћа Task који се доврши након одређеног
времена. Овај пример приказује Task који ће се извршити асинхроно након три секунде
асинхроног чекања.

Једна од стратегија примене паузирања одређени период времена је експоненцијална


задршка (exponential backoff). Она подразумева продужавање периода између покушаја
приступа. То је нарочито корисно како би се веб сервис заштитио од преплављености
захтевима. У следећем примеру чекање између покушаја се са једне секунде повећава на две,
а са две на четири.

async Task<string> PreuzmiStringSaPokusajima(HttpClient client, string uri)


{
TimeSpan sledeceCekanje = TimeSpan.FromSeconds(1);

for (int i = 0; i != 3; ++i)


{
try
{
return await client.GetStringAsync(uri);
}
catch (Exception)
{
}

await Task.Delay(sledeceCekanje);
Task.Delay се користи и као стратегија једноставног истека времена. Иако је препоручени
начин имплементације истека времена CancellationTokenSource, слично је могуће урадити и
на следећи начин. Креирају се два Task-а. Један који асинхроно извршава жељену операцију, а
други који асинхроно чека бесконачан период времена. Њему се проследи CancellationToken
којим је дефинисано колико дуго ће се чекати на одзив. У следећем примеру то су три секунде.
Чека се на завршетак бар једног од та два Task -а. Уколико се у оквиру три секунде не преузме
одговор, десиће се истек времена. Међутим, разлика у односу на прави механизам истека
времена који омогућава CancellationTokenSource је да ће резултат у овом случају свакако

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

async Task<string> PreuzmiStringSaIstekom(HttpClient client, string uri)


{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));

Task<string> downloadTask = client.GetStringAsync(uri);


Task timeoutTask = Task.Delay(Timeout.InfiniteTimeSpan, cts.Token);

Task completedTask = Task.WhenAny(downloadTask, timeoutTask);

if (completedTask == timeoutTask)
{
return null;
}

Коментар

Task.Delay се препоручује у модуларном тестирању или у имплементацији вишеструких


покушаја, док је за истек времена боље решење коришћење CancellationToken механизма
који у потпуности прекида извршење операције.

16
ИИ-3 Чекање завршетка групе Task-ова

Проблем

Потребно је сачекати да се заврши извршавање сваког Task-а из групе Task-ова.

Решење

Статичка метода WhenAll класе Task омогућава чекање завршетка групе Task-ова.
Функционише тако што враћа Task који се завршава када се заврши сваки од Task-ова из
прослеђене групе.

async Task SacekajGrupuTaskova()


{
Task task1 = Task.Delay(TimeSpan.FromSeconds(1));
Уколико сви Task-ови из групе враћају вредност истог типа и сви се успешно заврше, метода
WhenAll ће вратити низ вредности које су биле резултат појединачних од Task-ова из групе.

async Task SacekajRezultateGrupeTaskova()


{
Task<string> task1 = Task.FromResult("prvi");
Метода WhenAll има и варијанту која као улазни параметар прима IEnumerable колекцију
Task-ова. Она такође враћа низ резултата извршења.

Коментар

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

17
Када се чека извршење групе, уколико се асинхроно чека Task.WhenAll са бар једном грешком,
биће бачена само прва ухваћена грешка.

async Task ObradiPrvuGresku()


{
Task task1 = BaciPrvuGreskuAsync();
Task task2 = BaciDruguGreskuAsync();

try
{
await Task.WhenAll(task1, task2);
Међутим, уколико нас занимају све грешке које су се догодиле на групи која се извршавала,
оне су смештене у Task-у који враћа Task.WhenAll у атрибуту Exception.

async Task ObradiSveGreske()


{
Task task1 = BaciPrvuGreskuAsync();
Task task2 = BaciDruguGreskuAsync();

Task allTasks = Task.WhenAll(task1, task2);

try
{
await allTasks;
}

18
ИИ-4 Чекање завршетка било ког Task-а из групе

Проблем

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

Решење

Статичка метода WhenAny класе Task прима као улазне параметре групу Task-ова, а враћа Task
који се завршава када се заврши бар један од прослеђених Task-ова из групе. Необично је то
да је резултат враћеног (“спољашњег”) Task-а, заправо (“унутрашњи”) Task који се први
извршио.

async Task<int> SacekajBiloKojiTask(HttpClient client, string urlA, string


{
Task<byte[]> downloadTaskA = client.GetByteArrayAsync(urlA);
Task<byte[]> downloadTaskB = client.GetByteArrayAsync(urlB);

Task<byte[]> firstCompletedTask = await Task.WhenAny(downloadTaskA,

Коментар

Такозвани “спољашњи” Task који враћа метода WhenAny увек ће се завршити успешно. Чак и
када “унутрашњи” Task има грешку она неће бити прослеђена на “спољашњи” Task. Да бисмо
се уверили да није било грешака на Task-у који се први извршио, потребно је да га асинхроно
сачекамо.

Када се изврши неки од Task-ова из прослеђене групе, треба да одлучимо шта ће се догодити
са онима који се још нису извршили. Уколико их ни не чекамо нити отказујемо, они остају
напуштени. То значи да ће њихови резултати, као и могуће грешке које су настале током
њиховог извршења, бити игнорисани. Некада постоји потреба да се ти остали Task-ови након
извршења првог откажу, како не би непотребно трошили ресурсе. Није препоручљиво да се у
оквиру групе Task-ова смести и Task.Delay који би дефинисао истек времена, јер не би у
суштини прекинуо извршење. Боље решење је коришћење Cancellation стратегије.

19
ИИ-5 Обрада сваког Task-а из групе у редоследу извршавања

Проблем

Потребно је да се за сваки Task из групе изврши одређена обрада одмах након што Task буде
завршен. Уколико се извршава група Task-ова, важно је да се не обрађују редоследом којим су
посложени у групи, већ редоследом којим се завршавају.

async Task ObradiRedosledomPokretanja()


{
Task<int> taskA = OdloziIVratiVrednost(2);
Task<int> taskB = OdloziIVratiVrednost(3);
Task<int> taskC = OdloziIVratiVrednost(1);

Task<int>[] tasks = new[] { taskA, taskB, taskC };

foreach (var task in tasks)


На овај начин, Task-ови из групе чије извршавање подразумева асинхроно чекање, редом, две,
три и једну секунду, тако би били и обрађени. Међутим, циљ је да буду обрађени у редоследу
извршавања. Дакле, прво Task који има чекање од једне секунде, затим онај са две, а
последњи – Task са три секунде асинхроног чекања.

Решење

Обрада Task-ова у редоследу извршавања може се постићи реструктурирањем претходног


кôда, тако што ће се извршавање и обрада након завршетка радити асинхроно.

20
async Task ObradiRedosledomZavrsetka()
{
Task<int> taskA = OdloziIVratiVrednost(2);
Task<int> taskB = OdloziIVratiVrednost(3);
Task<int> taskC = OdloziIVratiVrednost(1);

Task<int>[] tasks = new[] { taskA, taskB, taskC };

Task[] processingTasks = tasks.Select(async t =>


{
int delayValue = await t;

Коментар

Разлика у односу на почетни кôд је што се обрада Task-ова врши конкурентно, а не обрађује се
један у једном тренутку. На тај начин је постигнуто да се Task обради одмах након што се
заврши, без обзира на свој редослед у групи која се извршава.

21
ИИ-6 Обрада грешака насталих у async Task методама

Проблем

Потребно је обезбедити обраду грешака које се догоде при извршавању асинхроних метода.

Решење

Када се у оквиру async Task методе догоди грешка, она се смешта на Task који је повратна
вредност те методе.

Грешку је могуће ухватити и обрадити помоћу try-catch блока, као када се обрађују грешке
синхроног кôда.

async Task TestAsync()


{
try
{
await BaciGreskuAsync();
Грешке се поново бацају само када се асинхроно сачека Task који је повратна вредност
асинхроне методе.

async Task TestPovratniTaskAsync()


{
Task task = BaciGreskuAsync();

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.

sealed class MojaAsyncCommand : ICommand


{
async void ICommand.Execute(object parameter)
{
await Execute(parameter);
}

public async Task Execute(object parameter)


{
try
{
await Task.Delay(TimeSpan.FromSeconds(1));
throw new InvalidOperationException("Test");
}
catch (Exception)
{
}
}

#region Ostali elementi

public event EventHandler CanExecuteChanged;

public bool CanExecute(object parameter)


{
Најбоље је не дозволити бацање грешака изван async void методе. Ако је неопходно
користити async void методу, онда је потребно у оквиру ње обезбедити try-catch блок и
обраду грешака у оквиру њега.

Постоји и други начин за решавање овог проблема. Када async void метода баци грешку, та
грешка је онда бачена у оквиру SynchronizationContext-а који је био активан када је почело
извршавање async void методе. Уколико извршно окружење омогућава
SynchronizationContext, онда оно често омогућава и обраду грешака глобално, на вишем
нивоу. Нпр. WPF пружа Application.DispatcherUnhandledException. Постоје одређене

24
библиотеке које омогућавају управљање SynchronizationContext-ом или имитирање
SynchronizationContext-а код апликација које немају уграђен, као што је случај код конзолних
апликација.

Коментар

Разлог зашто се препоручује употреба async Task метода пре него async void метода је
олакшано тестирање. Чак и креирање варијанти void метода заснованих на Task повратној
вредности у некој мери пружа могућност тестирања.

Уколико се инсталирају библиотеке које се баве SynchronizationContext-ом, важно је пазити


да се SynchronizationContext не инсталира на нитима које нам не припадају, као ни на нитима
које већ имају свој или су threadpool нити. Нит конзолне апликације или нити које сами
креирамо припадају нама.

25
ИИ-8 Креирање ValueTask-а

Проблем

Потребно је имплементирати методу чија је повратна вредност ValueTask<T>.

Решење

Опште правило је да се као повратна вредност асинхроне методе користи Task<T>. Када
асинхрона метода значајно дуго траје, трошак креирања Task<T> повратне вредности је
занемарљив. Међутим, некада је асинхрона операција која се иницира краткотрајна и
моментално враћа резултат. У тим случајевима не желимо Task<T> као повратну вредност
асинхроне методе, јер нема много смисла када се асинхрона активност одмах завршава. Тада
нам је потребан тип који је сличан типу Task и који се може асинхроно сачекати, а притом не
захтевати трошак креирања непотребне целокупне Task<T> повратне вредности.

Појавом C# 7.0 представљен је ValueTask<T>. Он би требало да буде употребљен као повратна


вредност само када алат за процену перформанси покаже да би се тиме постигло побољшање
перформанси. ValueTask<T> се користи као повратна вредност када постоји синхрони резултат
који може да се врати, а асинхроно понашање је доста ређе. У таквој методи се async и await
користе као и у било којој другој асинхроној методи.

Често је асинхрона метода одмах спремна да врати ValueTask<T> резултат. У том случају,
може се извршити оптимизација, коришћењем конструктора ValueTask<T>. У супротном
наставља се са асинхроном методом само ако је потребно .

public ValueTask<int> MetodaAsync()


{
if (MozeSePonasatiSinhrono)
{
return new ValueTask<int>(13);
}
Пример за употребу овог имплементационог идиома је имплементација IAsyncDisposable, где
се асинхрона логика извршава само једном, док се сваки следећи пут метода DisposeAsync
извршава успешно и синхроно.

26
public class MyAsyncDisposable : IAsyncDisposable
{
private Func<Task> _disposeLogic;

public ValueTask DisposeAsync()


{
if (_disposeLogic == null)
{
return default;
}
else
{
Func<Task> _logic = _disposeLogic;

Коментар

Препоручљиво је да већина метода које пишемо враћа Task<T>, јер таква пракса ствара мање
замки при употреби таквих метода него што је случај са ValueTask<T> повратном вредношћу.
Уколико се имплементира неки интерфејс који користи ValueTask<T> и ValueTask, онда је
најчешће могуће користити async и await. Међутим, када сами имплементирамо методе са
таквим повратним вредностима, потребна је нешто напреднија имплементација.

27
ИИ-9 Употреба ValueTask-а

Проблем

Потребно је имплементирати употребу методе са повратном вредношћу ValueTask<T> или


ValueTask.

Решење

Асинхрону методу чија је повратна вредност ValueTask<T> можемо употребити на више


начина, на начине на које употребљавамо Task<T>, али и свођењем ValueTask<T> на Task<T>.

Најчешћи начин за употребу ValueTask<T> вредности је једноставно асинхроно чекање са


await.

Такође, могуће је и асинхроно чекање након извршења конкурентне операције, као што се
ради са Task<T>.

Једно од ограничења ValueTask-а је што може бити асинхроно сачекан само једном. У
претходним примерима то ограничење није прекршено.

За комплексније активности, препоручује се конверзија у Task<T> употребом AsTask.

async Task UpotrebaMetodeAsync()


Task<T> који је резултат конверзије дозвољено је асинхроно чекати више пута. Такође, могуће
је урадити и све друго што је могуће са Task<T>, као што је, на пример, асинхроно чекање
извршења групе Task-ова.

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

class SinhronaImplementacija : IAsinhroniInterfejs


{
За методе које немају повратну вредност, може се користити Task.CompletedTask који
означава Task који је успешно завршен.

interface IAsinhroniInterfejs
{
Task VratiVrednostAsync();
}

class SinhronaImplementacija : IAsinhroniInterfejs


{
Task.FromResult подржава креирање завршених Task-ова само за успешне резултате. За
креирање неуспешно завршеног Task-а потребно је користити Task.FromException.

Ако постоји могућност да ће се при синхроном извршавању догодити грешка, неопходно је


ухватити грешке, обрадити их и вратити их помоћу Task.FromException.

30
interface IAsinhroniInterfejs
{
Task UradiAsync();
}

class SinhronaImplementacija : IAsinhroniInterfejs


{
public Task UradiAsync()
{
try
{
UradiNestoSinhrono();
return Task.CompletedTask;
}
catch (Exception ex)
{
return Task.FromException(ex);
}
}

За враћање Task-ова већ прекинутих прослеђеним CancellationToken-ом користи се


Task.FromCanceled.

Task<int> VratiVrednostAsync(CancellationToken token)


{
if (token.IsCancellationRequested)
{
return Task.FromCanceled<int>(token);
}

Коментар

Када се асинхрони интерфејс имплементира синхроно, мора се избегавати сваки облик


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

Логички, Task.FromResult, Task.FromException и Task.FromCanceled су помоћне методе и


пречице ширег механизма TaskCompletionSource<T>, који је тип нижег нивоа. Препорука је да
се за враћање завршених Task-ова користе помоћне методе, док се употреба
TaskCompletionSource<T> препоручује за Task завршен у неком тренутку у будућности.

31
ИИ-11 Извештавање о напретку извршења

Проблем

Потребно је реаговати на напредовање извршења Task-а.

Решење

Користе се доступни IProgress<T> и Progress<T> типови. Асинхрона метода чији напредак


пратимо треба да прими IProgress<T> као параметар. Притом је T тип вредности напретка који
пратимо.

async Task MetodaAsync(IProgress<double> progress = null)


{
bool done = false;
double percentageComplete = 0;

while (!done)
{
//...
progress?.Report(percentageComplete);
}
}

async Task PozoviMetoduAsync()


{
var progress = new Progress<double>();
progress.ProgressChanged += Progress_ProgressChanged;
await MetodaAsync(progress);
}

Коментар

IProgress<T> параметар, по конвеницији, може да буде null, јер методи која асинхрону
методу позива можда није потребно праћење напретка. Због тога је важна провера улазног
параметра који означава напредак.

IProgress<T>.Report метода је обично асинхрона. То значи да ће метода чији се напредак


прати наставити са извршавањем и пре него што извештавање о напретку буде завршено. Због
тога, тип T треба да буде вредносни или непроменљиви (immutable) тип. Ако је тип, ипак,
референтни, потребно је обезбедити одвојену копију сваки пут када се извештава о напретку.

Ако је 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-а, тј. пружа уланчавање између претходника и
последице. То је корисно када неки задаци морају да прате одређени редослед извршавања, а
неки други не.

Task.ContinueWith прихвата као аргумент претходника како би могао да приступи његовом


стању завршетка. Када је претходник завршио са извршавањем, аутоматски почиње асинхроно
извршавање последичног Task-а. Како ContinueWith такође враћа Task, то омогућава
уланчавање са неким следећим задатком итд.

Уколико се ContinueWith позове више пута над оригиналним Task-ом, толико последичних
Task-ова ће бити истовремено асинхроно покренуто над претходником. Редослед њиховог
извршења у време компајлирања није одређен, тако да редослед њиховог извршења може
варирати од једног до другог покретања апликације.

У следећем примеру извршавање почиње исписом почетка, који се без изузетка први
извршава, а наставља се пратећим исписом задатка A. Задаци B и C су последични задаци
задатка A и истовремено се асинхроно покрећу након завршетка задатка A. Редослед исписа
задатка B и задатка C може варирати из горе описаног разлога. Последњи ће се догодити испис
краја извршења јер он асинхроно чека завршетак групе задатака B и C.

async Task MetodaSaUlancavanjemAsync()


{
Console.WriteLine("Before");

Task taskA = Task.Run(() => { Console.WriteLine("Starting"); })


.ContinueWith(ant => { Console.WriteLine("Continuing A"

Task taskB = taskA.ContinueWith(ant => { Console.WriteLine("Continuing B"

Task taskC = taskA.ContinueWith(ant => { Console.WriteLine("Continuing C"

Коментар

Иако се покретање Task-ова у одређеном редоследу коси са концептом паралелизма,


реалност је, ипак, другачија и често захтева извршење по редоследу. Task.ContinueWith пружа

34
велики број варијанти са различитим параметрима којима се контролише понашање. Неке
варијанте примају TaskContinuationOptions вредности које се могу комбиновати логичким OR
оператором.

На тај начин можемо се претплатити на ослушкиваче догађаја оригиналног Task-а који прате
његов статус завршености и у зависности од тога да ли се извршио на предвиђен или
непредвиђен начин, покрећу одговарајући последични Task. Ова могућност је нарочито
корисна када је оригинални Task такав да га само иницирамо, уланчамо са потенцијалним
последичним задацима и, “заборавимо” на њега, тј. не позивамо се на њега поново.

У наредном примеру, регистровани су ослушкивачи догађаја који покривају следеће опције


уланчавања – OnlyOnFaulted, OnlyOnCanceled и OnlyOnRanToCompletion. Оне су међусобно
искључиве, тако да који год последични Task да се извршио, остале потенцијалне последичне
Task-ове моментално прекида и подешава им статус на Canceled. Уколико помоћу
Task.WhenAny(faultedTask, canceledTask, completedTask) сачекамо извршени последични
Task, биће бачена агрегирана грешка која треба да буде обрађена. Она настаје управо из горе
поменутог разлога – поништених преосталих неизвршених последичних задатака.

async Task MetodaAsync(CancellationToken token)


{
await Task.Delay(TimeSpan.FromSeconds(3), token);
}

async Task ReakcijaNaStatusUlancanoAsync()


{
CancellationTokenSource src = new CancellationTokenSource();
Task task = MetodaAsync(src.Token);

Task faultedTask = task.ContinueWith((ant) =>


{
Console.WriteLine("Task state: Faulted");
}, TaskContinuationOptions.OnlyOnFaulted);

Task canceledTask = task.ContinueWith((ant) =>


{
Console.WriteLine("Task state: Canceled");
}, TaskContinuationOptions.OnlyOnCanceled);

Task completedTask = task.ContinueWith((ant) =>


{
Console.WriteLine("Task state: Completed");
}, TaskContinuationOptions.OnlyOnRanToCompletion);

Task resultTask = await Task.WhenAny(faultedTask, canceledTask, completedTask);

try
{
await resultTask;

35
ИИ-13 Започињање дуготрајног Task-а

Проблем

Потребно је обавестити распоређивача задатака да је очекивано да извршење Task-а дуго


траје и да би самим тим на дуже време заробило ресурсе нити.

Решење

Threadpool претпоставља да ће процесорски-зависни задаци релативно кратко трајати и на


основу тих претпоставки планира број нити које се креирају. Тиме штеди на алокацији ресурса
и спречава потенцијално неконтролисано претплаћивање на процесорско време, смену
контекста и поделу процесорског времена. Како би распоређивач могао погодније да
распореди ресурсе, програмер му може сугерисати да се од задатка који асинхроно покреће
може очекивати дуго трајање. То се постиже на следећи начин, коришћењем опције
TaskCreationOptions.LongRunning при позиву методе Task.Factory.StartNew:

void DugotrajnaMetoda(CancellationToken token)


{
//...
}

async Task ZapocniDugotrajniTaskAsync(CancellationToken token)


{

Коментар

Два су главна ефекта примене TaskCreationOptions.LongRunning опције. Први је тај да се


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

Ипак, да не би дошло до нежељених ефеката, оваква могућност се мора користити опрезно и


штедљиво.

36
3.1.1 Асинхрони токови
Асинхрони токови (asynchronous streams) су начин да се асинхроно прихвати и обради више
објеката. Они су засновани на асинхроном енумерационом типу IAsyncEnumerable<T>. То је
асинхрона верзија енумерационог типа и омогућава преузимање објеката асинхроно, једног
по једног, на захтев, а затим и њихову асинхрону обраду. У наставку ће равноправно бити
коришћени изрази асинхрони ток и асинхрона колекција.

Најпре је потребно разјаснити потребу која је довела до увођења асинхроних колекција у


верзији C# 8.0. Рецимо да асинхрона метода треба да врати колекцију вредности. Када би се
итератор градио помоћу yield return, метода би требало да враћа повратну вредност типа
IEnumerable<T>. Ипак, та повратна вредност није асинхрона, тако да је није могуће асинхроно
чекати. Из тог разлога су настали асинхрони токови као начин да се истовремено обезбеде и
могућност асинхроног извршавања и могућност итерирања над колекцијом.

У наредној табели представљене су разлике употребе асинхроних токова и других сличних


типова, који су у одређеним ситуацијама погоднији.

37
Тип повратне Повратна
Карактеристике Push/Pull
вредности вредност
T Појединачна Синхроно враћање појединачне вредности.
Синхроно

IEnumerable<T> Вишеструка Синхроно враћање колекције вредности.

Task<T> Појединачна Погодно само за враћање појединачне вредности. Након што се Task<T> заврши, враћа
се једнострука T вредност. Чак и када је T колекција, вредност се враћа само једном.
Task<IEnumerable<T>> Вишеструка Иако је повратна вредност вишеструка и враћа се асинхроно, враћа се тек када цела
колекција буде спремна за враћање.
IAsyncEnumerable<T> Вишеструка Асинхроно враћање колекције вредности. Разликује се у односу на IEnumerable<T> у
томе што не блокира UI нит приликом узимања сваког објекта из колекције.
Асинхроно

IObservable<T> Појединачна Observable колекције су права подршка асинхроном извршавању. Не узрокују


или блокирање. Међутим, употреба овакве колекције захтева потпуно другачији приступ
вишеструка него употреба IAsyncEnumerable<T>. Користи се System.Reactive да би се
имплементирао ток који започиње са захтевима када се на њега претплати и који враћа
објекте један по један, сваки непосредно након обраде. Сва обавештења у
System.Reactive су синхрона, тако да чим једно обавештење буде послато
претплаћеним објектима, наставља се са извршењем над следећим објектом из
колекције. Главна разлика између IAsyncEnumerable<T> и IObservable<T> је што је
IObservable<T> push типа тј. шаље обавештења у кôд, а IAsyncEnumerable<T> pull типа тј.
Табела 1 Каректеристике типова повратних вредности синхроног и асинхроног програмирања у C#.NET-у

38
ИИ-14 Креирање асинхроних токова

Проблем

Потребно је вратити више вредности (нпр. IEnumerable<T>) од којих свака захтева додатну
асинхрону обраду.

Решење

Код асинхроних токова, могуће је комбиновати враћање вишеструких вредности из методе


помоћу yield return и асинхроних метода које користе async и await. Повратна вредност
треба да буде IAsyncEnumerable<Т>.

У наредном примеру се асинхроно враћа колекција укупно 10 вредности од којих свака пре
израчунавања свог квадрата захтева додатну асинхрону обраду.

async IAsyncEnumerable<int> VratiKvadrateVrednostiAsync()


{
for (int i = 1; i <= 10; i++)
Као и у свакој другој асинхроној методи, и при креирању асинхроног тока је дозвољено
асинхроно чекање већег броја асинхроних метода, тј. употреба више await команди.

async IAsyncEnumerable<int> VratiVrednostiAsync()


{
await Task.Delay(1000); //neka asinhrona obrada
Пример који се чешће среће у пракси је асинхрони пролазак кроз петљу резултата позива API
функције која користи параметре за страничење.

async IAsyncEnumerable<string> VratiVrednostiAsync(HttpClient client)


{
int offset = 0;
const int limit = 10;

39
//Kreiraj rezultate za ovu stranicu
foreach (string value in valuesOnThisPage)
{
yield return value;
}

//Ako je to poslednja stranica, gotovo je


if (valuesOnThisPage.Length != limit)
{
break;
}
else //Nastavi sa sledecom stranicom
Када VratiVrednostiAsync почне са извршавањем, асинхроно захтева прву страницу резултата
и креира први елемент. Када други елемент буде захтеван, метода га одмах креира, јер је
преузет у истој страници резултата. Сваки следећи, до свих 10 елемената, такође се налази у
тој страници. Када се захтева 11. елемент, прелази се у другу итерацију while петље и
асинхроно преузима друга страница резултата.

Коментар

Уобичајени узор везан за асинхроне токове је да је веома малом броју објеката потребна
асинхрона обрада. Већина итерација се одвија синхроно. У претходном примеру се асинхрона
обрада одвија једном на сваких 10 елемената, када је потребно асинхроно преузети следећу
страницу резултата.

40
ИИ-15 Употреба асинхроних токова

Проблем

Потребно је употребити вредности добијене из асинхроног тока, тј. асинхроне колекције.

Решење

Употреба асинхроног тока се ради комбиновањем await и foreach петље. Када метода
VratiKvadrateVrednostiAsync врати асинхрону колекцију, креира се асинхрони енумератор.
Разлика таквог енумератора у односу на уобичајени је у томе што прелазак на следећи
елемент колекције може бити асинхрони, тј. догодити се тек када следећи елемент пристигне.
Уколико следећи елемент пристигне, извршиће се тело методе, а уколико је крај пролазака
кроз асинхрону колекцију, изаћи ће се из петље. При обради сваког елемента могућа је и
одређена асинхрона обрада елемента. У том случају, на следећи елемент ће се прећи тек када
тело петље за тај елемент буде извршено.

async Task ObradiKvadratVrednostiAsync()


{
ΙΙ14 helper = new ΙΙ14();

await foreach (var val in helper.VratiKvadrateVrednostiAsync())


У комбинацији await foreach је сакривено await за асинхроно чекање следећег елемента
асинхроне колекције. И при употреби асинхроних токова могуће је избегавање оригиналног
контекста са ConfigureAwait(false).

async Task ObradiKvadratVrednostiAsync()


{
ΙΙ14 helper = new ΙΙ14();

await foreach (var val in helper.VratiKvadrateVrednostiAsync().ConfigureAwait(

Коментар

Иако је могуће и природно користити await foreach за пролазак кроз асинхрону колекцију,
доступна је и исцрпна библиотека са LINQ операторима који се могу користити у ту сврху. Као
што је већ напоменуто, доступно је и избегавање оригиналног контекста, али и прекидање
обраде асинхроних токова прослеђивањем CancellationToken-а.

Осим асинхроног чекања на преузимање следећег елемента, await foreach генерише и


асинхроно чекање на уклањање асинхроне колекције.

41
ИИ-16 Употреба LINQ са асинхроним токовима

Проблем

Потребна је обрада асинхроних токова употребом проверених LINQ оператора.

Решење

Као што IEnumerable<T> има подршку за LINQ над објектима, тако и IAsyncEnumerable<T> има
подршку доступну у оквиру NuGet пакета System.Linq.Async. Доступни су Where, Select,
SelectMany, Join и бројни други оператори. Већина LINQ оператора, као што је WhereAwait који
ће бити обрађен у наставку, сада прихвата и асинхроне делегате.

Једна од главних недоумица везаних за употребу LINQ са асинхроним токовима је како


користити Where оператор. Он је доступан за синхроне услове и као резултат његове примене
над асинхроном колекцијом добија се, такође, асинхрона колекција.

//vraca asinhronu kolekciju koja usporava izmedju svaka dva elementa


async IAsyncEnumerable<int> UsporavajucaKolekcija()
{
for (int i = 1; i <= 10; i++)
{
await Task.Delay(i * 100);
yield return i;
}
}

async Task VratiVrednostiLINQAsync()


{
IAsyncEnumerable<int> values = UsporavajucaKolekcija().Where(value => value % 2 == 0);

Међутим, питање је како користити Where оператор када је Where израз асинхрон. Конкретно,
како користити овај оператор када за сваки елемент треба асинхроно проверити, нпр. у бази,
да ли треба да буде уврштен у резултат.

Одговор је да се Where оператор не може користити са асинхроним изразима, зато што од свог
делегата очекује тренутан, синхрони одговор. За овакав случај погодан је WhereAwait оператор.

async IAsyncEnumerable<int> UsporavajucaKolekcija()


{

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() и тиме је претворити у
асинхрону. Над асинхроном колекцијом онда можемо користити такве операторе.

Важно је напоменути и начин именовања LINQ оператора за асинхроне токове. Неки


оператори имају суфикс Async док се други завршавају са Await.

Async оператори извлаче из колекције одређену вредност или обављају неку израчунавајућу
операцију над објектима колекције и враћају скаларну вредност коју треба асинхроно
сачекати. Пример таквог оператора је CountAsync који враћа појединачну нумеричку вредност
броја објеката колекције који задовољавају одређени услов.

Са друге стране, специфичност Await оператора је да примају асинхрони делегат и да


асинхроно чекају над делегатом који им се проследи, као што је објашњено у примеру
WhereAwait оператора.

Уколико оператор истовремено има могућност и да прими асинхрони делегат и да врати


коначну скаларну вредност, онда његов назив садржи оба суфикса – Await и Async. Пример за
то је CountAwaitAsync оператор који враћа скаларну вредност броја елемената асинхроне
колекције који задовољавају услов који садржи и одређену асинхрону проверу елемената.

43
ИИ-17 Прекид асинхроних токова

Проблем

Потребно је прекинути асинхрони ток.

Решење

Уколико је потребно прекинути пролазак кроз асинхрону колекцију када се задовољи


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

async IAsyncEnumerable<int> UsporavajucaKolekcija()


{
for (int i = 1; i <= 10; i++)
{
await Task.Delay(i * 100);
yield return i;
}
}

async Task PrekiniIznutraAsync()


{
await foreach (int result in UsporavajucaKolekcija())
{
Console.WriteLine(result);
Уколико је, са друге стране, потребно да се из спољног кôда прекине await foreach над
асинхроном колекцијом, користи се прекид употребом CancellationToken-а. Асинхрона
метода која враћа асинхрону колекцију прима CancellationToken означен атрибутом
EnumeratorCancellation, као улазни параметар. Он се затим прослеђује природно другим
методама које примају CancellationToken-е, као што је метода Task.Delay у следећем
примеру.

async IAsyncEnumerable<int> UsporavajucaKolekcija(


[EnumeratorCancellation] CancellationToken token = default)
{
for (int i = 1; i <= 10; i++)

44
async Task PrekiniSpoljaAsync()
{
using var cts = new CancellationTokenSource(500);
CancellationToken token = cts.Token;

Коментар

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


напоменути да није асинхрона колекција та коју је могуће прекинути, већ енумератор над
њом. Управо из тог разлога постоји проширена метода WithCancellation асинхроних токова.
Помоћу ње, CancellationToken се може проследити специфичним енумераторима над
асинхроном колекцијом, уместо читавој асинхроној колекцији. Навођењем
EnumeratorCancellation атрибута, компајлеру се сигнализира да брине о томе да токен из
WithCancellation проширене методе буде прослеђен одговарајућем улазном токену
асинхроне колекције. Захтев за прекид асинхроног тока, затим, узрокује појаву
OperationCanceledException изузетка у оквиру await foreach, након што је обрађено првих
неколико елемената тока.

45
3.1.2 Прекид извршавања
Подршка за прекид извршавања уведена је у .NET 4.0 оквиру. Исцрпна је и добро
имплементирана. Омогућава да прекид извршавања буде захтеван из спољног кôда, али
никако насилно изазван над кôдом. Такође, уколико кôд није написан тако да подржи прекид,
прекид над њим неће бити могућ.

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

Метода која подржава прекид прима CancellationToken као улазни параметар, како би
позивајућем кôду назначила подржану могућност прекида. Уколико метода не захтева прекид,
потребно је или подесити подразумевану вредност улазног токена на default или обезбедити
и варијанту методе која не прима токен као параметар.

//metoda sa podrazumevanom vrednoscu tokena


void PrekidnaMetodaSaPodrazumevanimTokenom(CancellationToken token = default
{
//...
}

//metoda sa obaveznim ulaznim tokenom


void PrekidnaMetodaSaObaveznimTokenom(CancellationToken token)
{
//...
}

CancellationToken.None који је
прослеђен у претходном примеру је еквивалент
подразумеване вредности CancellationToken-а и означава токен који никада не може бити
прекинут.

46
ИИ-18 Изазивање прекида извршавања

Проблем

Потребно је изазвати прекид извршавања методе која подржава прекиде.

Решење

CancellationTokenSource, који је извор сигнала прекида, само омогућава да се прекид


изврши. Његов атрибут Token враћа одговарајући CancellationToken за тај извор, а
припадајућа Cancel метода заправо изазива прекид извршавања.

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


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

У наставку је, најпре, приказан пример асинхроне методе која подржава прекид, а затим и
асинхроне методе у оквиру које се тај прекид изазива.

Када се прекид изазове могућа су три исхода извршавања методе у оквиру које је прекид
изазван. Први исход може бити у потпуности успешно извршење, уколико се читава
функционалност извршила пре него што је дошло до прекида из спољног кôда. Други могући
исход је прекид извршавања услед захтеваног прекида. Такође, може се догодити и трећи
исход у ком се није догодило ништа везано за ручно изазивање прекида, већ се извршавање
прекинуло услед неочекиване грешке пре изазивања прекида.

async Task IzazoviPrekidAsync()


{
using var cts = new CancellationTokenSource();

Task task = PrekidnaMetodaAsync(cts.Token);

//izvrsavanje uspesno traje

//izazvan prekid
cts.Cancel();

47
catch (OperationCanceledException ex)
{
//izvrsavanje prekinuto na zahtev, pre kraja izvrsavanja
}
catch (Exception ex)
CancellationTokenSource је могуће прекинути само једном. Уколико је поново потребан након
што је прекид захтеван, неопходно је креирати нову инстанцу ове класе и помоћу ње захтевати
прекид.

Коментар

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


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

48
ИИ-19 Реаговање на захтев прекида периодичном провером

Проблем

Потребно је у оквиру петље реаговати на прекид извршавања.

Решење

Када у кôду имамо петљу која врши одређену обраду, онда не постоји асинхрона метода на
нижем нивоу којој се може проследити CancellationToken. Уколико, ипак, морамо да
прекинемо њено извршавање на захтев позивајућег кôда, постоји начин да реагујемо на
прекид. Периодичном провером улазног CancellationToken-а, утврђујемо да ли је од
претходног проласка кроз петљу до тренутног – изазван прекид.

У следећем примеру, периодично проверавамо да ли треба да реагујемо на прекид


извршавања петље са рачунском операцијом. У раду са прекидом петље важно је размотрити
утицај написаног кôда на перформансе извршавања. Потребно је анализирати однос броја
пролазака кроз петљу и захтевност сваког проласка, како бисмо утврдили да ли је потребно да
се у сваком проласку проверава захтев за прекид. Уколико би провера у сваком проласку
негативно утицала на брзину извршавања, може се размислити о провери на сваких неколико
пролазака. Приказан је кôд петље са великим бројем пролазака од којих сваки изузетно кратко
траје. Из тог разлога је уведена провера захтева за прекид на сваких 1000 пролазака.

int PrekidnaMetoda(CancellationToken token)


{
for (int i = 0; i <= 100000; i++)
{
Thread.Sleep(1);

if (i % 1000 == 0)

Коментар

CancellationToken осим методе ThrowIfCancellationRequested(), као показатељ захтеваног


прекида, има и атрибут IsCancellationRequested који након изазваног прекида има вредност
true. Некада програмери реагују на његову позитивну вредност и тиме обрађују прекид, али то
није препоручљиво, јер одступа од узора прекида извршења. Прави начин за обраду прекида
је подизање OperationCanceledException грешке која означава прекинуто извршавање.

49
ИИ-20 Прекид услед истека времена

Проблем

Потребно је прекинути извршавање након истека одређеног времена.

Решење

Концепт прекида извршавања у случају прекида услед истека времена ни по чему се не


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

Постоје два уобичајена начина изазивања прекида услед истека времена, коришћењем типа
CancellationTokenSource. Најчешћи начин је прослеђивање бројача времена као параметра
CancellationTokenSource-а.

async Task IzazoviIstekVremena()


{
Међутим, није ретка ситуација да већ постоји инстанца CancellationTokenSource-а којој треба
доделити бројач времена. Тада се над њом позива метода CancelAfter са бројачем времена
као параметром.

async Task IzazoviIstekVremena()


{
using var cts = new CancellationTokenSource();

Коментар

Приказани начини изазивања прекида су ефикасни и лаки за примену. Ипак, важно је обратити
пажњу на то да кôд који се прекида, подржава прекиде. Није могуће прекинути кôд који није
прекидан.

50
ИИ-21 Прекид асинхроног извршавања

Проблем

Потребно је подржати прекид асинхроног извршавања.

Решење

Најједноставнији начин подржавања прекида асинхроног кôда је прослеђивање


CancellationToken-а кроз слојеве кôда. Често асинхроне методе из референцираних
библиотека подржавају прекиде. Тада и асинхрона метода коју имплементирамо треба да
прихвата CancellationToken као параметар и да га једноставно прослеђује даље свим
позиваним методама који подржавају прекид.

У наставку је пример асинхроне методе која асинхроно чека другу асинхрону методу, а притом
јој прослеђује CancellationToken, како би у потпуности подржала реаговање на прекид
извршавања.

Коментар

Неке методе, ипак, не подржавају прекиде. То није добро, јер се никада не зна која ће метода
са вишег нивоа позвати такву методу, а желети подржаност прекида извршења. Пожељно је да
прекиди буду подржани увек када је то могуће.

Уколико прекиди нису подржани, потребно је импровизовати прекид. Један начин је да позив
такве методе окружимо методом која подржава прекид. Уколико не желимо да радимо на тај
начин, можемо једноставно да “прекинемо” асинхроно извршавање игнорисањем резултата
извршења.

51
3.1.3 Модуларно тестирање асинхроног извршавања
Како бисмо обезбедили да извршавање програма протиче на предвиђен начин, креирамо и
покрећемо тестове. Постоје бројни типови тестова софтвера – интеграцијски тестови, веб
тестови, тестови оптерећења итд. Модуларно тестирање (unit testing) извршавања представља
парадигму тестирања засновану на уситњавању функционалности на мале целине понашања
погодне за тестирање. Оне представљају модуларне јединице (units). Модуларни тестови
тестирају појединачне компоненте и методе и треба да тестирају само кôд који је под
контролом програмера, али не и инфрастуктурне проблеме какви су рад са базом података,
системом датотека или мрежом.

Коришћењем оквира за модуларно тестирање аутоматизује се креирање тестова, покретање и


извештавање о резултатима. Модуларно тестирање треба да буде саставни део свакодневног
процеса развоја софтвера. Тада има оптималан ефекат на квалитет програмског кôда. Саветује
се креирање одговарајућег теста одмах након креирања нове методе. Тестови треба да
проверавају понашање над стандардним, граничним и неодговарајућим улазним подацима.
Затим се прати сваки утицај на кôд који промена улаза изазива. Правац који заговара писање
теста пре саме методе која је предмет тестирања назива се Test Driven Development (TDD).
Намењен је да програмерима омогући писање једноставнијег, читљивијег и ефикасног кôда.

За .NET је доступан разноврстан избор оквира за модуларно тестирање од којих се издвајају


MSTest, NUnit и xUnit. У Visual Studio окружењу се кроз Test Explorer флексибилно и ефикасно
покрећу аутоматски тестови.

Тестирање је итеративан процес анализирања, развоја тестова и реаранжирања кôда (code


refactoring), који поспешује робусност и ефективност кôда. Важно је истаћи да покретање
тестова треба да буде редовно, како би се проблем детектовао пре него што проблематичан
софтвер доспе до корисника. Резултат неуспешног теста увек садржи поруку о неуспеху.
Приказује се шта је било очекивано, а који исход се заправо догодио.

Бројне су предности модуларних тестова. Обезбеђују добар дизајн и пружају документацију


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

Квалитетан модуларни тест карактеришу:

 Брзина – извршавање у милисекундама;


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

52
У процесу тестирања асинхроног извршавања, важно је праћење Task-а који асинхрона метода
враћа. Њиме се враћа информација о завршетку Task-а, резултатима и потенцијално насталим
грешкама. Због тога, неопходно је асинхроно сачекати извршење методе која се тестира. У
супротном, тест би можда прошао као успешан, игноришући грешке настале у оквиру
асинхроне методе. Дакле, мора се обезбедити да тест покаже неуспех, онда када је кôд који је
под тестом неуспешан. Већина савремених оквира за модуларно тестирање подржава
асинхроне модуларне тестове са Task-ом као повратним типом. Асинхрона метода која је под
тестом, а није асинхроно сачекана изазваће упозорење компајлера са ознаком CS4014, којим
се препоручује коришћење await кључне речи за употребу враћеног Task-а. Када се тестна
метода прилагоди асинхроном чекању и потпис јој треба, природно, променити у async Task.

53
ИИ-22 Модуларно тестирање async Task метода

Проблем

Потребно је модуларно тестирати async Task методу.

Решење

Већина савремених оквира за аутоматско тестирање, попут MSTest, NUnit и xUnit оквира,
подржава модуларно тестирање async Task метода. Они препознају када је повратна
вредност методе која се тестира типа Task и чекају на завршетак Task-а пре него што тест
прогласе успешним или неуспешним. Уколико је оквир за тестирање такав да не препознаје
async Task методе, потребно је омогућити неку врсту чекања асинхроне операције која се
тестира. Један начин је коришћење GetAwaiter().GetResult(). Могуће је и коришћење Wait()
методе. Битан недостатак првог начина је што избегава AggregateException у ком би биле
смештене потенцијално настале грешке.

У наставку је пример модуларног MSTest-а async Task методе:

Коментар

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

Симулирање синхроног успеха може се извести употребом Task.FromResult.

class SinhroniUspeh : MojInterfejs


{
Синхрони неуспех се може изазвати коришћењем Task.FromException.

54
class SinhroniNeuspeh : MojInterfejs
{
Изазивање асинхроног понашања може се постићи помоћу Task.Yield, што је нарочито
корисно при модуларном тестирању.

class AsinhroniUspeh : MojInterfejs


{
public async Task<int> UradiNestoAsync()
Како би застоји и престизања при тестирању били откривени, корисно је у пројекат додати
датотеку са подешавањима која омогућава постављање појединачне вредности истека
времена за тест. Подразумевана вредност је прилично висока.

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 void треба заменити са async Task. Метода некада
због архитектуралних захтева, ипак, мора да буде async void. Пример такве ситуације је када
метода мора да имплементира одговарајућу async void методу интерфејса који
имплементира.

У случају обавезне async void методе, прибегава се решењу са две написане методе. Једна од
њих је изворна async void метода која задовољава имплементацију интерфејса. Она притом
асинхроно чека резултат извршавања друге, async Task методе, у оквиру које је смештена
читава логика. Новокреирана метода је погодна за тестирање, а обавезна структура ничим није
нарушена.

public sealed class MojaAsyncVoidCommand : ICommand


{
//obavezna async void implementacija
async void ICommand.Execute(object parameter)
{
await Execute(parameter);
}

//odgovarajuća async Task metoda


public async Task<bool> Execute(object parameter)
{
await Task.Delay(10);
return false;
}

#region Ostali elementi

public event EventHandler CanExecuteChanged;

public bool CanExecute(object parameter)


{
Метода са async Task потписом се затим тестира на уобичајени начин:

[TestMethod]

58
Коментар

Једна од кључних напомена у асинхроном програмирању је избегавање конструкције async


void. Увек је боље извршити рефакторинг проблематичног кôда, него тражити компликовано
неоптимално решење над постојећом структуром.

59
3.2 Имплементациони идиоми паралелног програмирања
У овом поглављу ће бити обрађени имплементациони идиоми којима се постиже
паралелизација обраде података. Аналогно секвенцијалном извршавању петље која итерира
кроз колекцију, при паралелизацији се користе методе Parallel.For и Parallel.ForEach. У
паралелним операцијама колекције се деле на сегменте који се, затим, извршавају
конкурентно, на више нити.

Такође, биће представљена и библиотека Parallel LINQ (PLINQ) која представља проширење
функционалности LINQ-а подршком за паралелну обраду. PLINQ је доста једноставнији за
употребу од класе Parallel, док она са друге стране има своје предности.

Биће приказано агрегирање резултата паралелне обраде са применом закључавања дељене


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

60
ИИ-25 Паралелна обрада података

Проблем

Потребно је процесорски захтевну операцију извршити над сваким елементом из колекције


података.

Решење

Тип Parallel садржи методу ForEach која је намењена управо за овај проблем. У наредном
примеру приказана је метода која врши паралелну ротацију матрица, прослеђених као
параметар, за одређени број степени.

Код прекидања паралелне петље разликују се два појма – заустављање петље које се изазива
унутар петље и прекид петље који се изазива из спољног кôда.

Следећи пример приказује раније заустављање извршавања петље, унутар петље, услед
наиласка на елемент који није валидан. За заустављање је коришћена метода Stop над
ParallelLoopState-ом. Након њеног позива ниједна итерација се не извршава.

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

void InvertujMatrice(IEnumerable<Matrix> matrices)


{
Parallel.ForEach(matrices, (matrix, state) =>
{
if (!matrix.IsInvertible)
{
state.Stop();
}
else
Са друге стране, прекид петље из спољног кôда иницира се изазивањем прекида над
CancellationTokenSource-ом. Улазни CancellationToken параметар се прослеђује паралелној
петљи кроз ParallelOptions.

61
void RotirajMatrice(IEnumerable<Matrix> matrices,
float degrees,
CancellationToken token)
Како би дељено стање између различитих нити на којима се извршавају паралелне операције
било конзистентно, мора се користити неки од механизама закључавања. У наставку је пример
неоптималне, али ефективне употребе закључавања због дељене вредности која представља
број невалидних елемената колекције, а која се мења са различитих нити.

int InvertujMatrice(IEnumerable<Matrix> matrices)


{
object mutex = new object();
int nonInvertibleCount = 0;

Parallel.ForEach(matrices, matrix =>


{
if (matrix.IsInvertible)
{
matrix.Invert();
}
else
{
lock (mutex)
{
++nonInvertibleCount;
}

Коментар

Поред Parallel.ForEach методе која омогућава паралелну обраду колекције вредности,


доступна је и метода Parallel.For која је погодна за пролазак кроз већи број низова података
који заузимају исте индексе. Међутим, аналогно решење пружа и Parallel LINQ (PLINQ) са
сличним скупом могућности, а LINQ синтаксом. Главна разлика ова два механизма је та што
PLINQ подразумева употребу свих језгара процесора, док Parallel динамички реагује на
промену процесорских услова.

62
ИИ-26 Агрегација резултата паралелне обраде података

Проблем

Потребно је агрегирати, тј. сабрати резултате паралелне обраде података. Пример сабирања је
одређивање суме или просечне вредности резултата.

Решење

Класа Parallel омогућава агрегацију кроз концепт локалних вредности. То су променљиве које
постоје локално у оквиру паралелне петље. Њима тело методе приступа директно, без
синхронизације. Када се локални резултати агрегирају, користи се делегат localFinally који
захтева синхронизацију приступа променљивој која означава коначни резултат. У наставку је
пример паралелног збира.

int ParallelSum(IEnumerable<int> values)


{
object mutex = new object();
int result = 0;

Parallel.ForEach(source: values,
localInit: () => 0,
body: (item, state, localValue) => localValue + item,
localFinally: localValue =>
{

Коментар

Ипак, Parallel LINQ (PLINQ) има доступан природнији механизам агрегације резултата
паралелне обраде. Поред општег механизма PLINQ агрегације, могуће је користити и готове
агрегатне операторе какав је Sum. У већини случејева PLINQ библиотека пружа знатно
интуитивнији, краћи и једноставнији кôд у односу на Parallel класу.

63
ИИ-27 Паралелни LINQ

Проблем

Потребно је паралелном обрадом једне колекције креирати другу колекцију или агрегирани
резултат обраде колекције.

Решење

Parallel LINQ (PLINQ) представља проширење функционалности LINQ-а подршком за паралелну


обраду. Погодан је за креирање одговарајуће колекције излаза од изворне колекције улаза.
Интензивна процесорска обрада улазних података је апстрахована једноставним дуплирањем
вредности сваког улазног елемента. Уколико је важно да се у новокреираној, излазној
колекцији резултата елементи налазе у истом редоследу као у изворној наводи се
AsOrdered(), док се истовремено задржава паралелизам. У супротном, излазни елементи ће
бити посложени у редоследу извршења операције над њима. То је подразумевано понашање
PLINQ-а.

PLINQ омогућава агрегирање паралелно израчунатих резултата над елементима улазне


колекције. Најједноставнији пример агрегирања резултата је паралелно израчунавање збира
вредности. У наставку су приказани, најпре, општи PLINQ механизам агрегирања вредности, а
затим и готов агрегатни Sum оператор.

int ParalelnaSuma(IEnumerable<int> values)


{

Коментар

PLINQ садржи бројне операторе, заступљене и у LINQ библиотеци. Неки од њих су оператор
пројектовања Select и Where оператор филтрирања. Могућа је једноставна агрегација
резултата паралелног извршавања коришћењем агрегатних функција какве су Sum, Average и
Aggregate.

Мада је PLINQ значајно једноставнији од класе Parallel, она је, ипак, погоднија за друге
процесе у систему. То је нарочито значајно на серверским уређајима.

64
ИИ-28 Паралелно позивање већег броја метода

Проблем

Потребно је паралелно позвати већи број релативно независних метода.

Решење

У класи Parallel постоји метода Invoke која је креирана да испуни овакве захтеве. Инициране
операције се притом извршавају симултано, тако да се може очекивати да целокупна обрада
траје колико и најдужа операција иницирана у оквиру Parallel.Invoke.

У следећем примеру, паралелно се позива обрада две половине прослеђеног низа.

void ObradiNiz(double[] array)


{
Parallel.Invoke(
() => ObradiSegmentNiza(array, 0, array.Length / 2),
() => ObradiSegmentNiza(array, array.Length / 2, array.Length)
);
}
Aко се до тренутка извршавања не зна колико позива треба да буде извршено, методи Invoke
може се проследити и низ акција које треба да буду инициране паралелно. Подржани су и
прекиди, тако што се CancellationToken проследи кроз ParallelOptions. Пример је обрађен у
наставку.

Коментар

Уколико акцију треба иницирати над сваким улазним елементом, уместо Parallel.Invoke
препоручује се употреба Parallel.ForEach. Такође, ако свака од акција креира излазни
резултат, саветује се коришћење оператора из PLINQ библиотеке.

65
ИИ-29 Прекид паралелног извршавања

Проблем

Потребно је омогућити прекид паралелног извршавања.

Решење

Прекид извршавања паралелне методе најједноставније се омогућава прослеђивањем


CancellationToken-а кроз инстанцу класе ParallelOptions. Такође, CancellationToken се може
посматрати и директно у оквиру паралелне петље. Могуће је пратити токен на оба начина
истовремено, али нипошто само у оквиру петље, јер Parallel класа сигурно пружа
интелигентнији механизам одлучивања тренутка провере.

void RotirajMatrice(IEnumerable<Matrix> matrices,


float degrees,
CancellationToken token)
{
Parallel.ForEach(matrices,
new ParallelOptions { CancellationToken = token },//preporučljivo
matrix =>
Провера токена директно у оквиру петље захтева више посла, а и не уклапа се најбоље у
механизам реаговања на прекиде, јер се притом OperationCanceledException окружује у
AggregateException.

Са друге стране, PLINQ има уграђену подршку за прекиде – оператор WithCancellation.

IEnumerable<int> Dupliraj(IEnumerable<int> values, CancellationToken token)

Коментар

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

66
ИИ-30 Обрада грешака при паралелном извршавању

Проблем

Потребно је обрадити грешке настале при извршавању паралелне петље.

Решење

У оквиру паралелне петље потенцијално је могуће да при извршавању сваке итерације настане
грешка. Свака од насталих грешака биће смештена у један AggregateException – јединствену
агрегирану грешку. Таква грешка ће бити ухваћена у catch блоку. Свакој од грешака се, затим,
приступа из InnerExceptions атрибута агрегиране грешке.

void RotirajMatrice(IEnumerable<Matrix> matrices, float degrees)


{
try
{
Parallel.ForEach(matrices,
matrix =>
{
matrix.Rotate(degrees);
throw new InvalidOperationException();
});
}
catch (AggregateException exception)
{
foreach (Exception ex in exception.InnerExceptions)

Коментар

Када би метода подржавала прекиде и директно у оквиру петље позивом


ThrowIfCancellationRequested() над CancellationToken-ом утврдила да је прекид инициран,
OperationCanceledException би, као и све друге грешке, био смештен у јединствену
агрегирану грешку.

67
3.3 Имплементациони идиоми вишенитног програмирања
Област вишенитног програмирања на коју посебно треба обрадити пажњу при обради
имплементационих идиома је област синхронизације програмских нити. Синхронизација
обезбеђује да извршавање протиче без опасности од трке за приступ (race conditions) и застоја
(deadlocks) и штити дељени ресурс од доспевања у невалидно стање.

.NET омогућава неколико стратегија синхронизације приступа ресурсу – синхронизоване


сегменте кôда, ручну синхронизацију, синхронизоване контексте и thread-safe колекције.

Синхронизација сегмената кôда служи да се синхронизује само блок који захтева


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

Ручна синхронизација односи се на коришћење синхронизационих објеката (synchronization


primitives) које омогућава .NET.

Синхронизовани контексти се остварују коришћењем класе SynchronizationAttribute за


једноставну аутоматску синхронизацију над ContextBoundObject објектима.

Thread-safe колекције за конкурентни рад имају уграђене синхронизоване операције за


додавање и избацивање елемената.

Над примером несинхронизованог стања који се налази у наставку, биће обрађени


имплементациони идиоми синхронизације више програмских нити коришћењем различитих
.NET механизама.

public class NesinhronizovanoStanje


{
const int total = int.MaxValue;
long count = 0;

public long Pokreni()


{
Task t = Task.Run(() =>
{
Dekrementiraj();
});

Inkrementiraj();

t.Wait();

return count;
}

private void Inkrementiraj()

68
private void Dekrementiraj()
{
for (int i = 0; i < total; i++)

69
ИИ-31 Синхронизација нити коришћењем типа Monitor

Проблем

Потребно је помоћу типа Monitor обезбедити симултани приступ податку дељеном између
нити.

Решење

Помоћу класе Monitor може се блокирати приступ друге нити, пре него што прва нит изађе из
дељеног сегмента кôда. Заштићени сегмент кôда се означава позивима статичких метода
Monitor.Enter() и Monitor.Exit(), који се међусобно препознају преко заједничког објекта
који се методама прослеђује. Важно је да заштићени блок буде окружен try-finally блоком.
Тиме сузбијамо могуће блокирање свих осталих нити ако се на тренутно закључаној догоди
грешка и, због тога, никада не стигне до Monitor.Exit().

public class SinhronizacijaMonitor


{
const int total = int.MaxValue;
long count = 0;
readonly static object sync = new object();

public long Pokreni()


{
Task t = Task.Run(() =>
{
Dekrementiraj();
});

Inkrementiraj();

t.Wait();

return count;
}

private void Inkrementiraj()


{
for (int i = 0; i < total; i++)
{
bool lockTaken = false;

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 кључна реч која омогућава
закључавање нити по истом узору синхронизације.

public class SinhronizacijaLock


{
const int total = int.MaxValue;
long count = 0;
readonly static object sync = new object();

public long Pokreni()


{
Task t = Task.Run(() =>
{
Dekrementiraj();
});

Inkrementiraj();

t.Wait();

return count;
}

private void Inkrementiraj()


{
for (int i = 0; i < total; i++)
{
lock (sync)
{
count++;
}
}
}

private void Dekrementiraj()


{
for (int i = 0; i < total; i++)
{
lock (sync)
{

72
Коментар

Самим тим да примена синхронизације негативно утиче на перформансе извршавања, саветује


се да се синхронизују само објекти са измењивим статичким стањем, али не и подаци уско
везани за појединачну инстанцу.

73
ИИ-33 Означавање поља кључном речју volatile

Проблем

Потребно је осигурати да се инструкције извршавају у редоследу којим су програмиране.

Решење

Некада се дешава да процесор оптимизује кôд на такав начин да се инструкције не изврше


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

Један од начина решавања овог проблема је означавање поља кључном речју volatile која
означава да је поље нестално, променљиво и да је осетљиво на промене од стране хардвера,
оперативног система или друге нити.

Коментар

Ипак, са volatile се мора бити опрезан. Уколико се употреба ове кључне речи не познаје
добро, препорука је да се користи lock механизам закључавања.

74
ИИ-34 Синхронизација нити коришћењем типа Interlocked

Проблем

Потребно је извршити синхронизацију нити коришћењем типа Interlocked.

Решење

Неколико линија кôда којима се проверава да ли је дозвољен улазак у методу која мења
вредност неке променљиве може непотребно трошити програмерско време. Синхронизација
употребом типа Interlocked прати Узор поређења и размене (Compare/Exchange Pattern).
Уместо ручног програмирања кôда који окружује сегмент осетљив на промене, у оквиру типа
Interlocked расположив је аналогни механизам Interlocked.CompareExchange.

У примеру који следи, првом параметру методе се нова вредност додељује само онда када је
нова вредност једнака вредности трећег параметра методе.

private static object? data;

Коментар

Interlocked методе се преводе у појединачне процесорске инструкције и атомичне су.


Погодне су за једноставне синхронизације. Неке од најчешће коришћених су Add(),
CompareExchange(), Increment(), Decrement() и Exchange(). Све су атомичне и углавном
пружају више варијанти типова над којима раде.

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

75
ИИ-35 Употреба Timer-а

Проблем

Потребно је иницирати одређену функционалност у задатим временским интервалима.

Решење

.NET омогућава две врсте мерача времена (timers) намењених за вишенитно окружење. То су
System.Threading.Timer и System.Timers.Timer. Основна разлика је та да прва од две класе на
threadpool нити извршава појединачни делегат у одређеним временским интервалима, док
други тип иницира догађај у задатим временским интервалима, такође, на threadpool нити.

У наредном примеру приказана је употреба System.Threading.Timer мерача времена. При


инстанцирању мерача времена, дефинишу се делегат за извршавање, опционо објекат стања,
временски период пре првог иницирања делегата и временски интервал који протиче између
два иницирања. Мерач времена се прекида позивом методе Timer.Dispose. У примеру
делегат се позива први пут након једне секунде, а затим на сваке две секунде. Притом када се
извршава, делегат инкрементира бројач који означава број позива делегата. Мерач времена се
прекида након 10 иницирања делегата.

public class UpotrebaTimera


{
private Timer timer;

void Pokreni()
{
var timerState = new TimerState { Counter = 0 };

timer = new Timer(callback: new TimerCallback(TimerTask),


state: timerState,
dueTime: 1000,
period: 2000);

while (timerState.Counter <= 10)


{
Task.Delay(1000).Wait();
}

timer.Dispose();
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}: done.");
}

private void TimerTask(object timerState)


{
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}: starting a new callback."
var state = timerState as TimerState;
Interlocked.Increment(ref state.Counter);
}

76
Коментар

Други тип који подржава мерење времена у вишенитном окружењу System.Timers.Timer


функционише тако што се дефинише интервал у ком се иницира Elapsed догађај. Такође,
подешава се и Enabled атрибут који означава да ли је дозвољено да се догађај иницира. Поред
њега, могуће је подесити и атрибут AutoReset који подразумевано има вредност true што
значи да ће се догађај иницирати редовно у подешеним интервалима. Уколико се вредност
подеси на false догађај ће бити остварен само једном.

77
4 Закључак

78
5 Литература
Beck, K. (2007). Implementation Patterns. Addison-Wesley Professional.

Cleary, S. (2019). Concurrency in C# Cookbook (2nd изд.). O'Reilly Media, Inc.

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.

Michaelis, M. (.). Essential C# 8.0. Addison-Wesley Professional.

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

Programming idioms. (.). Преузето Септембар 13, 2020 са https://dept-


info.labri.fr/~strandh/Teaching/MTP/Common/Strandh-Tutorial/idioms.html

Strauss, D. (2017). C#7 and .NET Core Cookbook (2nd изд.). Packt Publishing.

Watson, B. (2014). Writing High-Performance .NET Code. Ben Watson.

Why writing idiomatic code is important? (.). Преузето Септембар 13, 2020 са
https://medium.com/@abyu/why-writing-idiomatic-code-is-important-f7e46c799c26

80

You might also like