Для студентов згиа специальности 080403 ”Программное обеспечение




НазваниеДля студентов згиа специальности 080403 ”Программное обеспечение
страница6/34
Дата публикации25.02.2013
Размер3.71 Mb.
ТипМетодическое пособие
uchebilka.ru > Информатика > Методическое пособие
1   2   3   4   5   6   7   8   9   ...   34

Пример. Программа выводит на экран простое диалоговое окно с текстовым сообщением и при этом каждую секунду издает короткий звуковой сигнал. Для воспроизведения этого сигнала используется отдельный программный поток.

Листинг 2.4. Тревога!

#include

#include
// необходимо для обращения к __beginthread

HANDLE mainthread;

void beepthread(void *) {

DWORD xitcode; // код завершения

while (GetExitCodeThread(mainthread, &xitcode)&&xitcode==STILL_ACTIVE) {

MessageBeep(-1);

Sleep(1000);

}

}

void main() {

mainthread=GetCurrentThread();

_beginthread(beepthread,0.NULL);

MessageBox(NULL,"Red Alert","Alert",MB_OK);

}

^ Использование CreateThread

Поток создается в результате вызова CreateThread. Например:

long WINAPI ThreadEntry(LPARAM lparam)

{

//. . .

}

unsign long nThreadID;

HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) ThreadEntry, (void*)szHello, 0, &nThreadID);

В функцию CreateThread передается шесть параметров:

  • Указатель на структуру SECURITY_ATTRIBUTES, которая определяет атрибуты защиты для нового потока.

  • Исходный размер стека для нового потока. Если в качестве параметра передается 0, новый поток получает стек того же размера, что у исходного потока. Обычно это значение удобно использовать как принятое по умолчанию, поскольку Windows будет увеличивать размер стека по мере необходимости.

  • Адрес стартовой функции, с которой поток начнет выполнение.

  • 32-разрядный параметр, передаваемый стартовой функции нового потока.

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

  • Адрес 32-разрядной переменной, в которую заносится идентификатор потока при возврате CreateThread.

Если поток использует функции С-библиотеки исполняющей системы, функцию CreateThread применять не следует.

Создав объект ядра "поток", система выделяет стеку потока память из адресного пространства процесса и записывает в его самую верхнюю часть два значения. (Стеки потоков всегда строятся от старших адресов памяти к младшим) Первое из них является значением параметра pvParam, переданного Вами функции CreateThread, а второе — это содержимое параметра pfnStartAddr, который Вы тоже передаете в Create Thread.



Рис. 2.2 Создание и инициализация потока

У каждого потока собственный набор регистров процессора, называемый контекстом потока. Контекст отражает состояние регистров процессора на момент последнего исполнения потока и записывается в структуру CONTEXT (она определена в заголовочном файле WinNT.h). Эта структура содержится в объекте ядра "поток".

Указатель команд (IP) и указатель стека (SP) — два самых важных регистра в контексте потока. Вспомните: потоки выполняются в контексте процесса. Соответственно эти регистры всегда указывают на адреса памяти в адресном пространстве процесса. Когда система инициализирует объект ядра "поток", указателю стека в структуре CONTEXT присваивается тот адрес, по которому в стек потока было записано значение pfnStartAddr, а указателю команд — адрес недокументированной (и неэкспортируемой) функции BaseThreadStart. Эта функция содержится в модуле Kernel32.dll, где, кстати, реализована и функция CreateThread.

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

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

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

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

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

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

При завершении потока происходит следующее:

  • Освобождаются все описатели User-объектов, принадлежавших потоку. В Windows большинство объектов принадлежит процессу, содержащему поток, из которого они были созданы. Сам поток владеет только двумя User-объектами, окнами и ловушками (hooks). Когда поток, создавший такие объекты, завершается, система уничтожает их автоматически. Прочие объекты разрушаются, только когда завершается владевший ими процесс.

  • Код завершения потока меняется со STILL_ACTIVE на код, переданный в функцию ExitThread или TerminateThread.

  • Объект ядра "поток" переводится в свободное состояние.

  • Если данный поток является последним активным потоком в процессе, завершается и сам процесс.

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

При завершении потока сопоставленный с ним объект ядра "поток" не освобождается до тех пор, пока не будут закрыты все внешние ссылки на этот объект.

^ Планирование потоков, приоритет потоков

