Многонишкова синхронизация

Част две. Семафори и други

Последнияпоследнияпът, ако си спомняте, започнахме да говорим за синхронизация и аз ви я показах в цялата й слава, използвайки критични секции като пример. Сега да продължим и искрено се надявам, че по-нататъшното развитие на темата ще ви се стори достатъчно интересно.

Наистина ли критичните секции са толкова добри?

Защо продължаваме да говорим за синхронизация? В края на краищата изглежда, че вече сме обсъдили критичните секции в някои подробности, разгледахме как работят и стигнахме до извода, че те са доста лесни за използване и не изискват задълбочени познания на Windows API от програмиста. Между другото, тъй като говорим за Delphi (и казах, че сега ще разгледаме многонишкови приложения от гледна точка на този език за програмиране), тогава можете да направите, без да се свържете директно с Windows API, като използвате класовете на VCL библиотеката, специално проектирана за работа с критични секции. Въпреки това, след като говорих за критични секции, по някаква причина не се успокоих и реших да говоря за синхронизирането на нишки в многонишкови приложения по-нататък. И не е без причина - със сигурност трябва да има основателна причина да заемате вестникарските страници със синхрон.

Наистина, причината съществува и се нарича много просто - "несъвършенство на критичните участъци". Разбира се, те имат редица предимства и всичко в това е несъвършено, но има един много сериозен недостатък, който значително ограничава използването на критични секции: те не могат да се използват за синхронизиране на нишки, работещи в различни процеси. Разбира се, не всяко приложение се нуждае от възможността да синхронизира нишки, изпълнявани в различни процеси - но понякога тованеобходимо и затова е по-добре да знаете предварително как да разрешите този проблем. Друг значителен недостатък на критичните секции е, че само една нишка може да влезе в дадена критична секция наведнъж. Но ако стане необходимо едновременно да въведете определена област от кода на две нишки и да блокирате третата - какво да правите тогава? Това ще разберем днес.

Мютекси срещу. семафори срещу. критични секции

За да пишете многонишкови приложения, трябва да имате поне малко разбиране за още два вида обекти, използвани за синхронизация на нишки - мютекси и семафори. Струва си обаче да се отбележи, че семафорите са по-важни тук, защото в Windows един мютекс всъщност е специален случай на семафор.

За разлика от критичните секции, мютексите ви позволяват да синхронизирате нишки, изпълнявани в различни процеси. Това всъщност е най-голямата разлика между мютекс и критична секция. Друга, по-малко съществена разлика е, че мютексът освен самия дескриптор има и символно име, с което също може да бъде достъпен.

Що се отнася до семафорите, това са обекти, които развиват концепцията за mutexes и за разлика от последните имат вграден брояч на включени в тях нишки. По този начин семафорите просто решават втория проблем, свързан с критичните секции, тоест позволяват не само на една, но на няколко различни нишки да влязат в зоната, защитена от семафора, по такъв начин, че броят им да не надвишава посочения от разработчика на програмата. Като цяло, както виждате, семафорите са по-функционални обекти и поради тази причина ще се съсредоточим върху тях в тази статия. Тъй като мютексите са частнив случай на семафори, ние няма да ги разглеждаме подробно: винаги е по-лесно да се премине от общото към частното, отколкото от частното към общото.

Самите семафори

Със семафорите, както и преди с критичните секции, ще работим чрез специални функции от арсенала на Windows API - за да улесним пренасянето на примери от Delphi към други езици за платформата на Windows.

Първо, нека се запознаем с функцията, отговорна за създаването на семафори. Ако погледнете документацията на Windows API, можете да видите, че изглежда така: HANDLE CreateSemaphore (LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG InitialCount, LONG lMaximumCount, LPCTSTR lpName). Описанието тук, както е обичайно в документацията на Windows API, е дадено не на Delphi, а на C, но мисля, че можете лесно да го навигирате. Първият параметър са атрибутите за сигурност, които се предават на функцията, за да позволят на други процеси да използват семафора. Засега не се нуждаем от такива затруднения, така че тук просто ще предадем нулев указател към функцията - нула. Вторият и третият параметър са началните и максималните стойности на брояча, вграден във всеки семафор. Говорих за този брояч по-горе - той отчита броя на нишките, които в момента са "под юрисдикцията" на семафора. Разбира се, максималният брой не трябва да бъде по-малък от първоначалния, а началният трябва да бъде поне нула. Последният параметър, който се предава на функцията CreateSemaphore, когато бъде извикана, е името на семафора, което не трябва да съдържа обратни наклонени черти (\).

