Главная страница

Лабораторная работа 2. Лабораторная работа 2


Скачать 143 Kb.
НазваниеЛабораторная работа 2
Дата28.04.2021
Размер143 Kb.
Формат файлаdoc
Имя файлаЛабораторная работа 2.doc
ТипЛабораторная работа
#199875



Создание вторичных потоков в процессе

Лабораторная работа № 2




1. Цель работы



Изучение организации мультипрограммирования в ОС Windows, приобретение практических навыков использования средств API Windows для создания многопоточных приложений.

2. Основные сведения о потоках



Поток (thread) – это последовательность исполнения кода в процессе. При инициализации процесса система всегда создает первичный поток.

Большинство приложений обходится единственным первичным потоком, однако процессы могут создавать дополнительные потоки, что позволяет им эффективнее выполнять свою работу. Потоки всегда создаются в контексте какого-либо процесса, исполняют код и манипулируют данными в адресном пространстве процесса. Поэтому, если два или более потоков выполняется в контексте одного процесса, все они делят одно адресное пространство.
Процесс ничего не исполняет, он просто служит контейнером потоков.
Потоки могут исполнять одинаковый код над разными входными данными или по разному манипулировать одними и теми же данными, а также совместно использовать дескрипторы объектов ядра, созданных процессом, поскольку таблица дескрипторов принадлежит процессу и, следовательно, доступна любому его потоку.

Так как потоки расходуют существенно меньше ресурсов, чем процессы, старайтесь решать свои задачи за счет использования дополнительных потоков и избегайте создания новых процессов. Только не принимайте этот совет за жесткое правило — многие проекты лучше реализовать именно на основе множества процессов. Нужно просто помнить об издержках и соразмерять цель и средства.

Обычно в приложении существует один поток, отвечающий за поддержку пользовательского интерфейса - он создает все окна и содержит цикл обработки очереди сообщений. Любые другие потоки в процессе являются рабочими (т. e. отвечают за вычисления, ввод вывод и другие операции) и не создают никаких окон. Поток пользовательского интерфейса, как правило, имеет более высокий приоритет, чем рабочие потоки - это нужно для того, чтобы он всегда быстро реагировал на действия пользователя.

Однако многопоточность следует использовать разумно. Не создавайте несколько потоков только потому, что это возможно. Многие полезные и мощные программы по-прежнему строятся на основе одного первичного потока, принадлежащего процессу.
Входная функция вторичного потока

Каждый поток начинает выполнение с некоторой входной функции. В первичном потоке ею является main() или WinMain(). Если Вы хотите создать вторичный поток, в нем тоже должна быть входная функция, шаблон которой выглядит так:
DWORD WINAPI ThreadFunc(PVOID pvParam)
{
DWORD result = 0;

.................

return result;
}
Функция потока может выполнять любые задачи. Когда она закончит свою работу и вернет управление, поток завершится.

В отличие от входной функции первичного потока, функцию потока можно назвать как угодно. Но если в программе есть несколько функций потоков, им можно присвоить разные имена.

  • Функциям потоков передается единственный параметр (адрес некоторого объекта), смысл которого определяется Вами, а не операционной системой. Например, так можно передать целое или вещественное число, указатель на объект класса и.т.п.

  • Функция потока должна возвращать значение, которое будет использоваться как код завершения потока.

  • Функции потоков должны по мере возможности обходиться своими параметрами и локальными переменными. Так как к статической или глобальной переменной могут одновременно обратиться несколько потоков, есть риск повредить ее содержимое (Race Condition).


Функция CreateThread

Если Вы хотите создать дополнительные потоки, нужно вызвать из первичного потока функцию:
HANDLE CreateThread(

PSECURITY_ATTRIBUTES psa,

DWORD cbStack,
PTHREAD_START_ROUTINE pfnStartAddr,


PVOID pvParam,

DWORD fdwCreate,

PDWORD pdwThreadID

);
При каждом вызове этой функции система создает объект ядра «поток». Это не сам поток, а структура данных, которая используется операционной системой для управления потоком и хранит статистическую информацию о потоке. Так что объект ядра "поток" - полный аналог объекта ядра "процесс".

Система выделяет память под стек потока из адресного пространства процесса. Новый поток выполняется в контексте того же процесса, что и родительский поток. Поэтому он получает доступ ко всем описателям объектов ядра, всей памяти и стекам всех потоков в процессе. За счет этого потоки в рамках одного процесса могут легко взаимодействовать друг с другом, но при этом могут легко друг другу навредить.