Операционная система с вытесняющей многозадачностью должна использовать тот или иной алгоритм, позволяющий ей распределять процессорное время между потоками. Здесь мы рассмотрим алгоритмы, применяемые в Windows 98 и Windows 2000 [3]. Ранее мы уже обсудили структуру CONTEXT, поддерживаемую в объекте ядра "поток", и выяснили, что она отражает состояние регистров процессора на момент последнего выполнения потока процессором. Каждые 20 мс (или около того) Windows просматривает все существующие объекты ядра "поток" и отмечает те из них, которые могут получать процессорное время. Далее она выбирает один из таких объектов и загружает в регистры процессора значения из его контекста. Эта операция называется переключением контекста (context switching). По каждому потоку Windows ведет учет того, сколько раз он подключался к процессору. Этот показатель сообщают специальные утилиты вроде Microsoft Spy++.

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

Таков вкратце механизм планирования работы множества потоков.

Ранее мы отмечали, что поток получает доступ к процессору на 20 мс, после чего планировщик переключает процессор на выполнение другого потока. Так происходит, только если у всех потоков один приоритет, но на самом деле в системе существуют потоки с разными приоритетами, а это меняет порядок распределения процессорного времени.

Каждому потоку присваивается уровень приоритета — от 0 (самый низкий) до 31 (самый высокий). Решая, какому потоку выделить процессорное время, система сначала рассматривает только потоки с приоритетом 31 и подключает их к процессору по принципу карусели. Если поток с приоритетом 31 не исключен из планирования, он немедленно получает квант времени, по истечении которого система проверяет, есть ли еще один такой поток. Если да, он тоже получает свой квант процессорного времени.

Пока в системе имеются планируемые потоки с приоритетом 31, ни один поток с более низким приоритетом процессорного времени не получает. Такая ситуация называется "голоданием" (starvation). Она наблюдается, когда потоки с более высоким приоритетом так интенсивно используют процессорное время, что остальные практически не работают.

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

При загрузке системы создается особый поток — поток обнуления страниц (zero page thread), которому присваивается нулевой уровень приоритета. Ни один поток, кроме этого, не может иметь нулевой уровень приоритета. Он обнуляет свободные страницы в оперативной памяти при отсутствии других потоков, требующих внимания со стороны системы.

Windows поддерживает шесть классов приоритета для процессов: idle (простаивающий), below normal (ниже обычного), normal (обычный), above normal (выше обычного), high (высокий) и realtime (реального времени). Самый распространенный класс приоритета, естественно, — normal; его использует 99% приложений. Классы приоритета показаны в таблице 2.2.

Табл. 2.2. Классы приоритета процессов

Класс приоритета

Описание

Real-time

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

High

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

Above normal

Класс приоритета, промежуточный между normal и high. Это новый класс, введенный в Windows 2000.

Normal

Потоки в этом процессе не предъявляют особых требований к выделению им процессорного времени.

Below normal

Класс приоритета, промежуточный между normal и idle. Это новый класс, введенный в Windows 2000.

Idle

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

Так как же процесс получает класс приоритета? Очень просто. Вызывая CreateProcess, Вы можете указать в ее параметр fdwCreate нужный класс приоритета. Идентификаторы этих классов приведены в таблице 2.3.

Табл. 2.3. Идентификаторы классов приоритетов

Класс
приоритета


Идентификатор

Real-time

REALTIME_PRIORITY_CLASS

High

HIGH_PRIORITY_CLASS

Above normal

ABOVE_NORMAL_PRIORITY_CLASS

Normal

NORMAL_PRIORITY_CLASS

Below normal

BELOW_NORMAL_PRIORITY_CLASS

Idle

IDLE_PRIORITY_CLASS

В системе предусмотрена возможность изменения класса приоритета самим выполняемым процессом — вызовом функции SetPriorityClass:

BOOL SetPriorityClass( HANDLE hProcess, DWORD fdwPriority);

Парная ей функция GetPriorityClass позволяет узнать класс приоритета любого процесса.

DWORD GetPriorityClass(HANDLE hProcess);

Windows поддерживает семь относительных приоритетов потоков: idle (простаивающий), lowest (низший), below normal (ниже обычного), normal (обычный), above normal (выше обычного), highest (высший) и time-critical (критичный по времени). Эти приоритеты относительны классу приоритета процесса. Как обычно, большинство потоков использует обычный приоритет. Относительные приоритеты потоков описаны в таблице 2.4.

Табл. 2.4. Относительные приоритеты потоков

Относительный приоритет потока

Описание

Time-critical

Поток выполняется с приоритетом 31 в классе real-time и с приоритетом 15 в других классах

Highest