Друга функция, която ще ни трябва при работа със семафори е WaitForSingleObject. Изглежда така в помощта за Windows API: DWORD WaitForSingleObject (HANDLE hHandle, DWORDdwМилисекунди). Както можете да видите, той има много малко параметри - само два - но това не пречи тази функция да бъде много, много важна. За какво всъщност е необходимо? Той поставя нишка в състояние на изчакване за освобождаване на някакъв зает ресурс, от което може да излезе в два случая: или освобождаване на този ресурс от друга нишка, или изтичане на времето, определено за това изчакване. Съответно, параметрите, предадени при извикването на тази функция, просто определят тези две стойности: първият параметър е дескрипторът на обекта, който нашата нишка ще изчака за освобождаване, а вторият е броят милисекунди, които трябва да изминат, преди нишката да изглежда уморена от чакане. Ако искаме нишката просто да проверява състоянието на обекта, тогава можем да зададем 0 като време и ако искаме чакането да продължи до края, който ще дойде никой не знае кога, тогава трябва да използваме константата INFINITE.

И накрая, третата функция, необходима на тези, които ще работят със семафори, е функцията ReleaseSemaphore, която се използва за работа с брояча на семафори. Ще изглежда така: BOOL ReleaseSemaphore (HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount). Мисля, че тук е лесно да се досетите, че първият параметър е дескрипторът на самия семафор, с чийто брояч ще имаме работа. Вторият параметър е числото, с което намаляваме вътрешния брояч на семафора, използвайки тази функция. Е, в третия параметър програмата ще запази предишната стойност на брояча. Третият параметър на функцията ReleaseSemaphore е указател към целочислена променлива. Това е необходимо поради простата причина, че семафорът не принадлежи към никоя нишка. Засега обаче ще го направимизползвайте нула като този параметър.

"Използваните" семафори се премахват от програмата чрез функцията CloseHandle. Той има един-единствен параметър - манипулатора на този семафор, който вече няма да ни трябва в това приложение.

Е, нека сега се върнем към този изстрадал пример, който неуморно анализирахме в първата статия за многонишковостта и който оттогава се е променил доста. Последният път, когато извършихме синхронизация с помощта на критични секции, сега ще прибегнем до семафори. Няма да показвам отново несинхронизирания код - ако имате нужда от него, за да го сравните с новия, вижте предишната статия за многопоточността.

В списъка можете да видите кода за метода Execute на нашия клас нишки.

Както можете да видите, в този метод се използва функцията WaitForSingleObject. Когато върнатата от него стойност е равна на WAIT_OBJECT_0, тогава нишката ни се "събужда".

Сега нека да разгледаме кода за инициализация на нишката - както можете да видите, той също се е променил малко.

Обърнете внимание на параметрите на функцията CreateSemaphore. Ако променим началния и максимален брой нишки в семафора на 2, отново ще получим несинхронизирано приложение, където нишките ще се бият помежду си и ще сменят заглавието в заглавието на прозореца в произволен ред.

Както можете да видите, дори когато използваме семафор вместо критична секция в нашето малко демонстрационно приложение, това не ни се струва особено сложно, въпреки че семафорите по своята същност са по-сложни от критичните секции.

Толкова добри ли са семафорите?

Разбира се, трябва да се отбележи, че семафорите също изобщо не са небесни ангели и имат някои недостатъци, споредв сравнение с критичните участъци. Първият такъв недостатък е, че семафорите (както и мютексите, между другото) са сравнително бавни и превключването на нишки с тяхна помощ не е толкова бързо, колкото при използване на критични секции, което може да бъде критично за програми в реално време. Друг недостатък на семафорите е, че те изискват повече памет, което обаче не е толкова критично в наше време. Така че, както можете да видите, за повечето приложения семафорите все още ще бъдат най-гъвкавият и многофункционален начин за синхронизиране.

Е, сега може би разгледахме синхронизирането на нишките достатъчно подробно и можем да спрем дотук. Мисля, че две статии за синхронизация със сигурност бяха полезни за всички, които се интересуват от писане на многонишкови приложения. Що се отнася до продължаването на статиите за многонишковостта, мисля, че ще бъде, тъй като това е такава тема, че можете да я обсъждате на страниците на "KV" почти безкрайно.