Параметр psa

Параметр psa является указателем на структуру SECURITY_ATTRIBUTES. Если Вы хотите, чтобы объекту ядра "поток" были присвоены атрибуты защиты по умолчанию (что чаще всего и бывает), передайте в этом параметре NULL.
Параметр cbStack

Этот параметр определяет, какую часть адресного пространства поток сможет использовать под свой стек. Если Выпередаете в параметре cbStack ненулевое значение, функция резервирует всю указанную Вами память.Если же Вы передаете в параметре cbStack нулевое значение, CreateThread создает стандартный стек для нового потока, используя информацию, встроенную компоновщиком в exe-файл.
Параметры pfnStartAddr и pvParam

Параметр pfnStartAddr определяет адрес функции потока, с которой должен будет начать работу создаваемый поток, а параметр pvParam будет передан в виде параметра рvРаrат функции потока. CreateThread лишьпередает этот параметр по эстафете той функции, с которой начинается выполнение создаваемого потока. Таким образом, данный параметр позволяет передавать функции потока какое-либо инициализирующее значение. Оно может быть или просто числовым значением, или указателем на структуру данных с дополнительной информацией, но лучше всего этот параметр использовать для передачи указателя на объект класса.

Вполне допустимо и даже полезно создавать несколько потоков, у которых в качестве входной точки используется адрес одной и той же функции. Например, можно реализовать Web-сервер, который обрабатывает каждый клиентский запрос в отдельном потоке. При создании каждому потоку передается свое значение рvParam.

Учтите, что Windows — операционная система с вытесняющей многозадачностью, а следовательно, новый поток и поток, вызвавший CreateThread, могут выполняться одновременно. В связи с этим возможны проблемы.
Параметр fdwCreate

Этот параметр определяет дополнительные флаги, управляющие созданием потока. Он принимает одно из двух значений: 0 (исполнение потока начинается немедленно) или CREATE_SUSPENDED. В последнем случае система создает поток, инициализирует его и приостанавливает до последующих указаний.

Флаг CREATE_SUSPENDED позволяет программе изменить какие-либо свойства потока перед тем, как он начнет выполнять код, например, изменить приоритет потока.
Параметр pdwThreadlD

Последний параметр функции CreateThread — это адрес переменной типа DWORD, в которой функция возвращает идентификатор, приписанный системой новому потоку. В этом параметре можно передавать NULL (обычно так и делается). Тем самым Вы сообщаете функции, что Вас не интересует идентификатор потока.
Пример создания вторичного потока

HANDLE hThread;

//-------------------------------------

// Функция - точка входа нового потока (нити)

//-------------------------------------

DWORD CALLBACK ThreadFunc(void* P)

{

..............................

.........................

return 0;

}
//---------------------------------------------------------------------------

// создание потока
hThread = CreateThread(0,0,ThreadFunc,0,0,NULL);


Завершение потока

Поток можно завершить четырьмя способами:

  • функция потока возвращает управление (рекомендуемый способ),

  • поток самоуничтожается вызовом функции ExitThread (нежелательный способ);

  • один из потоков данного или стороннего процесса вызывает функцию TerminateThread (нежелательный способ);

  • завершается процесс, содержащий данный поток (тоже нежелательно).


Все нежелательные способы являются «аварийными» способами завершения потока, нужно стараться не использовать их в штатной работе.

Возврат управления функцией потока

Функцию потока следует проектировать так, чтобы поток завершался только после того, как она возвращает управление. Это единственный способ, гарантирующий корректную очистку всех ресурсов, принадлежавших Вашему потоку. При этом:

  • любые С++ - объекты, созданные данным потоком, уничтожаются соответствующими деструкторами;

  • система корректно освобождает память, которую занимал стек потока;

  • система устанавливает код завершения данного потока , который возвращает функция потока;

  • счетчик пользователей данного объекта ядра "поток" уменьшается на 1



Функция ExitThread

Поток может завершить себя принудительно, вызвав:
void ExitThread(DWORD dwExitCоde);
В параметр dwExitCode Вы помещаете значение, которое система рассматривает как код завершения потока. Возвращаемого значения у этой функции нет, потому что после ее вызова поток перестает существовать. При этом освобождаются все ресурсы операционной системы, выделенные данному потоку, но C/C++ - pеcypcы (например, объекты, созданные из С++-классов) не очищаются, не вызываются их деструкторы. Именно поэтому лучше возвращать управление из функции потока, чем изнутри него вызывать функцию ExitThread.

Функция TerminateThread