Поток выполняется с приоритетом на два уровня выше обычного для данного класса

Above normal

Поток выполняется с приоритетом на один уровень выше обычного для данного класса

Normal

Поток выполняется с обычным приоритетом процесса для данного класса

Below normal

Поток выполняется с приоритетом на один уровень ниже обычного для данного класса

Lowest

Поток выполняется с приоритетом на два уровня ниже обычного для данного класса

Idle

Поток выполняется с приоритетом 16 в классе real-time и с приоритетом 1 в других классах

Итак, Вы присваиваете процессу некий класс приоритета и можете изменять относительные приоритеты потоков в пределах процесса. Заметьте, что нигде речь не шла об уровнях приоритетов 0-31. Разработчики приложений не имеют с ними дела. Уровень приоритета формируется самой системой, исходя из класса приоритета процесса и относительного приоритета потока, а механизм его формирования — как раз то, чем Microsoft не хочет себя ограничивать. И действительно, этот механизм меняется практически в каждой версии системы. В таблице 2.5 показано, как формируется уровень приоритета в Windows 2000 [3]:

Табл. 2.5. Формирование уровней приоритета потоков в Windows 2000

^ Относительный приоритет потока

Класс приоритета процесса

Idle

Below normal

Normal

Above normal

High

Real-time

Time-critical (критичный по времени)

15

15

15

15

15

31

Highest (высший)

6

8

10

12

15

26

Above normal (выше обычного)

5

7

9

11

14

25

Normal (обычный)

4

6

8

10

13

24

Below normal (ниже обычного)

3

5

7

9

12

23

Lowest (низший)

2

4

6

8

11

22

Idle (простаивающий)

1

1

1

1

1

16

Уровни 17-21 и 27-30 в обычном приложении недоступны. Вы можете пользоваться ими, только если пишете драйвер устройства, работающий в режиме ядра. И еще одно: уровень приоритета потока в процессе с классом real-time не может опускаться ниже 16, а потока в процессе с любым другим классом – подниматься выше 15.

Относительный приоритет потока устанавливается вызовом функции:

BOOL SetThreadPriority( HANDLE hThread, int nPriority);

Параметр hThread указывает на поток, чей приоритет Вы хотите изменить, а через nPriority передается один из идентификаторов (см. таблицу 2.6)

Табл. 2.6. Идентификаторы относительных уровней приоритета

Относительный
приоритет потока


Идентификатор

Time-critical

THREAD_PRIORITY_TIME_CRITICAL

Highest

THREAD_PRIORITY_HIGHEST

Above normal

THREAD_PRIORITY_ABOVE_NORMAL

Normal

THREAD_PRIORITY_NORMAL

Below normal

THREAD_PRIORITY_BELOW_NORMAL

Lowest

THREAD_PRIORITY_LOWEST

Idle

THREAD_PRIORITY_IDLE

Функция GetThreadPriority, парная SetThreadPriority, позволяет узнать относительный приоритет потока.

int GetThreadPriority(HANDLE hThread);

Она возвращает один из идентификаторов, показанных в таблице выше.

^ Локальная память потоков

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

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

Windows имеет специальное средство для разрешения данной проблемы. Это так называемая локальная память потоков (thread-local storage – TLS), которая позволяет создавать переменные, поддерживаемые на уровне определенного потока, причем без больших затрат со стороны программиста.

Существует два типа локальной памяти потоков:

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

  • динамическая.

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

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

Чтобы объявить переменную локальной памяти потоков, используйте __declspec(thread) как часть объявления переменной:

__declspec(thread) int nMeals = 0;

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

Динамическая локальная память потоков создается и используется во время функционирования приложения. Хотя она отличается большей сложностью, чем статическая, динамическая локальная память потоков является более универсальной, поскольку может выделяться и освобождаться по мере необходимости.

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

Каждый поток процесса имеет собственную копию локальной памяти потоков.

Для управления локальной памятью потоков предусмотрено четыре функции:

  • TlsAlloc используется для запроса у системы Windows нового индекса локальной памяти потоков;

  • TlsSetValue применяется потоком для записи 32-разрядного значения в собственную копию локальной памяти потоков;

  • TlsGetValue применяется потоком для считывания 32-разрядного значения, сохраненного в локальной памяти потоков;

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

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


      1. ^ Синхронизация потоков

Обеспечение совместной работы нескольких потоков – важная задача, которую приходится решать при разработке многих программ. Потоки должны взаимодействовать друг с другом в двух основных случаях:

  • совместно используя разделяемый ресурс (чтобы не разрушить его);

  • когда нужно уведомлять другие потоки о завершении каких-либо операций.

