Състояния на нишката. Критични раздели Завършване на синхронизирането в Windows OS

Този обект за синхронизиране може да се използва само локално в процеса, който го е създал. Останалите обекти могат да се използват за синхронизиране на нишките на различни процеси. Името на обекта "критична секция" се свързва с някакъв абстрактен избор на част от програмния код (секция), която изпълнява някои операции, чийто ред не може да бъде нарушен. Това означава, че опит от две различни нишки да изпълнят едновременно кода на този раздел ще доведе до грешка.

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

За критичната секция се въвеждат две операции:

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

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

Най-общо казано, в други API, различни от Win32 (като OS/2), критичната секция не се третира като обект за синхронизация, а като част от програмен код, който може да бъде изпълнен само от една нишка на приложение. Тоест влизането в критичната секция се счита за временно изключване на механизма за превключване на нишки до излизането от тази секция. API на Win32 третира критичните секции като обекти, което води до известно объркване - те са много близки по своите свойства до неназовани изключителни обекти ( мютекс, виж отдолу).

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

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

Има няколко случая на ефективно използване на критични секции:

читателите не влизат в конфликт с писателите (само писателите трябва да бъдат защитени);

всички нишки имат приблизително равни права на достъп (да речем, не можете да отделите чисти писатели и читатели);

при конструиране на съставни синхронизиращи обекти, състоящи се от няколко стандартни, за защита на последователни операции върху съставен обект.

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

WaitForMultipleObjects и други функции за изчакване

Както си спомняте, за да синхронизирате нишки, обикновено трябва временно да спрете изпълнението на една от нишките. Трябва обаче да се преведе със средства операционна системав състояние на изчакване, където не заема процесорно време. Вече знаем две функции, които могат да направят това: SuspendThread и ResumeThread. Но както казах в предишната част на статията, поради някои характеристики тези функции не са подходящи за синхронизация.

Днес ще разгледаме друга функция, която също поставя нишката в състояние на изчакване, но за разлика от SuspendThread/ResumeThread, тя е специално проектирана за организиране на синхронизация. Това е WaitForMultipleObjects. Тъй като тази функция е толкова важна, ще се отклоня малко от моето правило да не навлизам в подробностите на API и ще говоря за него по-подробно, дори ще дам неговия прототип:

DWORD WaitForMultipleObjects (

DWORD nCount , // брой обекти в масива lpHandles

CONST ДРЪЖКА * lpHandles , // указател към масив от дескриптори на обект на ядрото

BOOL bWaitAll , // флаг, указващ дали да се изчакат всички обекти или е достатъчен само един

DWORD dwмилисекунди // таймаут

Основният параметър на тази функция е указател към масив от манипулатори на обект на ядрото. Ще говорим за това какви са тези обекти по-долу. Засега е важно за нас да знаем, че всеки от тези обекти може да бъде в едно от двете състояния: неутрално или "сигнално" (сигнализирано състояние). Ако флагът bWaitAll е FALSE, функцията ще се върне веднага щом поне един от обектите даде сигнал. И ако флагът е TRUE, това ще се случи само когато всички обекти започнат да сигнализират наведнъж (както ще видим, това е най-важното свойство на тази функция). В първия случай по върнатата стойност можете да разберете кой от обектите е подал сигнал. Трябва да извадите константата WAIT_OBJECT_0 от него и ще получите индекс в масива lpHandles. Ако времето за изчакване надвишава времето за изчакване, посочено в последния параметър, функцията ще спре да чака и ще върне стойността WAIT_TIMEOUT. Като таймаут можете да посочите константата INFINITE и тогава функцията ще изчака "докато спре" или можете обратното 0 и тогава нишката изобщо няма да бъде спряна. В последния случай функцията ще се върне веднага, но нейният резултат ще ви каже състоянието на обектите. Последната техника се използва много често. Както можете да видите, тази функция има богати възможности. Има няколко други функции WaitForXXX, но всички те са вариации на основната тема. По-специално, WaitForSingleObject е просто негова опростена версия. Останалите имат собствена допълнителна функционалност, но като цяло се използват по-рядко. Например, те позволяват да се реагира не само на сигнали от обекти на ядрото, но и на пристигането на нови съобщения в прозореца в опашката на нишката. Тяхното описание, както и подробна информация за WaitForMultipleObjects, ще намерите, както обикновено, в MSDN.

Сега какво представляват тези мистериозни „обекти на ядрото“. Като начало те включват самите нишки и процеси. Те влизат в състояние на сигнализация веднага след завършване. Това е много важна характеристика, защото често е необходимо да се следи кога една нишка или процес е прекратен. Нека, например, нашето сървърно приложение с набор от работни нишки трябва да бъде завършено. В същото време контролната нишка трябва да информира работните нишки по някакъв начин, че е време да приключи работата (например чрез задаване на глобален флаг) и след това да изчака, докато всички нишки завършат, като прави всичко необходимо за правилното завършване на действието: освобождаване на ресурси, информиране на клиенти за изключване, затваряне на мрежови връзки и др.

Фактът, че нишките включват сигнал в края на работата, прави изключително лесно решаването на проблема със синхронизацията с прекратяването на нишката:

// За простота, нека имаме само една работна нишка. Нека го стартираме:

HANDLE hWorkerThread = :: Създаване на нишка (...);

// Преди края на работата трябва по някакъв начин да кажем на работната нишка, че е време за качване.

// Изчакайте нишката да приключи:

DWORD dwWaitResult = :: WaitForSingleObject ( hWorkerThread , БЕЗКРАЕН );

ако( dwWaitResult != WAIT_OBJECT_0 ) { /* обработка на грешки */ }

// "Дръжката" на потока може да бъде затворена:

ПРОВЕРКА (:: CloseHandle ( hWorkerThread );

/* Ако CloseHandle се провали и върне FALSE, не хвърлям изключение. Първо, дори ако това се случи поради системна грешка, това няма да има преки последици за нашата програма, защото тъй като затваряме манипулатора, тогава не се очаква работа с него в бъдеще. В действителност повредата на CloseHandle може да означава само грешка във вашата програма. Затова ще вмъкнем тук макроса VERIFY, за да не го пропуснем на етапа на отстраняване на грешки в приложението. */

Кодът, който чака процесът да приключи, ще изглежда подобно.

Ако нямаше такава вградена възможност, работната нишка би трябвало по някакъв начин да предаде информация за завършването си на самата основна нишка. Дори и да направи това последно, основната нишка не може да бъде сигурна, че работникът не разполага с поне няколко инструкции на асемблер, останали за изпълнение. AT индивидуални ситуации(например, ако кодът на нишката е в DLL, който трябва да бъде разтоварен, когато приключи), това може да бъде фатално.

Искам да ви напомня, че дори след прекратяване на нишка (или процес), нейните манипулатори все още остават в сила, докато не бъдат изрично затворени от функцията CloseHandle. (Между другото, не забравяйте да направите това!) Това се прави само за да можете по всяко време да проверите състоянието на потока.

И така, функцията WaitForMultipleObjects (и нейните аналози) ви позволява да синхронизирате изпълнението на нишка със състоянието на обектите за синхронизиране, по-специално други нишки и процеси.

Специални обекти на ядрото

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

събитие

Може би най-простият и най-фундаментален синхронизиращ обект. Това е само флаг, който може да бъде зададен с функциите SetEvent / ResetEvent: сигнализиране или неутрално. Събитието е най-удобният начин да сигнализирате на чакаща нишка, че се е случило някакво събитие (затова се нарича) и можете да продължите да работите. Използвайки събитие, можем лесно да разрешим проблема със синхронизацията при инициализиране на работна нишка:

// Нека запазим манипулатора на събитието в глобална променлива за простота:

HANDLE g_hEventInitComplete = НУЛА ; // никога не оставяйте променлива неинициализирана!

{ // код в основната нишка

// създаване на събитие

g_hEventInitComplete = :: CreateEvent (НУЛА,

НЕВЯРНО , // ще говорим за този параметър по-късно

НЕВЯРНО , // начално състояние - неутрално

ако(! g_hEventInitComplete ) { /* Не забравяйте за обработката на грешки */ }

// създаване на работна нишка

DWORD idWorkerThread = 0 ;

HANDLE hWorkerThread = :: Създаване на нишка (НУЛА , 0 , & WorkerThreadProc , НУЛА , 0 , & idWorkerThread );

ако(! hWorkerThread ) { /* обработка на грешки */ }

// изчакайте сигнал от работната нишка

DWORD dwWaitResult = :: WaitForSingleObject ( g_hEventInitComplete , БЕЗКРАЕН );

ако( dwWaitResult != WAIT_OBJECT_0 ) { /* грешка */ }

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

ПРОВЕРКА (:: CloseHandle ( g_hEventInitComplete )); // не забравяйте да затворите ненужните обекти

g_hEventInitComplete = НУЛА ;

// функция на работния процес

DWORD WINAPI WorkerThreadProc ( LPVOID_параметър )

InitializeWorker (); // инициализация

// сигнализира, че инициализацията е завършена

BOOL е ОК = :: SetEvent ( g_hEventInitComplete );

ако(! е ОК ) { /* грешка */ }

Трябва да се отбележи, че има две значително различни разновидности на събитията. Можем да изберем един от тях с помощта на втория параметър на функцията CreateEvent. Ако е TRUE, се създава събитие, чието състояние се контролира само ръчно, тоест от функциите SetEvent/ResetEvent. Ако е FALSE, ще се генерира събитие за автоматично нулиране. Това означава, че веднага щом някоя нишка, чакаща дадено събитие, бъде освободена от сигнал от това събитие, тя автоматично ще бъде върната обратно в неутрално състояние. Тяхната разлика е най-силно изразена в ситуация, в която няколко нишки чакат едно събитие наведнъж. Ръчно контролирано събитие е като стартов пистолет. Веднага щом се настрои на сигнализираното състояние, всички нишки ще бъдат освободени наведнъж. Събитието за автоматично нулиране, от друга страна, е като турникет на метрото: то ще освободи само един поток и ще се върне в неутрално състояние.

Mutex

В сравнение със събитие, това е по-специализиран обект. Обикновено се използва за решаване на често срещан проблем със синхронизацията, като например достъп до ресурс, споделен от множество нишки. В много отношения това е подобно на събитие за автоматично нулиране. Основната разлика е, че има специално обвързване към конкретна нишка. Ако мютексът е в сигнализирано състояние, това означава, че е свободен и не принадлежи към никоя нишка. Веднага след като дадена нишка изчака този мютекс, последният се нулира до неутрално състояние (тук е точно като събитие за автоматично нулиране) и нишката става негов собственик, докато изрично не освободи мютекса с функцията ReleaseMutex, или прекратява. По този начин, за да сте сигурни, че само една нишка работи със споделени данни в даден момент, всички места, където се извършва такава работа, трябва да бъдат заобиколени от двойка: WaitFor - ReleaseMutex :

HANDLE g_hMutex ;

// Нека манипулаторът на mutex се съхранява в глобална променлива. Разбира се, той трябва да бъде създаден предварително, преди началото на работните нишки. Да приемем, че това вече е направено.

вътрчакам = :: WaitForSingleObject ( g_hMutex , БЕЗКРАЕН );

превключвател(чакам ) {

случай WAIT_OBJECT_0 : // Всичко е наред

прекъсвам;

случай WAIT_BANDONED : /* Някаква нишка приключи, забравяйки да извикате ReleaseMutex. Най-вероятно това означава грешка във вашата програма! Затова за всеки случай ще вмъкнем ASSERT тук, но във финалната версия (release) ще считаме този код за успешен. */

ТВЪРДЕТЕ ( невярно );

прекъсвам;

по подразбиране:

// Обработката на грешки трябва да е тук.

// Част от код, защитена от mutex.

ProcessCommonData ();

ПРОВЕРКА (:: ReleaseMutex ( g_hMutex ));

Защо mutex е по-добър от събитие за автоматично нулиране? В горния пример може също да се използва, само ReleaseMutex ще трябва да бъде заменен с SetEvent. Въпреки това може да възникне следната трудност. Най-често трябва да работите със споделени данни на няколко места. Какво се случва, ако ProcessCommonData в нашия пример извика функция, която работи със същите данни и която вече има своя собствена двойка WaitFor - ReleaseMutex (на практика това е много често)? Ако използваме събитие, програмата очевидно ще увисне, защото вътре в защитения блок събитието е в неутрално състояние. Мутексът е по-сложен. Той винаги остава в състояние на сигнализиране за главната нишка, въпреки че е в неутрално състояние за всички останали нишки. Следователно, ако нишка е придобила мютекса, повторното извикване на функцията WaitFor няма да блокира. Освен това, брояч също е вграден в mutex, така че ReleaseMutex трябва да бъде извикан същия брой пъти, колкото е имало извиквания към WaitFor. По този начин можем безопасно да защитим всяка част от кода, която работи със споделени данни с двойка WaitFor - ReleaseMute x, без да се притесняваме, че този код може да бъде извикан рекурсивно. Това прави mutex много лесен инструмент за използване.

Семафор

Още по-специфичен обект за синхронизация. Трябва да призная, че в моята практика все още не е имало случай, в който да е полезно. Семафорът е проектиран да ограничава максималния брой нишки, които могат да работят върху ресурс едновременно. По същество семафорът е събитие с брояч. Докато този брояч е по-голям от нула, семафорът е в състояние на сигнализиране. Въпреки това, всяко извикване на WaitFor намалява този брояч с единица, докато стане нула и семафорът премине в неутрално състояние. Подобно на mutex, семафорът има функция ReleaseSemaphor, която увеличава брояч. Въпреки това, за разлика от mutex, семафорът не е обвързан с нишка и извикването на WaitFor/ReleaseSemaphor отново ще намали/увеличи брояча.

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

Какво друго може да се каже за обектите за синхронизиране на ядрото? Много е удобно да им давате имена. Всички функции, които създават обекти за синхронизация, имат съответния параметър: CreateEvent, CreateMutex, CreateSemaphore. Ако извикате, например, CreateEvent два пъти, като и двата пъти посочите едно и също непразно име, тогава вторият път функцията, вместо да създаде нов обект, ще върне манипулатора на съществуващ. Това ще се случи, дори ако второто извикване е направено от друг процес. Последното е много удобно в случаите, когато искате да синхронизирате нишки, принадлежащи към различни процеси.

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

Искам да ви напомня това По най-добрия начинза да се гарантира, че CloseHandle или подобна функция за "почистване" със сигурност ще бъде извикана, дори в случай на необичайна ситуация, е да я поставите в деструктор. Между другото, това веднъж беше добре и подробно описано в статията на Кирил Плешивцев „Умен деструктор“. В горните примери не използвах тази техника само за образователни цели, така че работата на API функциите беше по-визуална. В реалния код винаги трябва да използвате обвиващи класове с интелигентни деструктори за почистване.

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

Завършвайки прегледа на обектите за синхронизация, бих искал да спомена обект, който не е в Win32 API. Много мои колеги се чудят защо Win32 няма специализиран обект "един пише, много чете". Един вид „разширен mutex“, който гарантира, че само една нишка може едновременно да има достъп до споделени данни за запис, а няколко нишки могат само да четат наведнъж. Подобен обект може да се намери в UNIX "ах. Някои библиотеки, например от Borland, предлагат да го емулират въз основа на стандартни обекти за синхронизация. Истинската полза от такива емулации обаче е много съмнителна. Такъв обект може да бъде ефективно внедрен само на нивото на ядрото на операционната система.Но в ядрото на Windows не предоставя такъв обект.

Защо разработчиците на ядрото на Windows NT не са се погрижили за това? Защо сме по-лоши от UNIX? Според мен отговорът е, че просто все още не е имало реална нужда от такъв обект за Windows. На нормална еднопроцесорна машина, където нишките все още не могат физически да работят едновременно, това ще бъде практически еквивалентно на mutex. На многопроцесорна машина може да се възползва, като позволи на нишките на четеца да работят паралелно. В същото време тази печалба ще стане осезаема само когато вероятността от "сблъсък" на нишки за четене е висока. Несъмнено, например, на 1024-процесорна машина такъв обект на ядрото ще бъде жизненоважен. Съществуват подобни машини, но те са специализирани системи, работещи със специализирани операционни системи. Често такива операционни системи са изградени на базата на UNIX, вероятно оттам обект като „един пише, много четат“ е попаднал в по-често използваните версии на тази система. Но на машините x86, с които сме свикнали, като правило се инсталира само един и само понякога два процесора. И само най-модерните модели процесори като Intel Xeon поддържат 4 или дори повече процесорни конфигурации, но такива системи все още остават екзотични. Но дори и на такава "усъвършенствана" система, "усъвършенстваният мютекс" може да даде забележимо увеличение на производителността само в много специфични ситуации.

По този начин внедряването на "разширен" mutex просто не си струва труда. На машина с "нисък процесор" може дори да е по-малко ефективен поради сложността на логиката на обекта в сравнение със стандартния мютекс. Моля, имайте предвид, че изпълнението на такъв обект не е толкова просто, колкото може да изглежда на пръв поглед. При неуспешно внедряване, ако има твърде много нишки за четене, нишката за писане просто няма да „пропусне“ до данните. Поради тези причини аз също не препоръчвам да се опитвате да емулирате такъв обект. В реални приложения на реални машини обикновен мютекс или критична секция (която ще бъде обсъдена в следващата част на статията) ще се справи перфектно със задачата за синхронизиране на достъпа до споделени данни. Въпреки че, предполагам, с развитието на операционната система Windows рано или късно ще се появи обектът на ядрото "един пише, много чете".

Забележка. Всъщност обектът "един пише - много четат" в Windows NT все още съществува. Просто не знаех за това, когато написах тази статия. Този обект се нарича "ресурси на ядрото" и не е достъпен за програми в потребителски режим, което вероятно е причината да не е добре известен. Прилики за него могат да бъдат намерени в DDK. Благодаря на Константин Манурин, че ми посочи това.

Безизходица

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

Защо е необходима функция за изчакване на един от няколко обекта е разбираемо. При липса на специална функция това може да стане, освен чрез последователна проверка на състоянието на обектите в празен цикъл, което, разбира се, е неприемливо. Но необходимостта от специална функция, която ви позволява да изчакате момента, в който няколко обекта преминават в състояние на сигнал наведнъж, не е толкова очевидна. Наистина, представете си следната типична ситуация: в определен момент нашата нишка се нуждае от достъп до два набора от споделени данни наведнъж, всеки от които отговаря за свой собствен mutex, нека ги наречем A и B. Изглежда, че нишката може първо изчакайте, докато мутекс A бъде освободен, заснемете го, след това изчакайте мутекс B да бъде освободен... Изглежда, че можем да направим с няколко извиквания на WaitForSingleObject. Наистина, това ще работи, но само докато всички други нишки придобиват мютексите в същия ред: първо A, след това B. Какво се случва, ако определена нишка се опита да направи обратното: първо придобие B, след това A? Рано или късно ще възникне ситуация, когато една нишка е уловила mutex A, друга B, първата чака B да бъде освободена, втората A. Ясно е, че никога няма да чакат това и програмата ще виси.

Този вид блокиране е много често срещан бъг. Както всички грешки, свързани със синхронизацията, тя се появява само от време на време и може да съсипе много нерви на програмиста. В същото време почти всяка схема, която включва няколко обекта за синхронизация, е изпълнена с блокиране. Следователно на този проблем трябва да се обърне специално внимание на етапа на проектиране на такава схема.

В дадения прост пример блокирането е доста лесно да се избегне. Необходимо е да се изисква всички нишки да придобиват мютекси в определен ред: първо A, след това B. Въпреки това, в сложна програма, където има много обекти, свързани един с друг по различни начини, това обикновено не е толкова лесно да се постигне. Не два, а много обекти и нишки могат да бъдат включени в една ключалка. Следователно най надежден начинЗа да избегнете задънена улица в ситуация, в която една нишка се нуждае от няколко обекта за синхронизация наведнъж, трябва да ги прихванете всички с едно извикване на функцията WaitForMultipleObjects с параметъра bWaitAll=TRUE. Честно казано, в този случай ние просто прехвърляме проблема с задънените блокировки към ядрото на операционната система, но основното е, че това вече няма да бъде наша грижа. Въпреки това, в сложна програма с много обекти, когато не винаги е възможно веднага да се каже кой от тях ще бъде необходим за извършване на определена операция, често не е лесно да се съберат всички WaitFor повиквания на едно място и да се комбинират.

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

Пример за синхронизация

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

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

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

клас CQuota {

публичен: // методи

невалиденкомплект ( unsigned int _nКвота );

unsigned intЗаявка ( unsigned int _nBytesToRead , HANDLE_hStopEvent );

невалиденОсвобождаване ( unsigned int _nBytesRevert , HANDLE_hStopEvent );

Периодично, например веднъж в секунда, контролната нишка извиква метода Set, задавайки квотата за изтегляне. Преди работната нишка да прочете данните, получени от мрежата, тя извиква метода Request, който проверява дали текущата квота не е нула и ако е така, връща броя байтове, които могат да бъдат прочетени, по-малко от текущата квота. Квотата съответно се намалява с това число. Ако квотата е нула, когато се извика Request, извикващата нишка трябва да изчака, докато квотата е налична. Понякога се случва действително да бъдат получени по-малко байтове от заявените, в който случай нишката връща част от квотата, разпределена за нея от метода Release. И както казах, потребителят може по всяко време да даде команда за спиране на изтеглянето. В този случай изчакването трябва да бъде прекъснато, независимо от наличието на квота. За това се използва специално събитие: _hStopEvent. Тъй като задачите могат да се стартират и спират независимо, всяка работна нишка има свое собствено събитие за спиране. Неговият манипулатор се предава на методите Request и Release.

В един от неуспешните варианти се опитах да използвам комбинация от mutex, който синхронизира достъпа до класа CQuota и събитие, което сигнализира за наличието на квота. Стоп събитието обаче не се вписва в тази схема. Ако нишка иска да придобие квота, тогава нейното състояние на изчакване трябва да се контролира от сложен булев израз: ((mutex И квота събитие) ИЛИ стоп събитие). Но WaitForMultipleObjects не позволява това, можете да комбинирате няколко обекта на ядрото или с И или ИЛИ операция, но не смесени. Опитът да се раздели чакането с две последователни извиквания на WaitForMultipleObjects неизбежно води до блокиране. Като цяло този път се оказа задънена улица.

Вече няма да пускам мъглата и ще ви кажа решението. Както казах, mutex е много подобен на събитие за автоматично нулиране. И тук имаме точно този рядък случай, когато е по-удобно да го използвате, но не един, а два наведнъж:

клас CQuota {

лично: // данни

unsigned int m_nQuota ;

CEvent m_eventHasQuota ;

CEvent m_eventNoQuota ;

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

Ще дам пример за реализацията на метода Request. Останалите се изпълняват по подобен начин. Леко опростих кода, използван в реалния проект:

unsigned int CQuota :: Заявка ( unsigned int _nЗаявка , HANDLE_hStopEvent )

ако(! _nЗаявка ) връщане 0 ;

unsigned int nОсигурете = 0 ;

ОБРАБОТЕТЕ hEvents [ 2 ];

hСъбития [ 0 ] = _hStopEvent ; // Stop събитието има по-висок приоритет. Ние го поставяме на първо място.

hСъбития [ 1 ] = m_eventHasQuota ;

вътр iWaitResult = :: WaitForMultipleObjects ( 2 , hСъбития , НЕВЯРНО , БЕЗКРАЕН );

превключвател( iWaitResult ) {

случай WAIT_FAILED :

// ГРЕШКА

хвърляй нов CWin32Exception ;

случай WAIT_OBJECT_0 :

// Спиране на събитието. Справих се с него с персонализирано изключение, но нищо не ми пречи да го внедря по друг начин.

хвърляй нов CStopException ;

случай WAIT_OBJECT_0 + 1 :

// Събитие "налична квота"

ТВЪРДЕТЕ ( m_nQuota ); // Ако сигналът е даден от това събитие, но всъщност няма квота, значи някъде сме направили грешка. Трябва да потърся бъга!

ако( _nЗаявка >= m_nQuota ) {

nОсигурете = m_nQuota ;

m_nQuota = 0 ;

m_eventNoQuota . комплект ();

друго {

nОсигурете = _nЗаявка ;

m_nQuota -= _nЗаявка ;

m_eventHasQuota . комплект ();

прекъсвам;

връщане nОсигурете ;

Малка забележка. Библиотеката MFC не беше използвана в този проект, но, както вероятно вече се досещате, направих свой собствен клас CEvent, обвивка около обекта на ядрото „събитие“, подобно на MFC „schnoy. Както казах, такива прости класове обвивка са много полезни, когато има някакъв ресурс (в този случай обект на ядрото), който трябва да се запомни да освободи в края на работата. В останалото няма значение дали пишете SetEvent(m_hEvent) или m_event.Set( ).

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

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

Процесът е екземпляр на програма, зареден в паметта. Този екземпляр може да създава нишки, които са последователност от инструкции, които трябва да бъдат изпълнени. Важно е да разберете, че не се изпълняват процеси, а нишки.

Освен това всеки процес има поне една нишка. Тази нишка се нарича основна (главна) нишка на приложението.

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

В зависимост от ситуацията нишките могат да бъдат в три състояния. Първо, една нишка може да се изпълнява, когато й е дадено процесорно време, т.е. може да е активен. На второ място, може да е неактивен и да чака да бъде разпределен процесор, т.е. бъдете в състояние на готовност. И има трета, също много важно условие- състояние на заключване. Когато дадена нишка е блокирана, за нея изобщо не се разпределя време. Обикновено ключалката се поставя в очакване на някакво събитие. Когато възникне това събитие, нишката автоматично преминава от блокирано състояние в състояние на готовност. Например, ако една нишка извършва изчисления, докато друга трябва да изчака резултатите да бъдат записани на диск. Вторият може да използва цикъл като "while(!isCalcFinished) continue;", но е лесно да се види на практика, че процесорът е 100% зает, докато този цикъл работи (това се нарича активно чакане). Такива примки трябва да се избягват, когато е възможно, при което заключващият механизъм оказва безценна помощ. Втората нишка може да се блокира, докато първата нишка не зададе събитие, което да сигнализира, че четенето е приключило.

Синхронизиране на нишки в Windows OS

Windows прилага превантивна многозадачност, което означава, че по всяко време системата може да прекъсне изпълнението на една нишка и да прехвърли контрола на друга. Преди това в Windows 3.1 беше използван метод на организация, наречен кооперативна многозадачност: системата изчакваше, докато самата нишка прехвърли управлението към нея и затова, ако едно приложение замръзне, компютърът трябваше да се рестартира.

Всички нишки, които принадлежат към един и същ процес, споделят някои общи ресурси, като RAM адресно пространство или отворени файлове. Тези ресурси принадлежат на целия процес, а оттам и на всяка негова нишка. Следователно всяка нишка може да работи с тези ресурси без никакви ограничения. Но... Ако една нишка все още не е приключила работата си с който и да е споделен ресурс и системата е превключила към друга нишка, използваща същия ресурс, тогава резултатът от работата на тези нишки може да бъде изключително различен от предвидения. Такива конфликти могат да възникнат и между нишки, принадлежащи към различни процеси. Всеки път, когато две или повече нишки използват някакъв споделен ресурс, възниква този проблем.

Пример. Нишките са извън синхронизация: Ако временно спрете нишката за показване (на пауза), нишката за запълване на фонов масив ще продължи да се изпълнява.

#включи #включи int a; ДРЪЖКА hThr; неподписан дълъг uThrID; void Thread(void* pParams) ( int i, num = 0; while (1) ( for (i=0; i<5; i++) a[i] = num; num++; } } int main(void) { hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID); while(1) printf("%d %d %d %d %d\n", a, a, a, a, a); return 0; }

Ето защо е необходим механизъм, който да позволи на нишките да координират работата си със споделени ресурси. Този механизъм се нарича механизъм за синхронизиране на нишки.

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

Има няколко обекта за синхронизация, най-важните от които са mutex, критична секция, събитие и семафор. Всеки от тези обекти прилага свой собствен метод за синхронизация. Също така самите процеси и нишки могат да се използват като обекти за синхронизация (когато една нишка чака завършването на друга нишка или процес); както и файлове, комуникационни устройства, въвеждане на конзола и известия за промени.

Всеки обект за синхронизация може да бъде в така нареченото сигнализирано състояние. За всеки тип обект това състояние има различно значение. Нишките могат да проверяват текущото състояние на даден обект и/или да чакат това състояние да се промени и по този начин да координират своите действия. Това гарантира, че когато една нишка работи с обекти за синхронизация (създава ги, променя състоянието), системата няма да прекъсне нейното изпълнение, докато не завърши това действие. По този начин всички крайни операции върху обекти за синхронизация са атомарни (неделими.

Работа с обекти за синхронизация

За да създадете един или друг обект за синхронизация, се извиква специална WinAPI функция от типа Create... (например CreateMutex). Това извикване връща манипулатор на обект (HANDLE), който може да се използва от всички нишки, принадлежащи към дадения процес. Възможно е да получите достъп до обекта за синхронизиране от друг процес, или чрез наследяване на манипулатора на обекта, или, за предпочитане, чрез извикване на функцията Open... на обекта. След това извикване процесът ще получи манипулатор, който по-късно може да се използва за работа с обекта. Обектът трябва да получи име, освен ако не е предназначен да бъде използван в рамките на един процес. Имената на всички обекти трябва да са различни (дори да са от различен тип). Не можете например да създадете събитие и семафор с едно и също име.

Чрез наличния дескриптор на даден обект можете да определите текущото му състояние. Това става с помощта на т.нар. чакащи функции. Най-често използваната функция е WaitForSingleObject. Тази функция приема два параметъра, първият е манипулаторът на обекта, вторият е времето за изчакване в ms. Функцията връща WAIT_OBJECT_0, ако обектът е в сигнализирано състояние, WAIT_TIMEOUT, ако времето за изчакване е изтекло, и WAIT_ABANDONED, ако мютексът не е бил освободен преди прекратяването на притежаващата нишка. Ако времето за изчакване е посочено като нула, функцията се връща незабавно, в противен случай изчаква определеното време. Ако състоянието на обекта стане сигнализирано преди изтичането на това време, функцията ще върне WAIT_OBJECT_0, в противен случай функцията ще върне WAIT_TIMEOUT. Ако символната константа INFINITE е посочена като време, тогава функцията ще чака безкрайно време, докато състоянието на обекта стане сигнализирано.

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

Критични участъци

Обектно критична секция помага на програмиста да изолира секцията от код, където нишката има достъп до споделен ресурс и предотвратява едновременното използване на ресурса. Преди да използва ресурса, нишката влиза в критичната секция (извиква функцията EnterCriticalSection). Ако някоя друга нишка след това се опита да влезе в същата критична секция, нейното изпълнение ще спре, докато първата нишка напусне секцията с извикване на LeaveCriticalSection. Използва се само за нишки в един процес. Редът за влизане в критичния раздел не е дефиниран.

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

Пример. Синхронизиране на нишки с помощта на критични секции.

#включи #включи CRITICAL_SECTION cs; int a; ДРЪЖКА hThr; неподписан дълъг uThrID; void Thread(void* pParams) ( int i, num = 0; while (1) ( EnterCriticalSection(&cs); for (i=0; i<5; i++) a[i] = num; num++; LeaveCriticalSection(&cs); } } int main(void) { InitializeCriticalSection(&cs); hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID); while(1) { EnterCriticalSection(&cs); printf("%d %d %d %d %d\n", a, a, a, a, a); LeaveCriticalSection(&cs); } return 0; }

Взаимно изключване

Обекти за взаимно изключване (mutexes, mutex - от MUTual EXclusion) ви позволяват да координирате взаимното изключване на достъпа до споделен ресурс. Сигнализираното състояние на даден обект (т.е. състоянието "настроено") съответства на момента във времето, когато обектът не принадлежи към никоя нишка и може да бъде "заловен". Обратно, състоянието "нулиране" (не е сигнализирано) съответства на момента, в който дадена нишка вече притежава този обект. Достъпът до обект се предоставя, когато нишката, която притежава обекта, го освободи.

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

Множество нишки могат да придобият манипулатор на един и същ мютекс, което прави комуникацията между процесите възможна. Можете да използвате следните механизми за този подход:

  • Дъщерен процес, създаден с помощта на функцията CreateProcess, може да наследи манипулатора на mutex, ако параметърът lpMutexAttributes е зададен, когато mutex е създаден от функцията CreateMutex.
  • Нишка може да получи дубликат на съществуващ мютекс с помощта на функцията DuplicateHandle.
  • Нишката може да посочи името на съществуващ мютекс, когато извиква функциите OpenMutex или CreateMutex.

За да декларирате mutex, притежаван от текущата нишка, трябва да се извика една от чакащите функции. Нишката, която притежава обекта, може да го „улови“ многократно, колкото пъти пожелае (това няма да доведе до самозаключване), но ще трябва да го освободи толкова пъти, като използва функцията ReleaseMutex.

За да синхронизирате нишките на един процес, е по-ефективно да използвате критични секции.

Пример. Синхронизиране на нишки с помощта на mutexes.

#включи #включи HANDLE hMutex; int a; ДРЪЖКА hThr; неподписан дълъг uThrID; void Thread(void* pParams) ( int i, num = 0; while (1) ( WaitForSingleObject(hMutex, INFINITE); for (i=0; i<5; i++) a[i] = num; num++; ReleaseMutex(hMutex); } } int main(void) { hMutex=CreateMutex(NULL, FALSE, NULL); hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID); while(1) { WaitForSingleObject(hMutex, INFINITE); printf("%d %d %d %d %d\n", a, a, a, a, a); ReleaseMutex(hMutex); } return 0; }

Разработки

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

Функцията CreateEvent създава обект за събитие, SetEvent - задава събитието в състояние на сигнала, ResetEvent - нулира събитието. Функцията PulseEvent задава събитието и след възобновяване на нишките, чакащи това събитие (всички с ръчно нулиране и само една с автоматично), го нулира. Ако няма чакащи нишки, PulseEvent просто нулира събитието.

Пример. Синхронизиране на нишки с помощта на събития.

#включи #включи HANDLE hEvent1, hEvent2; int a; ДРЪЖКА hThr; неподписан дълъг uThrID; void Thread(void* pParams) ( int i, num = 0; while (1) ( WaitForSingleObject(hEvent2, INFINITE); for (i=0; i<5; i++) a[i] = num; num++; SetEvent(hEvent1); } } int main(void) { hEvent1=CreateEvent(NULL, FALSE, TRUE, NULL); hEvent2=CreateEvent(NULL, FALSE, FALSE, NULL); hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID); while(1) { WaitForSingleObject(hEvent1, INFINITE); printf("%d %d %d %d %d\n", a, a, a, a, a); SetEvent(hEvent2); } return 0; }

семафори

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

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

Пример. Синхронизиране на нишки с помощта на семафори.

#включи #включи ДРЪЖКА hSem; int a; ДРЪЖКА hThr; неподписан дълъг uThrID; void Thread(void* pParams) ( int i, num = 0; while (1) ( WaitForSingleObject(hSem, INFINITE); for (i=0; i<5; i++) a[i] = num; num++; ReleaseSemaphore(hSem, 1, NULL); } } int main(void) { hSem=CreateSemaphore(NULL, 1, 1, "MySemaphore1"); hThr=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Thread,NULL,0,&uThrID); while(1) { WaitForSingleObject(hSem, INFINITE); printf("%d %d %d %d %d\n", a, a, a, a, a); ReleaseSemaphore(hSem, 1, NULL); } return 0; }

Защитен достъп до променливи

Има редица функции, които ви позволяват да работите с глобални променливи от всички нишки, без да се притеснявате за синхронизацията, защото. тези функции се грижат за това сами - тяхното изпълнение е атомарно. Това са функциите InterlockedIncrement, InterlockedDecrement, InterlockedExchange, InterlockedExchangeAdd и InterlockedCompareExchange. Например функцията InterlockedIncrement атомарно увеличава стойността на 32-битова променлива с единица, което е полезно за различни броячи.

За да получите пълна информация относно предназначението, употребата и синтаксиса на всички WIN32 API функции, трябва да използвате системата за помощ на MS SDK, която е част от средите за програмиране Borland Delphi или CBuilder, както и MSDN, който се доставя като част от системата за програмиране Visual C.


За програми, които използват множество нишки или процеси, е необходимо всички те да изпълняват възложените им функции в желаната последователност. В средата на Windows 9x за тази цел се предлага да се използват няколко механизма, които осигуряват безпроблемната работа на нишките. Тези механизми се наричат механизми за синхронизация.Да предположим, че разработвате програма, в която две нишки работят паралелно. Всяка нишка има достъп до една споделена глобална променлива. Една нишка, всеки път, когато тази променлива е достъпна, я увеличава, а втората нишка я намалява. При едновременна асинхронна работа на нишки неизбежно възниква следната ситуация: - първата нишка е прочела стойността на глобална променлива в локална; - ОС го прекъсва, тъй като разпределеното за него време на процесора е приключило и прехвърля управлението на втория поток; - втората нишка също прочете стойността на глобалната променлива в локална, намали я и записа новата стойност обратно; - ОС отново прехвърля контрола на първата нишка, която, без да знае нищо за действията на втората нишка, увеличава локалната си променлива и записва стойността й в глобалната. Очевидно промените, направени от втората нишка, ще бъдат загубени. За да се избегнат подобни ситуации, е необходимо да се отдели използването на споделени данни във времето. В такива случаи се използват механизми за синхронизация, които осигуряват правилната работа на множество нишки. Инструменти за синхронизация в ОСWindows: 1) критичен участък (КритиченРаздел) е обект, който принадлежи на процеса, а не на ядрото. Това означава, че не може да синхронизира нишки от различни процеси. Има и функции за инициализация (създаване) и изтриване, влизане и излизане от критичен раздел: създаване - InitializeCriticalSection(...), изтриване - DeleteCriticalSection(...), влизане - EnterCriticalSection(...), излизане - LeaveCriticalSection (...). Ограничения: тъй като не е обект на ядрото, той не е видим за други процеси, т.е. можете да защитите само нишките на вашия собствен процес. Критичната секция анализира стойността на специална променлива на процеса, която се използва като флаг, за да предотврати едновременното изпълнение на част от кода на множество нишки. Сред синхронизиращите обекти критичните секции са най-простите. 2) мютекспроменливизключвам. Това е обект на ядрото, има име, което означава, че те могат да се използват за синхронизиране на достъпа до споделени данни от няколко процеса, по-точно от нишки на различни процеси. Никоя друга нишка не може да придобие mutex, който вече е собственост на една от нишките. Ако мутекс защитава някои споделени данни, той ще може да изпълнява функцията си само ако всяка нишка проверява състоянието на този мутекс преди достъп до тези данни. Windows третира mutex като споделен обект, който може да бъде сигнализиран или нулиран. Сигнализираното състояние на mutex показва, че е заето. Нишките трябва независимо да анализират текущото състояние на мютексите. Ако искате мютексът да бъде достъпен от нишки от други процеси, трябва да му дадете име. Функции: CreateMutex(name) - създаване, hnd=OpenMutex(name) - отваряне, WaitForSingleObject(hnd) - изчакване и заемане, ReleaseMutex(hnd) - освобождаване, CloseHandle(hnd) - затваряне. Може да се използва за защита срещу рестартиране на програми. 3) семафор -семафор. Обектът на ядрото „семафор“ се използва за отчитане на ресурсите и служи за ограничаване на едновременния достъп до ресурс от няколко нишки. С помощта на семафор можете да организирате работата на програмата по такъв начин, че няколко нишки да имат достъп до ресурса едновременно, но броят на тези нишки ще бъде ограничен. При създаването на семафор се посочва максималният брой нишки, които могат едновременно да работят с ресурса. Всеки път, когато програма има достъп до семафор, броячът на ресурсите на семафора се намалява с единица. Когато стойността на брояча на ресурси стане нула, семафорът е недостъпен. създайте CreateSemaphore, отворете OpenSemaphore, вземете WaitForSingleObject, освободете ReleaseSemaphore 4 ) събитие -събитие. Събитията обикновено просто уведомяват за края на дадена операция, те също са обекти на ядрото. Можете не само да освободите изрично, но има и операция за настройка на събитие. Събитията могат да бъдат ръчни (manual) и единични (single). Едно събитие е по-скоро общ флаг. Едно събитие е в сигнализирано състояние, ако е зададено от някоя нишка. Ако програмата изисква само една от нишките да реагира на нея в случай на събитие, докато всички останали нишки продължават да чакат, тогава се използва едно събитие. Ръчното събитие не е просто общ флаг в множество нишки. Той изпълнява малко по-сложни функции. Всяка нишка може да зададе това събитие или да го нулира (изчисти). След като дадено събитие е зададено, то ще остане в това състояние за произволно дълго време, независимо от това колко нишки чакат събитието да бъде зададено. Когато всички нишки, чакащи това събитие, получат съобщение, че събитието е настъпило, то автоматично ще се нулира. Функции: SetEvent, ClearEvent, WaitForEvent. Типове събития: 1) събитие за автоматично нулиране: WaitForSingleEvent. 2) събитие с ръчно нулиране (ръчно), тогава събитието трябва да се нулира: ReleaseEvent. Някои теоретици отделят друг обект за синхронизация: WaitAbleTimer е обект на ядрото на ОС, който независимо превключва в свободно състояние след определен интервал от време (будилник).

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

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

Има две общи състояния, в които може да бъде една задача. Първо, задачата може да бъдат извършени(или бъде готов за изпълнение веднага щом има достъп до ресурсите на процесора). Второ, задачата може да бъде блокиран.В този случай изпълнението му се спира, докато ресурсът, от който се нуждае, не бъде освободен или не настъпи определено събитие.

Windows има специални услуги, които ви позволяват да ограничите достъпа до споделени ресурси по определен начин, тъй като без помощта на операционната система отделен процес или нишка не може да определи сам дали има единствен достъп до даден ресурс. Операционната система Windows съдържа процедура, която в една непрекъсната операция проверява и, ако е възможно, задава флага за достъп до ресурса. На езика на разработчиците на операционни системи такава операция се нарича проверка и инсталиране операция. Извикват се флаговете, използвани за осигуряване на синхронизация и контрол на достъпа до ресурси семафори(семафор). Win32 API осигурява поддръжка за семафори и други обекти за синхронизация. MFC библиотеката също включва поддръжка за тези обекти.

Обекти за синхронизация и mfc класове

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

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

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

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

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

Критичните секции, за разлика от други типове обекти за синхронизация, се използват само за синхронизиране на нишки в рамките на един процес. Други типове обекти могат да се използват за синхронизиране на нишки в рамките на процес или за синхронизиране на процеси.

В MFC механизмът за синхронизация, предоставен от интерфейса Win32, се поддържа чрез следните класове, извлечени от класа CSyncObject:

    CCriticalSection- реализира критичен раздел.

    CEvent- имплементира обекта събитие

    CMutex- реализира изключителен семафор.

    CSemaphore- реализира класически семафор.

В допълнение към тези класове, MFC дефинира и два спомагателни класа за синхронизация: CSingleLockи CMultiLock. Те контролират достъпа до обекта за синхронизация и съдържат методите, използвани за предоставяне и освобождаване на такива обекти. Клас CSingleLockконтролира достъпа до един обект за синхронизация и класа CMultiLock- към няколко обекта. По-нататък ще разгледаме само класа CSingleLock.

Когато се създаде някакъв обект за синхронизация, достъпът до него може да се контролира с помощта на класа CSingleLock. За да направите това, първо трябва да създадете обект от тип CSingleLockс помощта на конструктора:

CSingleLock(CSyncObject* pObject, BOOL bInitialLock = FALSE);

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

След като бъде създаден обект от тип CSingleLock, достъпът до обекта, към който сочи параметърът pObject, може да се контролира с помощта на две функции: ключалкаи Отключиклас CSingleLock.

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

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

При работа с клас CSingleLockОбщата процедура за контролиране на достъпа до ресурс е следната:

    създайте обект от тип CSyncObj (например семафор), който ще се използва за контрол на достъпа до ресурса;

    като използвате създадения обект за синхронизация, създайте обект от тип CSingleLock;

    извикайте метода Lock, за да получите достъп до ресурса;

    направете обаждане до ресурса;

    извикайте метода Unlock, за да освободите ресурса.

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