Вызов этой функции также завершает поток:

bool TerminateThread(HANDLE hThread, DWORD dwExitCode);
Эта функция завершает поток, указанный в параметре hThread. В параметр dwExitCode заносится значение, которое система рассматривает как код завершения потока. После того как поток будет уничтожен, счетчик пользователей его объекта ядра "поток» уменьшится на 1.

Корректно написанное приложение не должно вызывать эту функцию, поскольку поток не получает никакого уведомления о завершении; из-за этого он не может выполнить должную очистку ресурсов.
Функции ExitProcess()и TerminateProcess(), рассмотренные ранее, прекращают выполнение всех потоков, принадлежавших завершаемому процессу. При этом гарантируется высвобождение любых выделенных процессу ресурсов, в том числе стеков потоков. Однако эти две функции уничтожают потоки принудительно — так, будто для каждого из них вызывается функция TerminateThread() . А это означает, что очистка проводится некорректно, деструкторы С++ - объектов не вызываются, данные на диск не сбрасываются и т. д.

Приостановка и возобновление потоков

В объекте ядра "поток" имеется переменная — счетчик числа простоев данного потока. При вызове CreateProcess или CreateThread он инициализируется значением, равным 1, которое запрещает системе выделять новому потоку процессорное время. Такая схема весьма разумна: сразу после создания поток не готов к выполнению, ему нужно время для инициализации.

После того как поток полностью инициализирован, CreateProcess или CreateThread проверяет, не передан ли ей флаг CREATE_SUSPENDED, и если да, то возвращает управление, оставив поток в приостановленном состоянии. В ином случае счетчик простоев обнуляется, и поток включается в число планируемых — если только он не ждет какого-то события (например, ввода с клавиатуры).

Создав поток в приостановленном состоянии, Вы можете настроить некоторые его свойства, например, приоритет. Закончив настройку, Вы должны разрешить выполнение потока. Для этого вызовите ResumeThread и передайте дескриптор потока, возвращенный функцией CreateThread.
DWORD ResumeThread(HANDLE hThread);
Если вызов ResumeThread прошел успешно, она возвращает предыдущее значение счетчика простоев данного потока; в ином случае — 0xFFFFFFFF.

Выполнение отдельного потока можно приостанавливать несколько раз. Если поток приостановлен 3 раза, то и возобновлен он должен быть тоже 3 раза — лишь тогда система выделит ему процессорное время.

Выполнение потока можно приостановить не только при его создании с флагом CREATE_SUSPENDED, но и вызовом SuspendThread.
DWORD SuspendThread(HANDLE hThread);
Любой поток может вызвать эту функцию и приостановить выполнение другого потока, если известен его дескриптор.
Приостановить свое выполнение поток способен сам, а возобновить себя

без посторонней помощи — нет.
Как и ResumeThread, функция SuspendThread возвращает предыдущее значение счетчика простоев данного потока. Поток можно приостанавливать не более чем MAXIMUM_SUSPEND_COUNT раз (в файле WinNT.h это значение определено как 127).

Создавая реальное приложение, будьте осторожны с вызовами SuspendThread, так как нельзя заранее сказать, чем будет заниматься его поток в момент приостановки. Например, он пытается выделить память из кучи и поэтому заблокировал к ней доступ. Тогда другим потокам, которым тоже нужна динамическая память, придется ждать его возобновления. SuspendThread безопасна только в том случае, когда Вы точно знаете, что делает (или может делать) поток, и предусматриваете все меры для исключения вероятных проблем и взаимной блокировки потоков.

Функция Sleep

Поток может сообщить системе не выделять ему процессорное время на определенный период, вызвав:
void Sleep(DWORD dwMilliseconds);
Эта функция приостанавливает поток на dwMilliseconds миллисекунд. Отметим несколько важных моментов:

  • Вызывая Sleep, поток добровольно отказывается от остатка выделенного ему кванта времени.

  • Система прекращает выделять потоку процессорное время на период, примерноравный заданному. Если Вы укажете остановить поток на 100 мс, приблизительно на столько он и "заснет", хотя не исключено, что его сон продлится на несколько миллисекунд секунд или даже секунд больше. Вспомните, Windows не является системой реального времени. Ваш поток может возобновиться и в заданный момент, но это зависит от того, какая ситуация сложится в системе к тому времени.

  • Вы можете вызвать Sleep и передать в dwMilliseconds значение INFINITE, вообще запретив планировать поток. Но это не очень практично — куда лучше корректно завершить поток, освободив его стек и объект ядра.

  • Вы можете вызвать Sleep и передать в dwMilliseconds нулевое значение. Тогда Вы откажетесь от остатка своего кванта времени и заставите систему подключить к процессору другой поток. Однако система может снова запустить Ваш поток, если других планируемых потоков с тем же приоритетом нет.