Примитив синхронизации (synchronization primitive) – это объект, который помогает управлять многопоточным приложением. В Windows доступно пять основных типов примитивов синхнонизации:

  • События (events) – создаваемые программистом объекты, которые используются для сигнализации о разрешении доступа к переменной или процедуре.

  • Критические секции (critical sections) – области кода, доступ к которым может осуществлять только один поток в данный момент времени.

  • Мьютексы (взаимные исключения, mutexes) – объекты Windows, использование которых гарантирует, что только один поток получает доступ к защищенной переменной или коду.

  • Семафоры (semaphores) напоминают взаимные исключения, однако функционируют как счетчики, позволяющие определить количество потоков, осуществляющих доступ к защищенной переменной или коду.

  • ^ Атомарные операции API-уровня (API-level atomic operations), предоставленные системой Windows, позволяют программисту инкрементировать, декрементировать или обменивать содержимое переменной за одну операцию.

Каждый из приведенных выше примитивов синхронизации применяется в определенных ситуациях.

^ Использование взаимно блокированных операций Win32

Самые простые примитивы синхронизации служат для обработки или проверки значения одной или двух переменных. API Win32 включает семь функций, которые гарантированно являются атомарными и безопасными для потоков, даже при наличии нескольких процессоров. Чаще всего используются функции:

  • InterlockedIncrement инкрементирует 32-разрядную переменную и возвращает новое значение.

  • InterlockedDecrement декрементирует 32-разрядную переменную и возвращает новое значение.

  • InterlockedExchange заменяет значение 32-разрядной переменной на новое и возвращает предыдущее.

^ Критические участки

Критический участок (critical section) – это участок кода, который должен использоваться только одним потоком одновременно. Если в одно время два или больше потоков пытаются осуществить доступ к критическому участку, контроль над ним будет предоставлен только одному из потоков, а все остальные будут блокированы (переведены в режим ожидания) до тех пор, пока участок не будет свободен.

В сравнении с другими методами синхронизации (которые будут описаны ниже) создание критического участка является выгодным с точки зрения затрам вычислительных ресурсов. Однако в отличие от других примитивов синхронизации Windows его можно применять только в пределах одного процесса.

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

Для инициализации переменной CRITICAL_SECTION используется функция InitializeCriticalSection:

CRITICAL_SECTION cs;

InitializeCriticalSection(&cs);

Чтобы завладеть критическим участком, поток должен вызвать функцию EnterCriticalSection:

EnterCriticalSection(&cs);

Если критический участок не используется, он обозначается как занятый и поток немедленно продолжает выполнение. Если критический участок уже используется, поток блокируется, пока участок не будет освобожден.

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

LeaveCriticalSection(&cs);

Wait-функции

Wait-функции позволяют потоку в любой момент приостановиться и ждать освобождения какого-либо объекта ядра. Из всего семейства этих функций чаще всего используется WaitForSingleObject:

DWORD WaitForSingleObject( HANDLE hObject,

DWORD dwMilliseconds);

Когда поток вызывает эту функцию, первый параметр, hObject, идентифицирует объект ядра, поддерживающий состояния "свободен-занят". Второй параметр, dwMilliseconds, указывает, сколько времени (в миллисекундах) поток готов ждать освобождения объекта.

Возвращаемое значение функции WaitForSingleObject указывает, почему вызывающий поток снова стал планируемым. Если функция возвращает WAIT_OBJECT_0, объект свободен, а если WAIT_TIMEOUT — заданное время ожидания (таймаут) истекло. При передаче неверного параметра (например, недопустимого описателя) WaitForSingleObject возвращает WAIT_FAILED. Чтобы выяснить конкретную причину ошибки, вызовите функцию GetLastError.

Функция WaitForMultipleObjects аналогична WaitForSingleObject с тем исключением, что позволяет ждать освобождения сразу нескольких объектов или какого-то одного из списка объектов:

DWORD WaitForMultipleObjects(

DWORD dwCount,

CONST HANDLE* phObjects,

BOOL fWaitAll,

DWORD dwMilliseconds);

Параметр dwCount определяет количество интересующих Вас объектов ядра. Его значение должно быть в пределах от 1 до MAXIMUM_WAIT_OBJECTS (в заголовочных файлах Windows оно определено как 64). Параметр phObjects — это указатель на массив описателей объектов ядра.

WaitForMultipleObjects приостанавливает поток и заставляет его ждать освобождения либо всех заданных объектов ядра, либо одного из них. Параметр fWaitAll как раз и определяет, чего именно Вы хотите от функции. Если он равен TRUE, функция не даст потоку возобновить свою работу, пока не освободятся все объекты.
Мьютексы

Объекты ядра "мьютексы" гарантируют потокам взаимоисключающий доступ к единственному ресурсу. Отсюда и произошло название этих объектов (mutual exclusion, mutex). Они содержат счетчик числа пользователей, счетчик рекурсии и переменную, в которой запоминается идентификатор потока.Мьютексы ведут себя точно так же, как и критические секции. Однако, если последние являются объектами пользовательского режима, то мьютексы — объектами ядра. Кроме того, единственный объект-мьютекс позволяет синхронизировать доступ к ресурсу нескольких потоков из разных процессов; при этом можно задать максимальное время ожидания доступа к ресурсу.

Идентификатор потока определяет, какой поток захватил мьютекс, а счетчик рекурсий — сколько раз.

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

HANDLE CreateMutex(

PSECURITY_ATTRIBUTES psa,

BOOL bInitialOwner,

PCTSTR pszName);

Параметр bInitialOwner определяет начальное состояние мьютекса. Если в нем передается FALSE (что обычно и бывает), объект-мьютекс не принадлежит ни одному из потоков и поэтому находится в свободном состоянии. При этом его идентификатор потока и счетчик рекурсии равны 0. Если же в нем передается TRUE, идентификатор потока, принадлежащий мьютексу, приравнивается идентификатору вызывающего потока, а счетчик рекурсии получает значение 1. Поскольку теперь идентификатор потока отличен от 0, мьютекс изначально находится в занятом состоянии.

Любой процесс может получить свой ("процессо-зависимый") описатель существующего объекта "мьютекс", вызвав OpenMutex:

HANDLE OpenMutex(

DWORD fdwAccess,

BOOL bInheritHandle,

PCTSTR pszName);

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

Если Wait-функция определяет, что у мьютекса идентификатор потока не равен 0 (мьютекс занят), вызывающий поток переходит в состояние ожидания. Когда ожидание мьютекса потоком успешно завершается, последний получает монопольный доступ к защищенному ресурсу. Все остальные потоки, пытающиеся обратиться к этому ресурсу, переходят в состояние ожидания. Когда поток, занимающий ресурс, заканчивает с ним работать, он должен освободить мьютекс вызовом функции ReleaseMutex:

BOOL ReleaseMutex(HANDLE hMutex);

Семафоры

Объекты ядра "семафор" используются для учета ресурсов. Как и все объекты ядра, они содержат счетчик числа пользователей, но, кроме того, поддерживают два 32-битных значения со знаком: одно определяет максимальное число ресурсов (контролируемое семафором), другое используется как счетчик текущего числа ресурсов.

Для семафоров определены следующие правила:

  • когда счетчик текущего числа ресурсов становится больше 0, семафор переходит в свободное состояние;

  • если этот счетчик равен 0, семафор занят;

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

  • счетчик текущего числа ресурсов не может быть больше максимального числа ресурсов

Не путайте счетчик текущего числа ресурсов со счетчиком числа пользователей объекта-семафора.

Объект ядра "семафор" создается вызовом CreateSemaphore:

HANDLE CreateSemaphore(

PSECURITY_ATTRIBUTES psa,

LONG lInitialCount,

LONG lMaximumCount,

PCTSTR pszName);

Параметр lInitialCount определяет значение счетчика текущего числа ресурсов, а параметр lMaximumCount – значение максимального числа ресурсов.

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

Поток увеличивает значение счетчика текущего числа ресурсов, вызывая функцию ReleaseSemaphore:

BOOL ReleaseSemaphore(

HANDLE hSem,

LONG lReleaseCount,

PLONG plPreviousCount);

Она просто складывает величину lReleaseCount со значением счетчика текущего числа ресурсов. Обычно в параметре lReleaseCount передают 1, но это вовсе не обязательно. Функция возвращает исходное значение счетчика ресурсов в *plPreviousCount. Если Вас не интересует это значение (а в большинстве программ так оно и есть), передайте в параметре plPreviousCount значение NULL.

События

События - самая примитивная разновидность объектов ядра [3]. Они содержат счетчик числа пользователей (как и все объекты ядра) и две булевы переменные: одна сообщает тип данного объекта-события, другая — его состояние (свободен или занят).