3. Критическая секция


Критическая секция(Critical Section) - это объект, позволяющий обеспечить в каждом потоке одного процесса монопольный доступ к определенному участку кода, в котором выполняется работа с разделяемым ресурсом. Следовательно, критическая секция должна защищать такой кодовый участок в каждом потоке, который работает с данным ресурсом.

Разумеется, система может в любой момент вытеснить данный поток и подключить к процессору другой, но ни один из потоков, которым нужен занятый ресурс, не получит процессорное время до тех пор, пока поток, работающий с ресурсом, не выйдет за границы критической секции.
Предварительно критическую секцию следует объявить как глобальный объект:
TRTLCriticalSection Sect1; // Критическая секция как структура
и инициализировать:
InitializeCriticalSection(&Sect1);
Охраняемая ею часть программы – это блок кода, который начинается вызовом функции EnterCriticalSection и заканчивается вызовом LeaveCriticalSection:

EnterCriticalSection (&cs);
< Защищаемый блок кода >
LeaveCriticalSection (&cs);
Если объект «Критическая секция» больше не нужен, его следует удалить:
DeleteCriticalSection(&Sect1);
Пример использования критической секции в головной функции вторичного потока:
DWORO WINAPI SomeThread(PVOID pvParam)
{

for(int k = 0; k < kmax; k++)

{
EnterCriticalSection(&Sect1);
< Защищаемый блок кода >


LeaveCriticalSection(&Sect1);

}

return 0 ;
}

4. Синхронизация потоков с использованием объектов ядра


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

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

Следующие объекты ядра используются для синхронизации доступа любых потоков к разделяемым ресурсам:

  • мьютексы

  • события

  • семафоры

  • таймеры ожидания


Эти объекты, подобно критической секции, «охраняют» вход в некоторый участок кода и бывают в свободном или занятом состоянии. Последнее означает, что их «захватил» некоторый поток, и другие потоки, желающие войти в свой кодовый участок, «охраняемый» данным объектом, должны ждать его освобождения.

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

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

Потоки обычно «усыпляют себя» до освобождения синхронизирующего объекта с помощью функции:
DWORD WaitForSingleObject(HANDLE hObject, DWORD dwTimeOut);
где hObject –дескриптор объекта, dwMilliseconds указывает, сколько времени (в миллисекундах) поток готов ждать освобождения объекта.
В качестве значения параметра dwTimeOut используют три варианта:


dwTimeOut

Действия

0

Функция только проверяет состояние объекта (занят или свободен) и сразу же

возвращается

Число

МСек

Функция ожидает освобождения объекта не более указанного времени

  INFINITE

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

Функция WaitForSingleObject может возвращать одно из следующих значений:

Возвращаемое значение

Описание

WAIT_TIMEOUT

Объект не перешел в свободное состояние, но интервал времени

истек

WAIT_ABANDONED

Ожидаемый объект является мьютексом, который не был освобожден владеющим им потоком перед окончанием этого потока. Объект мьютекс автоматически переведен системой в свободное состояние.

WAIT_OBJECT_0

Объект перешел в свободное состояние

WAIT_FAILED

Произошла ошибка, причину которой можно узнать, вызвав GetLastError


Вот пример ожидания освобождения некоторого обобщенного объекта с дескриптором hObject:
DWORD dw = WaitForSlngleObject(hObject, 5000);

switch (dw)
{
case WAIT_OBJECT_0: // объект свободен
break;


case WAIT_TIMEOUT: // объект не освободился в течение 5000 мс
break;


case WAIT_FAILED: // некорректный вызов функции
break;
}

Мьютексы

Объекты ядра «мьютексы» гарантируют любым потокам монопольный доступ к разделяемому ресурсу и ведут себя точно так же, как и критические секции. Мьютекс позволяет синхронизировать доступ к ресурсу для потоков из разных процессов; при этом можно задать максимальное время ожидания доступа к ресурсу.

Для использования объекта-мьютекса один из процессов должен сначала создать его вызовом функции:
HANDLE CreateMutex(

PSECURITY_ATTRIBUTES psa,

BOOL fInitialOwner,

PCTSTR pszName

);
Параметр psaобычно равен NULL.