События просто уведомляют об окончании какой-либо операции. Объекты-события бывают двух типов: со сбросом вручную (manual-reset events) и с автосбросом (auto-reset events). Первые позволяют возобновлять выполнение сразу нескольких ждущих потоков, вторые — только одного.

Объект ядра "событие" создается функцией CreateEvent:

HANDLE CreateEvent(

PSECURITY_ATTRIBUTES psa,

BOOL bManualReset,

BOOL bInitialState,

PCTSTR pszName);

Параметр bManualReset (булева переменная) сообщает системе, хотите Вы создать событие со сбросом вручную (TRUE) или с автосбросом (FALSE). Параметру bInitialState определяет начальное состояние события — свободное (TRUE) или занятое (FALSE). После того как система создает объект событие, CreateEvent возвращает описатель события, специфичный для конкретного процесса. Потоки из других процессов могут получить доступ к этому объекту: 1) вызовом CreateEvent с тем же параметром pszName; 2) наследованием описателя; 3) применением функции DuplicateHandle; и 4) вызовом OpenEvent с передачей в параметре pszName имени, совпадающего с указанным в аналогичном параметре функции CreateEvent.

Ненужный объект ядра "событие" следует, как всегда, закрыть вызовом CloseHandle. Создав событие, Вы можете напрямую управлять его состоянием. Чтобы перевести его в свободное состояние, Вы вызываете:

BOOL SetEvent(HANDLE hEvent);

А чтобы поменять его на занятое

BOOL ResetEvent(HANDLE hEvent);

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

Для полноты картины упомяну о еще одной функции, которую можно использовать с объектами-событиями:

BOOL PulseEvent(HANDLE hEvent);

PulseEvent освобождает событие и тут же переводит его обратно в занятое состояние; ее вызов равнозначен последовательному вызову SetEvent и ResetEvent. Если Вы вызываете PulseEvent для события со сбросом вручную, любые потоки, ждущие этот объект, становятся планируемыми. При вызове этой функции применительно к событию с автосбросом пробуждается только один из ждущих потоков.

^ Ожидаемые таймеры

Ожидаемые таймеры (waitable timers) — это объекты ядра, которые самостоятельно переходят в свободное состояние в определенное время или через регулярные промежутки времени. Чтобы создать ожидаемый таймер, достаточно вызвать функцию CreateWaitableTimer.

HANDLE CreateWaitableTimer(

PSECURITY_ATTRIBUTES psa,

BOOL bManualReset,

PCTSTR pszName);

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

Объекты "ожидаемый таймер" всегда создаются в занятом состоянии. Чтобы сообщить таймеру, в какой момент он должен перейти в свободное состояние, вызовите функцию SetWaitableTimer.

BOOL SetWaitableTimer(

HANDLE hTimer,

const LARGE_INTEGER *pDueTime,

LONG lPeriod,

PTIMERAPCROUTINE pfnCompletionRoutine,

PVOID pvArgToCompletionRoutine,

BOOL fResume);

Параметр hTimer определяет нужный таймер. Следующие два параметра (pDueTime и lPeriod) используются совместно, первый из них задает, когда таймер должен сработать в первый раз, второй определяет, насколько часто это должно происходить в дальнейшем. Попробуем для примера установить таймер так, чтобы в первый раз он сработал 1 января 2002 года в 1:00 PM, а потом срабатывал каждые 6 часов [3].

// объявляем свои локальные переменные

HANDLE hTimer;

SYSTEMTIME st;

FILETIME ftLocal, ftUTC;

LARGE_INTEGER liUTC;
// создаем таймер с автосбросом

hTimer = CreateWaitableTimer(NULL, FALSE, NULL);
// таймер должен сработать в первый раз 1 января 2002 года в 1:00 PM

// по местному времени

st.wYear = 2002; // год

st.wMonth = 1; // январь

st.wDayOfWeek = 0; // игнорируется

st.wDay = 1; // первое число месяца

st.wHour = 13; // 1 PM

st.wMinute = 0; // 0 минут

st.wSecond = 0; // 0 секунд

st.wMilliseconds = 0; // 0 миллисекунд
SystemTimeToFileTime(&st, &ftLocal);
// преобразуем местное время в UTC-время

LocalFileTimeToFileTime(&ftLocal, &ftUTC);
// преобразуем FILETIME в LARGE_INTEGER из-за различий в выравнивании данных

liUTC.LowPart = ftUTC.dwLowDateTime;

liUTC.HighPart = ftUTC.dwHighDateTime;
// устанавливаем таймер

SetWaitableTimer(hTimer, &liUTC, 6 * 60 * 60 * 1000, NULL, NULL, FALSE);

...


    1. ^ Динамически подключаемые библиотеки

      1. DLL: основы.

Динамически подключаемые библиотеки (dynamic-link libraries, DLL) — краеугольный камень операционной системы Windows, начиная с самой первой её версии [2]. В DLL содержатся все функции Windows API. Три самые важные DLL: Kernel32.dll (управление памятью, процессами и потоками), User32.dll (поддержка пользовательского интерфейса, в том числе функции, связанные с созданием окон и передачей сообщений) и GDI32.dll (графика и вывод текста).

В Windows есть и другие DLL, функции которых предназначены для более специализированных задач. Например, в AdvAPI32.dll содержатся функции для защиты объектов, работы с реестром и регистрации событий, в ComDlg32.dll — стандартные диалоговые окна (вроде File Open и File Save), а ComCtl32 dll поддерживает стандартные элементы управления.

Вот лишь некоторые из причин, по которым нужно применять DLL [3]:

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

  • ^ Возможность использования разных языков программирования. У Вас есть выбор, на каком языке писать ту или иную часть приложения. Так, пользовательский интерфейс приложения Вы скорее всего будете создавать на Microsoft Visual Basic, но прикладную логику лучше всего реализовать на C++. Программа на Visual Basic может загружать DLL, написанные на C++, Коболе, Фортране и др.

  • ^ Более простое управление проектом. Если в процессе разработки программного продукта отдельные его модули создаются разными группами, то при использовании DLL таким проектом управлять гораздо проще. Однако конечная версия приложения должна включать как можно меньше файлов

  • ^ Экономия памяти. Если одну и ту же DLL использует несколько приложений, в оперативной памяти может храниться только один ее экземпляр, доступный этим приложениям. Пример — DLL-версия библиотеки С/C++. Ею пользуются многие приложения. Если всех их скомпоновать со статически подключаемой версией этой библиотеки, то код таких функций, как sprintf, strcpy, malloc и др., будет многократно дублироваться в памяти. Но если они компонуются с DLL-версией библиотеки С/C++, в памяти будет присутствовать лишь одна копия кода этих функций, что позволит гораздо эффективнее использовать оперативную память.

  • ^ Разделение ресурсов. DLL могут содержать такие ресурсы, как шаблоны диалоговых окон, строки, значки и битовые карты (растровые изображения). Эти ресурсы доступны любым программам.

  • ^ Упрощение локализации. DLL нередко применяются для локализации приложений. Например, приложение, содержащее только код без всяких компонентов пользовательского интерфейса, может загружать DLL с компонентами локализованного интерфейса.

^ DLL и адресное пространство процесса

Зачастую создать DLL проще, чем приложение, потому что она является лишь набором автономных функций, пригодных для использования любой программой, причем в DLL обычно нет кода, предназначенного для обработки циклов выборки сообщений или создания окон. DLL представляет собой набор модулей исходного кода, в каждом из которых содержится определенное число функций, вызываемых приложением (исполняемым файлом) или другими DLL. Файлы с исходным кодом компилируются и компонуются так же, как и при создании EXE-файла. Но, создавая DLL, Вы должны указывать компоновщику ключ /DLL. Тогда компоновщик записывает в конечный файл информацию, по которой загрузчик операционной системы определяет, что данный файл — DLL, а не приложение.

Чтобы приложение (или другая DLL) могло вызывать функции, содержащиеся в DLL, образ ее файла нужно сначала спроецировать на адресное пространство вызывающего процесса. Это достигается либо за счет неявного связывания при загрузке, либо за счет явного — в период выполнения.

Как только DLL спроецирована на адресное пространство вызывающего процесса, ее функции доступны всем потокам этого процесса. Фактически библиотеки при этом теряют почти всю индивидуальность: для потоков код и данные DLL — просто дополнительные код и данные, оказавшиеся в адресном пространстве процесса. Когда поток вызывает из DLL какую-то функцию, та считывает свои параметры из стека потока и размещает в этом стеке собственные локальные переменные. Кроме того, любые созданные кодом DLL объекты принадлежат вызывающему потоку или процессу — DLL ничем не владеет.

^ Неявная загрузка DLL