Параметр fInitialOwner определяет начальное состояние мъютекса. Если в нем передается false (что обычно и бывает), мьютекс изначально не захвачен никаким потоком и находится в свободном состоянии. Если же в нем передается true, мьютекс считается захваченным потоком, создавшим его.

Параметр pszNameимя мьютекса (char-строка), возможно пустое.

Функция возвращает дескриптор мьютекса, который имеет смысл только в данном процессе.

Любой другой процесс может получить свой «процессо-зависимый» дескриптор существующего объекта «мьютекс», вызвав OpenMutex:
HANDLE OpenMutex( DWORD fdwAccess, BOOL bInheritHandle, PCTSTR pszName);
Чтобы это выполнить, нужно знать имя мьютекса, которое просвоено ему при создании.

Поток получает доступ к разделяемому ресурсу, вызывая функцию WaitForSingleObject и передавая ей дескриптор мьютекса, который охраняет этот ресурс. Если функция определяет, что мьютекс занят, вызывающий поток переводится в состояние ожидания.

Когда ожидание мьютекса потоком успешно завершается, последний получает монопольный доступ к защищенному ресурсу. Все остальные потоки, пытающиеся обратиться к этому ресурсу, переходят в состояние ожидания.

Когда поток, занимающий ресурс, заканчивает с ним работать, он должен освободить мьютекс вызовом функции
BOOL ReleaseMutex(HANDLE hMutex);
Объект-мьютекс отличается от остальных объектов ядра тем, что занявшему его потоку передаются права на владение им. Прочие объекты могут быть либо свободны, либо заняты — вот, собственно, и все. А объекты-мьютексы способны еще и запоминать, какому потоку они принадлежат. Если какой-то посторонний поток попытается освободить мьютекс вызовом функции ReleaseMutex, то она, проверив идентификаторы потоков и обнаружив их несовпадение, ничего делать не станет, а просто вернет FALSE.
Если поток, которому принадлежит мьютекс, завершится, не успев его освободить? В таком случае система считает, что произошел отказ от мьютекса, и автоматически переводит его в свободное состояние. Тогда Wait-функция возвращает WAIT_ABANDONED вместо WAIT_OBJECT_0, сигнализируя тем самым, что мьютекс свободен, но освобожден некорректно. Выяснить, что сделал с защищенными данными бывший владелец объекта-мьютекса, увы, невозможно.

Обычно не проверяют возвращаемое значение на WAIT_ABANDONED, потому что такое завершение потоков происходит очень редко.
События

Объект «событие» не хранит информацию о том, кто его занял.

Переключение состояния объекта осуществляется вызовом функций:
ResetEvent(hEvent); // Перевод в занятое состояние

SetEvent(hEvent); // Перевод в свободное состояние
События бывают двух типов: с «ручным захватом» и «автоматическим захватом».
События с автоматическим захватом переводятся в занятое состояние функцией WaitForSingleObject(...), не требуя явного вызова ResetEvent().
Для событий с ручным захватом требуется явно вызывать ResetEvent().
Тип и начальное состояние объекта «событие» задаются при его создании:

HANDLE CreateEvent(
PSECURITY_ATTRIBUTES psa, // Обычно NULL


BOOL ManualReset, // ручной захват - авто

BOOL InitialState, // исходно свободен-занят

PCTSTR pszName // Имя события

);





Параметр



Значение



Смысл

ManualReset

true

с ручным захватом

false

с автозахватом

InitialState

true

изначально свободно

false

изначально занято


Если имя события == NULL, его можно использовать только в рамках одного процесса.

Функция возвращает дескриптор созданного объекта, который имеет смысл в рамках данного процесса. Потоки других процессов обычно получают свой дескриптор объекта вызовом функции OpenEventcпередачей в параметре pszName имени объекта:
HANDLE OpenEvent( DWORD fdwAccess, BOOL fInherit, PCTSTR pszName);
Функция возвращает дескриптор только в том случае, когда событие уже создано каким-то процессом, в противном случае возвращается NULL. Ненужный более объект «событие» следует закрыть вызовом CloseHandle(hEvent).
Семафоры

Семафоры предназначены для ограничения числа потоков, имеющих одновременный доступ к какому-либо ресурсу.
Семафор представляет собой счетчик, который считается свободным, если значение счетчика больше нуля, и занятым при нулевом значении.
Объект ядра «семафор» создается вызовом функции:

HANDLE CreateSemaphore(

PSECURITY_ATTRIBUTE psa,

LONG InitialCount,

LONG MaximumCount,

PCTRTR pszName

);
Параметр MaximumCountопределяет максимальное число потоков, которые могут одновременно пользоваться разделяемым ресурсом.

Параметр InitialCount- счетчик числа потоков, пользующихся ресурсом в данный момент.

Любой процесс может получить свой «процессо-зависимый» дескриптор существующего объекта «семафор», вызвав OpenSemaphore:
HANDLE OpenSemaphore(DWORD fdwAccess, BOOL bInheritHandle, PCTSTR pszName);
где обычно fdwAccess = SEMAPHORE_ALL_ACCESS, bInheritHandle = true.
Функция возвращает дескриптор только в том случае, когда семафор уже создан каким-то процессом, в противном случае возвращается NULL. Работа с ресурсом, охраняемым семафором, строится так. Создаем объект «семафор»:
long MaximumCount = .... ;

HANDLE hSem = CreateSemaphore(NULL, MaximumCount, MaximumCount,”Sem1”);

Это не опечатка – обычно текущее значение счетчика исходно равно максимально допустимому. При каждом успешном вызове функции WaitForSingleObject() текущее значение уменьшается на 1.
Другой процесс может получить свой дескриптор:

HANDLE hSemaphor = OpenSemaphore(SEMAPHORE_ALL_ACCESS,true, “Sem1”);

Запрос доступа к охраняемому ресурсу оформляется так:
DWORD TimeOut = 2000;
DWORD Result = WaitForSingleObject(hSem, TimeOut);

if(Result == WAIT_OBJECT_0) // Доступ получен

{

< Операции с ресурсом >

ReleaseSemaphore(hSem,1,NULL); // Инкремент счетчика ресурсов - (я

// освободил)

}

else

{

// Доступ к ресурсу не получен

// Можно делать что-нибудь другое

}

Функция WaitForSingleObject автоматически уменьшает значение счетчика семафора на 1, если счетчик ненулевой, или переводит поток в режим ожидания до тех пор, пока другой поток не увеличит значение счетчика семафора вызовом функции ReleaseSemaphore().
Поток увеличивает значение счетчика текущего числа ресурсов, заканчивая работу с ресурсом и вызывая функцию ReleaseSemaphore(...):
BOOL ReleaseSemaphore( HANDLE hSem,LONG lReleaseCount,

PLONG plPreviousCount);
Параметр l ReleaseCount указывает, на сколько увеличить счетчик текущего числа ресурсов. Это значение должно быть > 0 (обычно 1).

5. Задание на работу


1. Разработать Windows-приложение, содержащее два вторичных потока.


  • В приложении создать глобальную целочисленную переменную.

  • Один вторичный поток прибавляет к этой переменной некоторую константу, а затем ее же вычитает. Другой вторичный поток делает противоположные действия – вычитает из этой переменной ту же константу, а затем ее прибавляет.

  • Указанные действия каждый поток выполняет в цикле заданное число раз (например, 50). После каждого прохождения тела цикла поток выводит на экран текущее значение глобальной переменной (в Memo). Пример операторов тела цикла (С++Builder):


GlobalData = GlobalData + 3;

Sleep(1);

GlobalData = GlobalData - 3;

i = GlobalData;

Form1->Memo1->Lines->Add(IntToStr(i));

Пояснение. Поскольку время выполнения подобного цикла мало, для моделирования асинхронного доступа двух потоков к разделяемому ресурсу (глобальной переменной) в тело цикла добавлен оператор Sleep(1), означающий отказ от остатка кванта времени.




  • Обработчик кнопки ПУСК содержит вызовы функции создания вторичных потоков.


2. Добавить различные объекты синхронизации доступа к разделяемому ресурсу (глобальной переменной), используя их поочередно:

  • без синхронизации

  • критическая секция

  • мьютекс

Оформить выбор типа синхрообъектов посредством элемента RadioGroup.
3. Провести моделирование и доказать что синхронизация доступа работает
Рекомендации:
1. Объекты синхронизации (критическая секция, мьютекс) лучше всего создавать один раз, вначале программы. При выходе из программы освобождать.
2. Обработчик кнопки ПУСК должен корректно работать при втором, третьем и т.д. нажатии. Необходимо отслеживать и освобождать дескрипторы потоков, созданных предыдущим нажатием и закончивших свою работу.
3. Для того, чтобы наглядно смоделировать проблему гонки сигналов при работе потоков без синхронизации, можно поэкспериментировать с различными значениями времени засыпания потоков в функции Sleep().


написать администратору сайта