Собирая исполняемый модуль, который импортирует функции и переменные из DLL, Вы должны сначала создать эту DLL. А для этого нужно следующее [3].

  1. Прежде всего, Вы должны подготовить заголовочный файл с прототипами функций, структурами и идентификаторами, экспортируемыми из DLL. Этот файл включается в исходный код всех модулей Вашей DLL. Как Вы потом увидите, этот же файл понадобится и при сборке исполняемого модуля (или модулей), который использует функции и переменные из Вашей DLL.

  2. Вы пишете на С/C++ модуль (или модули) исходного кода с телами функций и определениями переменных, которые должны находиться в DLL. Так как эти модули исходного кода не нужны для сборки исполняемого модуля, они могут остаться коммерческой тайной компании-разработчика.

  3. Компилятор преобразует исходный код модулей DLL в OBJ-файлы (по одному на каждый модуль).

  4. Компоновщик собирает все OBJ-модули в единый загрузочный DLL-модуль, в который в конечном итоге помещаются двоичный код и переменные (глобальные и статические), относящиеся к данной DLL. Этот файл потребуется при компиляции исполняемого модуля.

  5. Если компоновщик обнаружит, что DLL экспортирует хотя бы одну переменную или функцию, то создаст и LIB-файл. Этот файл совсем крошечный, поскольку в нем нет ничего, кроме списка символьных имен функций и переменных, экспортируемых из DLL. Этот LIB-файл тоже понадобится при компиляции EXE-файла.

Создав DLL, можно перейти к сборке исполняемого модуля.

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

  2. Вы пишете на С/C++ модуль (или модули) исходного кода с телами функций и определениями переменных, которые должны находиться в EXE-файле. Естественно, ничто не мешает Вам ссылаться на функции и переменные, определенные в заголовочном файле DLL-модуля.

  3. Компилятор преобразует исходный код модулей EXE в OBJ-файлы (по одному на каждый модуль).

  4. Компоновщик собирает все OBJ-модули в единый загрузочный EXE-модуль, в который в конечном итоге помещаются двоичный код и переменные (глобальные и статические), относящиеся к данному EXE. В нем также создается
1   2   3   4   5   6   7   8   9   ...   34

Похожие:

Для студентов згиа специальности 080403 ”Программное обеспечение iconМетодические указания к лабораторному практикуму для студентов згиа...
Методические указания к лабораторному практикуму для студентов згиа специальности 080403 «Программное обеспечение автоматизированных...

Для студентов згиа специальности 080403 ”Программное обеспечение iconМетодические указания для студентов специальности 080403
Згиа [8, 10] и других вузов [4]. Наиболее полно описаны курсовые, дипломные и квалификационные работы, однако большинство положений...

Для студентов згиа специальности 080403 ”Программное обеспечение iconМетодические указания к курсовой работе по дисциплине «Системное...
Методические указания к курсовой работе по дисциплине «Системное программирование и операционные системы» для студентов специальности...

Для студентов згиа специальности 080403 ”Программное обеспечение iconПравила использования программного обеспечения в рамках программы...
Используя программное обеспечение вы тем самым подтверждаете свое согласие придерживаться этих правил. Если вы не согласны, не используйте...

Для студентов згиа специальности 080403 ”Программное обеспечение iconМетодические указания по выполнению лабораторных работ по курсу “
Информационные управляющие системы и технологии, 080403 – Программное обеспечение автоматизированных систем

Для студентов згиа специальности 080403 ”Программное обеспечение iconКлассификация программного обеспечения
В отличие от аппаратного обеспечения, программы, которые выполняются на нем, неосязаемы и классифицируются как программное обеспечение....

Для студентов згиа специальности 080403 ”Программное обеспечение iconКурсовая работа выполняется на основании 'Задания на курсовую работу'...
Целью курсовой работы является закрепление практических навыков самостоятельной постановки и решения задачи обработки данных с помощью...

Для студентов згиа специальности 080403 ”Программное обеспечение icon1. Классификация программного обеспечения
Назначением ЭВМ является выполнение программ. Программа содержит команды, определяющие порядок действии компьютера. Совокупность...

Для студентов згиа специальности 080403 ”Программное обеспечение iconОпорный конспект лекций по дисциплине Компьютерная графика для специальности...
Тема. Основные понятия компьютерной графики. Аппаратное и программное обеспечение

Для студентов згиа специальности 080403 ”Программное обеспечение iconМетодические указания и задание к выполнению курсового проекта по...
Методические указания и задание к выполнению курсового проекта по дисциплине «Алгоритмическое и программное обеспечение электротехнических...

Вы можете разместить ссылку на наш сайт:
Школьные материалы


При копировании материала укажите ссылку © 2013
контакты
uchebilka.ru
Главная страница


<