Операционная система windows использует вытесняющую многозадачность

2.2.1.
Основные
принципы
многозадачности
в Windows

В
Windows каждый процесс имеет свое собственное виртуальное адресное про-

странство (4Gb).
Процесс состоит из кода, данных и других
системных ресурсов, таких

как открытые файлы, каналы
(pipes), синхронизирующие объекты. Однако процесс

статический объект,
который сам по себе действия не производит.
Поток (thread) — базо-

вый объект,
которому операционная система
распределяет время центрального процес-

сора. Поток выполняет
команды программы с учетом заданного
ему маршрута. Каждый

57

процесс представляет собой
один начальный поток, который иногда
называют первич-

ным
потоком.

Первичный поток способен создать вторичные потоки. Все потоки, принадлежа-

щие одному процессу,
имеют совместный доступ к его ресурсам.
Все они работают под

управлением команд
одной и той же программы, обращаются к
одним и тем же глобаль-

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

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

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

запуска
«дочернего» процесса необходимо временно приостанавливать основной про-

цесс, что приводит
к замедлению выполнения программы в
целом. Дополнительные по-

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

асинхронные операции, работает
одновременно с несколькими окнами.

Организация многозадачности в
MS Windows различается в линейках 9x и NT. В

Windows
9x реализована приоритетная многозадачность.
В данном случае каждому

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

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

дующему потоку.
Это не дает возможность программам
полностью захватывать ресурсы

процессора.

Windows
NT использует вытесняющую
многозадачность.
Выполнение всех про-

цессов строго
контролируется операционной системой.
Операционная система выделяет

каждой из программ
некоторое количество процессорного
времени и периодически про-

изводит переключение
между запущенными на компьютере
программами. Обратившись

к специальному
системному вызову, вы можете как бы
приостановить (sleep) выполне-

ние программы,
однако если вы этого не сделаете, со
временем операционная система

сделает это за
вас. Подвисание одной из программ не
приведет к подвисанию всей сис-

темы.

В общем и целом,
вытесняющая многозадачность, которая
ранее рассмотрена бо-

лее подробно, выглядит
привлекательней. Однако
за все приходится платить. И в пер-

вую очередь расплачиваться приходится программистам, которые
разрабатывают при-

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

Представьте себе, что на компьютере
работает несколько программ, использующих
один

и тот же файл журнала ошибок. Если вам
необходимо сделать запись в этом файле,
то в

Windows 9x вы можете
без лишних сложностей открыть файл,
записать в него данные, а

затем закрыть его.
Если при этом вы ни разу не обратились
к специальным системным

функциям, вы можете
быть уверенными, что в то время, пока вы
работали с файлом, ни

одна другая
программа не обратилась к этому же файлу
(так как фактически в ходе ра-

боты с файлом
на компьютере работает только одна
ваша программа, а все остальные

программы находятся в состоянии
ожидания).

В среде Windows NT все
не так просто. Предположим, что один из
потоков открыл

файл и начал в него
запись, но в этот момент операционная система
передала управле-

ние другому потоку.
Что произойдет, если другой поток
попытается открыть тот же са-

мый файл? Либо
этого сделать не удастся, либо другой
поток откроет файл в режиме со-

вместного доступа
(for sharing), что может не соответствовать
вашим ожиданиям. Даже

если в ходе работы с
файлом операционная система не осуществила
передачу управле-

ния другому потоку,
получить доступ к файлу может попытаться
поток, работающий на

другом процессоре. В результате вы
столкнетесь с той же проблемой.

58

На системном уровне каждый поток представляет собой
объект, созданный сис-

темным менеджером
объектов. Аналогично с остальными
системными объектами, поток

содержит данные
(атрибуты) и методы
(функции). Схематически объект-поток может

быть представлен в следующем виде
(рис.2.5.) [12]:

Стандартный
объект
заголовка

Атрибуты
потока

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

Контекст

Динамический приоритет

Базовый приоритет

Привязанность к архитектуре

процессора

Время выполнения

Статус оповещения

Счетчик прерываний

Маркер передачи прав доступа

Порт завершения

Код завершения

Методы
потока

Создание потока

Открытие потока

Запрос информации о потоке

Установка информации о потоке

Текущий поток

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

Получение контекста

Установление контекста

Прерывание

Возобновление

Предупреждение

Проверка поступления преду-

преждения

Регистрация порта завершения

Рис.2.5.
Схема
объекта
потока.

Для большинства методов потока имеются
соответствующие API — функции Win32.

Windows защищает свои внутренние структуры от прямого вмешательства пользова-

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

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

ровать или изменять
параметры системных объектов. Все
операции с ними выполняются

посредством функций
Win32 API. Windows предоставляет дескриптор, идентифици-

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

честве аргумента одной из
API-функций. Свои дескрипторы имеют потоки, процессы,

семафоры, файлы и др. объекты.
Внутренняя структура объектов
доступна только ме-

неджеру объектов.
Функция, создающая поток, возвращает
дескриптор нового объекта.

С помощью этого дескриптора можно
выполнить следующие операции:

• повысить или понизить плановый
приоритет потока;

• приостановить поток и возобновить
его выполнение;

• прекратить выполнение потока;

• определить код завершения потока.

В ОС
Windows потоки, процессы, семафоры и исключающие семафоры могут

иметь несколько разных дескрипторов. Завершив работу с объектом, необходимо вы-

звать функцию
CloseHandle, которая закрыв последний дескриптор, сама уничтожит

объект. В целом в
Windows каждый процесс не может одновременно
поддерживать бо-

лее 65536 открытых дескрипторов.

Работа
с потоками не сводится только к их
запуску и остановке. Необходимо обес-

печить совместное
функционирование потоков. Для организации
эффективного взаимо-

действия между несколькими потоками необходимо производить контроль за их вре-

менными параметрами. Контроль
осуществляется [12]:

59

• установлением приоритетов;

• синхронизацией.

Приоритет потока определяет, насколько часто
данный поток получает доступ к

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

общим ресурсам.
Когда системная программа-планировщик
останавливает один поток и

ищет другой, который
должен быть запущен следующим, она
отдает предпочтение по-

токам, имеющим наиболее высокий приоритет. Обработчики системных прерываний

всегда имеют более
высокий приоритет по сравнению с
пользовательскими процессами.

Каждому процессу
присущ собственный приоритет. Базовый
плановый приоритет пото-

ка определяется
на основе приоритета процесса, который
является владельцем этого по-

тока. Всего различают
32 уровня приоритета от 0 до 31. При этом
приоритеты уровня от

0 до 15 называются
переменными приоритетами, а от 16 до 31 –
фиксированными при-

оритетами. Схема наследования приоритетов
потока показана на рис.2.6 [12].

31

30

29

28

27

26

25

24

23

22

21

20

19

18

17

16

15

14

13

12

11

10

9

8

7

6

5

4

3

2

1

0

Уровни

приоритета

Класс
реального

времени

Наивысший

класс

Класс
переднего

плана

Фоновый
класс

Класс
простоя

Базовый

приоритет

процесса

Диапазон
базового

приоритета
потока,

владельцем
которо-

го
является
процесс

с
приоритетом
пе-

реднего
плана

Диапазон
дина-

мического
при-

оритета
для
то-

го
же
самого

потока

Рис.2.6
Схема
наследования
приоритета
потоков
от
исходного
приоритета
процесса

60

Среди атрибутов объекта потока различают
базовый и динамический приоритеты.

При вызове команды
для изменения приоритета потока меняется
его базовый приоритет,

который не может
быть выше или ниже приоритета
процесса-владельца более чем на 2

уровня.
Операционная система способствует
«продвижению» потока для его выполне-

ния. Для этого система поддерживает динамический приоритет потоков, которые вы-

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

считывает
данные с диска временно повышается
приоритет всех потоков такого процес-

са. Эти временные
приращения в сумме с базовым приоритетом
образуют динамический

приоритет процесса.
Планировщик определяет очередность
выполнения потоков на ос-

новании их динамического приоритета. Со следующим тактом процесса приращение

приоритета начинает
уменьшаться на один уровень, постепенно
достигая уровня базово-

го приоритета.

Выбирая поток,
который будет выполняться следующим,
программа-планировщик

начинает просмотр
очереди заданий с потоков, имеющий
наивысший приоритет, выпол-

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

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

приостановлены
или заблокированы. В каждый момент
времени поток может находить-

ся в одном из шести состояний.

Ready(готов) — поставлен в очередь и
ожидает выполнения.

Standby(ближайший) — готов быть выполненным
следующим.

Running
(выполнение) — находится в режиме
выполнения и взаимодействует с централь-

ным процессором.

Waiting(ожидание) не выполняется, ожидая
сигнала выполнения.

Transition(промежуточное) будет выполняться после того, как система загрузит его

контекст.

Terminated(завершен) — выполнение
завершено, однако объект не удален.

Когда
программа-планировщик выбирает из
очереди готовый к выполнению поток,

она
загружает его контекст.
В состав контекста входит набор значений
регистров про-

цессора, стек ядра,
блок параметров окружения потока и
пользовательский стек в адрес-

ном пространстве процесса, который является владельцем данного потока. Если часть

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

пока система соберет все составные
части контекста.

Чтобы потоки могли надежно работать,
их необходимо синхронизировать. Пред-

ставьте себе, что
один поток создает кисть, а затем создает
несколько потоков, которые

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

должен уничтожать
кисть до тех пор, пока другие потоки не
завершат операции рисова-

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

и записывает их в
файл, а другие потоки считывают данные
из этого файла и образовы-

вают веденный
текст. Считывание не должно происходить
в тот момент, когда идет за-

пись. В обоих случаях
надо принять меры по
координации последовательности опера-

ций в нескольких потоков.

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

Boolean, которую один из потоков будет
использовать с целью информирования
других

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

присвоить
переменной bDone
значение TRUE,
а потоки, считывающие данные из файла,

будут циклически
просматривать эту переменную до тех
пор, пока ее значение не изме-

нится. Такая схема вполне работоспособна, однако циклический просмотр флага не-

сколькими потоками
занимает много времени процессора. Вот
почему в Win32 поддер-

живается набор синхронизирующих объектов
[9, 12]. Перечислим их.

Объект типа
исключающий семафор функционирует
подобно узкой двери, «про-

пуская» (т.е. давая допуск) одновременно
по одному потоку.

61

Объект типа семафор
функционирует подобно многостворчатой
двери, ограничи-

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

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

ком, для которого он адресован.

Объект
типа критический
раздел
аналогичен исключающему семафору, но
рабо-

тает только в пределах одного процесса.

Все указные объекты являются системными и создаются менеджером объектов.

Хотя каждый
синхронизирующий объект координирует
определенный вид взаимодейст-

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

ределенную
синхронизируемую операцию, ожидает
ответ от одного из этих объектов и

осуществляет свои функции только после получения на то разрешения. Программа-

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

нимали времени
центрального процессора. После получения
соответствующего сигнала

планировщик
возобновляет работу потока. Место и
способ получения сигнала зависит

от конкретного объекта.

Исключающие семафоры, обычные семафоры и события позволяют координиро-

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

разделы воспринимаются лишь потоками одного процесса. Если
один процесс создает

другой процесс,
дочерний процесс часто наследует
дескрипторы имеющихся синхрони-

зирующих объектов. Объекты критических
разделов не могу быть унаследованы.

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

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

екты могут находиться
в двух состояниях: при наличии сигнала
и при отсутствии тако-

вого. Потоки
взаимодействуют с синхронизирующими объектами
путем установки сиг-

нала или путем его
ожидания. Поток, находящейся в состоянии
ожидания, блокируется

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

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

включает сигнал и освобождает объект.

Потоки могут
ожидать разрешения не только от
исключающих и обычных семафо-

ров, событий и
критических разделов, но и от других
объектов. Иногда возникает ситуа-

ция,
когда необходимо ожидать разрешения от процесса,
другого потока, таймера или

файлового объекта.
Все эти объекты имеют свое предназначение,
но подобно синхрони-

зирующим объектом
они способны давать сигналы разрешения.
Процессы и потоки сиг-

нализируют о своем завершении, объекты-таймеры
— об истечении определенного ин-

тервала времени, а файловые объекты
— о завершении операций чтения или записей

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

2.2.2. API-функции
для
реализации
механизма
многозадачности

В Win32 API определены
следующие функции работы с процессами
и потоками [4]

(таблица 2.1.)

Таблица
2.1

Название функции

1

AttachThreadInput

CommandLineToArgvW

CreateProcess

CreateRemoteThread

CreateThread

Выполняемое действие

2

Переключение механизмов ввода с одной
нити на дру-

гую

Производит разбор командной строки в
Unicode

Создает процесс

Создает поток в адресном пространстве
другого про-

цесса

Создает поток

62

1

2

Продолжение
табл. 2.1

ExitProcess

ExitThread

FreeEnvironmentStrings

GetCommandLine

GetCurrentProcess

GetCurrentProcessId

GetCurrentThread

GetCurrentThreadId

GetEnvironmentStrings

GetEnvironmentVariable

GetExitCodeProcess

GetExitCodeThread

GetPriorityClass

GetProcessAffinityMask

GetProcessShutdownParame-

ters

GetProcessTimes

GetCurrentProcess

GetProcessWorkingSetSize

GetStartupInfo

GetThreadPriority

GetThreadTimes

OpenProcess

ResumeThread

SetEnvironmentVariable

SetPriorityClass

Завершает процесс и все его потоки

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

Освобождает память области переменных
среды

Возвращает указатель на командную
строку

Возвращает описатель (handle) текущего
процесса

Возвращает идентификатор текущего
процесса

Возвращает описатель
(handle) текущего потока

Возвращает идентификатор текущего
потока

Возвращает строку переменных среды

Возвращает значение указанной переменной
среды

Возвращает код завершения процесса

Возвращает код завершения потока

Возвращает класс приоритета процесса

Сообщает, на каких процессорах разрешено
исполне-

ние процесса

Сообщает параметры поведения процесса
при завер-

шении работы системы

Возвращает временные характеристики
процесса

Сообщает версию Windows, для которой
предназна-

чен процесс

Возвращает характеристики доступного
процессу ад-

ресного пространства

Возвращает параметры процесса, полученные
им при

создании

Сообщает приоритет указанного потока

Возвращает временные характеристики
указанного

потока

Возвращает описатель (handle) указанного
процесса

Уменьшает счетчик задержаний потока
(или запуска-

ет его)

Устанавливает значение указанной
переменной среды

Устанавливает класс приоритета процесса

SetProcessShutdownParameters Устанавливает параметры
поведения процесса при

завершении работы системы

SetThreadAffinityMask

SetThreadPriority

Sleep

SleepEx

SetProcessWorkingSetSize

SuspendThread

TerminateProcess

TerminateThread

TlsAlloc

Устанавливает, на каких процессорах
разрешено ис-

полнение потока

Устанавливает приоритет указанного
потока

Задерживает исполнение потока на
указанное коли-

чество миллисекунд

Задерживает исполнение до наступления
события

ввода/вывода или на время

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

адресного пространства

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

Завершает указанный процесс

Завершает указанный поток

Распределяет индекс локальной памяти
потока

(thread local storage TLS)

63

TlsFree

1

Освобождает индекс TLS

2

Окончание
табл. 2.1

TlsGetValue

TlsSetValue

Возвращает данные, размещенные в TLS с
указанным индексом

Помещает данные в TLS с указанным индексом

WaitForInputIdle Ждет, пока не начнется ввод
для указанного процесса

WinExec

Выполняет указанное приложение

Подробное описание
функций приведено в Win32 Programmer’s
Reference. Далее

рассмотрим только некоторые основные
функции [4].

Функция
CreateProcess() — создает новый процесс и его первичный поток. Новый

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

BOOL CreateProcess(LPCTSTR lpApplicationName,
// имя исполняемого файла

LPTSTR lpCommandLine, // командная строка

LPSECURITY_ATTRIBUTES lpProcessAttributes, // атрибуты
защиты процесса

LPSECURITY_ATTRIBUTES lpThreadAttributes, // атрибуты защиты
потока

BOOL bInheritHandles, // флаг наследования
описателей

DWORD dwCreationFlags, // флаги создания

LPVOID lpEnvironment, // указатель блока переменных
среды

LPCTSTR lpCurrentDirectory, // текущий каталог

LPSTARTUPINFO lpStartupInfo, // блок начальных
параметров

LPPROCESS_INFORMATION lpProcessInformation //
указатель

// структуры, описывающей порожденный
процесс

);

Функция возвращает TRUE в случае успеха
и FALSE — в случае неудачи.

Параметры:

lpApplicationName
— указатель на строку, содержащую имя исполняемой программы.

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

щем каталоге. Параметру может быть
присвоено значение NULL. В этом случае в

качестве имени
файла выступает первая выделенная пробелами
лексема из строки

lpCommandLine;

lpCommandLine — указатель
командной строки. Если параметр
lpApplicationName имеет

значение NULL, то
имя исполняемого файла выделяется из
lpCommandLine, а по-

иск исполняемого
файла производится в соответствии с
правилами, действующими

в системе;

lpProcessAttributes —
указатель на структуру, описывающую
параметры защиты процесса.

Если параметру присвоено значение
NULL, то устанавливаются атрибуты “по

умолчанию”;

lpThreadAttributes- указатель
на структуру, описывающую параметры
защиты первично-

го
потока. Если параметру присвоено
значение NULL, то устанавливаются атрибу-

ты “по умолчанию”;

bInheritHandles
— определяет, будет ли порожденный процесс наследовать описатели

(handles) объектов
родительского процесса. Например, если
родительский процесс

AB, то он получил
описатель процесса B и может им
манипулировать. Если теперь

он порождает
процесс C с параметром bInheritHandles равным
TRUE, то и процесс

C сможет работать с описателем процесса
B;

dwCreationFlags
— определяет некоторые дополнительные условия
создания процесса и

его класс приоритета;

lpEnvironment- указатель на блок переменных
среды порожденного процесса. Если этот

параметр равен
NULL, то порожденный процесс наследует
среду родителя. Иначе

64

он должен указывать на завершающийся
нулем блок строк, каждая из которых за-

вершается нулем (аналогично
DOS);

lpCurrentDirectory — указатель
на строку, содержащую полное имя текущего
каталога по-

рожденного процесса.
Если этот параметр равен NULL, то
порожденный процесс

наследует каталог родителя;

lpStartupInfo
— указатель на структуру
STARTUPINFO, которая определяет параметры

главного окна порожденного процесса;

lpProcessInformation —
указатель на структуру, которая
будет заполнена информацией о

порожденном процессе после возврата
из функции.

Пример: программа, запускающая
Microsoft Word

#include <windows.h>

#include <conio.h>

#include <stdio.h>

main()

{

PROCESS_INFORMATION pi ;

STARTUPINFO si ;

ZeroMemory( &si, sizeof(si)) ;

si.cb = sizeof( si ) ;

printf( «Press any key to start WinWord — » );

getch() ;

CreateProcess( NULL, «WinWord», NULL, NULL, FALSE, 0,

NULL, NULL, &si, &pi ) ;

return 0 ;

}

Функция CreateThread()
— создает новый поток в адресном пространстве
процесса.

Формат функции [8]:

HANDLE CreateThread(

LPSECURITY_ATTRIBUTES lpThreadAttributes, // атрибуты защиты
потока

DWORD dwStackSize,

// размер стека в байтах

LPTHREAD_START_ROUTINE lpStartAddress,
//указатель на функцию потока

LPVOID lpParameter,

BDWORD dwCreationFlags,

LPDWORD lpThreadId );

// аргумент, передаваемый в функцию
потока

// флаги управления созданием потока

// область памяти для возвращения

//идентификатора потока

Функция возвращает описатель порожденного
потока. Параметры:

lpThreadAttributes — указатель на структуру,
описывающую параметры защиты потока.

Если параметру присвоено значение
NULL, то устанавливаются атрибуты “по

умолчанию”;

dwStackSize — устанавливает
размер стека, который отводится потоку.
Если параметр ра-

вен нулю, то устанавливается стек,
равный стеку первичного потока;

lpStartAddress — адрес функции, которую
будет исполнять поток. Функция имеет
один

32-битный аргумент и возвращает 32 битное
значение;

lpParameter — параметр, передаваемый в функцию,
которую будет исполнять поток;

dwCreationFlags
— дополнительный флаг, который управляет созданием потока. Если

этот параметр
равен CREATE_SUSPENDED, то поток после порождения
не запус-

кается на исполнение до вызова функции
ResumeThread;

65

lpThreadId — указатель на
32-битную переменную, которой будет присвоено значение

уникального идентификатора потока.

Пример: программа, порождающая поток

#include <stdio.h>

#include <conio.h>

#include <windows.h>

DWORD WINAPI Output( LPVOID Param )

{

while( TRUE )

{

printf( «A» ) ;

Sleep(100) ;

}

return( 0 ) ;

}

main()

{

HANDLE hThread ;

DWORD ThreadId ;

hThread = CreateThread( NULL, 0, Output, NULL, 0, &ThreadId ) ;

getch() ;

TerminateThread( hThread, 0 ) ;

return(0) ;

}

Потоки, обладающие высоким приоритетом, занимают большую часть времени

центрального
процессора, раньше завершают свою работу
и способны быстрее реагиро-

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

приоритет, ничего хорошего не выйдет.
Дело в том, что если нескольким потокам
будет

присвоен один и
тот же приоритет (не имеет значения,
высокий или низкий), программа

— планировщик выделит им одинаковое
время работы центрального процессора,
и сама

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

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

равной степени
и применимо и к процессом. Старайтесь
ограничивать приоритет всех

потоков и процессов
низким или средним уровнем и присваивайте
им высоких приори-

тет только по мере необходимости [8, 12].

Приведенные ниже функции проверяют или
изменяют базовый приоритет потока.

BOOL SetThreadPriority(

HANDLE hThread
// дескриптор потока

int iPriority );
// новый уровень приоритета

int GetThreadPriority ( HANDLE hThread );

Функция SetThreadPriority
возвращает значение TRUE
в случае успешного завер-

шения потока, а значение FALSE
-при возникновении ошибки. Функция GetThread-

Priority возвращает значение, определяющее приоритет. Для обозначения возможных

значений приоритета в обеих функциях
используется набор констант.

66

THREAD_PRIORITY_LOWEST

са

На два уровня ниже приоритета процес-

THREAD_PRIORITY_BELOW_NORMALНа один уровень ниже
приоритета про-

цесса

THREAD_PRIORITY_NORMAL

цесса

THREAD_PRIORITY_ABOVE_NORMAL

цесса

THREAD_PRIORITY_HIGHEST

са

THREAD_PRIORITY_TIME_CRITICAL

ских процессов)

THREAD_PRIORITY_IDLE

ских процессов)

Тот же уровень приоритета, что и у про-

На один уровень выше приоритета про-

На два уровня выше приоритета процес-

Уровень 15 (для обычных пользователь-

Уровень 1 (для обычных пользователь-

Прерванный поток приостанавливает
свое выполнение и не учитывается при
рас-

пределении времени
центрального процессора. Поток остается
в таком состоянии до тех

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

вести, в частности,
в том случае, если пользователь прерывает выполнение
определен-

ной задачи. До тех
пор, пока задание не будет отменено,
поток можно перевести в со-

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

выполнение с той
точки, где он был остановлен. Для
приостановки и возобновления вы-

полнения потоков служат функции

DWORD SuspendThread ( HANDLE hThread ) ;

DWORD ResumeThread( HANDLE hThread );

Один и тот же
поток можно последовательно остановить
несколько раз, не возоб-

новляя его выполнения, однако каждой последовательной команде SuspendThread

должна соответствовать ответная команда
ResumeThread.
Система отчитывает количе-

ство отмененных команд с помощью счетчика прерываний.
Каждая команда Suspend-

Thread
инкрементирует значения счетчика, а
каждая команда ResumeThread
декремен-

тирует его. Обе
функции возвращают предыдущее значение
счетчика в виде параметра

типа DWORD.
Поток возобновит свое выполнение только
в том случае, если счетчик

примет значение 0.

Поток способен остановить
себя, но он не в состоянии самостоятельно
возобновить

свое выполнение.
Однако он может на нужное время перенести
себя в режим ожидание.

Команда
Sleep задерживает выполнения потока, удаляя его из очереди программы-

планировщика до
тех пор, пока не пройдет заданный
интервал времени. Интерактивные

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

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

режима ожидания
предпочтительнее задействования
«пустого» цикла, поскольку в этом

случае не используется время центрального
процессора.

Для осуществления паузы в течение заданного времени поток вызывает следую-

щие функции:

VOID Sleep ( DWORD dwMilliseconds ) ;

DWORD SleepEx(DWORD dwMilliseconds,

BOOL bAlertable );

// продолжительность паузы

// TRUE — возобновить работу

// при завершении операции ввода/вывода

67

Расширенная функция SleepEx обычно работает
совместно с функциями фонового

ввода/вывода и
может использоваться для инициации
команд чтения или записи, не тре-

буя их завершения.
Эти операции выполняются в фоновом
режиме. По завершении опе-

рации система извещает об этом пользователя, обращаясь к предусмотренной в про-

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

ввод/вывод
далее будет рассмотрен более подробно)
чаще всего применяется в интерак-

тивных программах,
которые должны реагировать на команды
пользователя, не преры-

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

магнитной ленте или с сетевым диском.

Параметр bAlertable
функции SleepEx имеет тип Boolean. Если этот
параметр равен

TRUE, система может преждевременно возобновить выполнение потока при условии,

что перекрывающаяся
операция ввода/вывода завершилась до
истечения заданного вре-

мени
ожидания. В случае досрочного прерывания
функция sleepEx возвращает значение

WAIT_IO_COMPLETION, по истечении указанного
времени — значение 0.

По специальному запросу поток возвращает свои дескриптор и идентификатор.

Указанные ниже функции позволяют
получить информацию о текущем потоке:

DWORD GetCurrentThreadId( VOID ) ;

HANDLE GetCurrentThread( VOID );

Результирующее
значение функции GetCurrentThreadId совпадает
со значением па-

раметра lpIDThread
после выполнения функции CreateThread и
однозначно идентифици-

рует поток в
системе. Хотя идентификатор потока
нужен лишь для незначительного ко-

личества функций
Win32 API, он может использоваться с целью мониторинга

системных потоков
без необходимости поддерживать открытый
дескриптор для каждого

потока. Открытый дескриптор защищает
поток от уничтожения.

Дескриптор, полученный в результате выполнения функции
GetCurrentThread,

служит для тех же целей, что и дескриптор, возвращенный функцией
CreateThread. И

несмотря на то,
что он может использоваться аналогично другим
дескрипторам, на са-

мом деле этот параметр является псевдодескриптором. Псевдодескриптор
— это специ-

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

тому,
как одиночная точка (.) в DOS всегда
указывает на текущий каталог, а параметр

this в C++ определяет
текущий объект. Константа — псевдодескриптор,
полученная в ре-

зультате выполнения
функции GetCurrentThread, указывает на текущий
поток. В отличие

от настоящих
дескрипторов, псевдодескриптор не может
передаваться другим потокам.

Чтобы получить настоящий переносимый дескриптор потока, необходимо выполнить

следующие действия:

HANDLE hThread;

hThread = DuplicateHandle(

GetCurrentProcess(),

GetCurrentThread(),

GetCurrentProcess(),

&hThread,

0,

FALSE,

DUPLICATE_SAME_ACCESS );

68

// процесс-источник

// исходный дескриптор

// целевой процесс

// новый дублирующийся дескриптор

//привилегии доступа (подавляемые

// последним параметром)

// дочерние объекты не унаследуют

// дескриптор

// привилегии доступа копируются у

// исходного дескриптора

Хотя функция CloseHandle не влияет на
псевдодескрипторы, дескриптор, созданный с

помощью функции
DuplicateHandle, является настоящим и в конце
концов должен быть

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

GetCurrentThread, поскольку в этом случае подразумевается, что поток имеет полный

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

По аналогии с тем, как
Windows-программа завершается по достижении конца

функции WinMain, поток
обычно прекращает свое существование
при достижении кон-

ца функции, в
которой был начат. Когда он достигает
конца стартовой функции, система

автоматически вызывает команду
ExitThread, имеющую такой синтаксис:

VOID ExitThread( DWORD dwExitCode );

Хотя операционная
система вызывает функцию ExitThread
автоматически, при не-

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

разом:

DWORD ThreadFunction( LPDWORD lpdwParam )

{

HANDLE hThread = CreateThread( <параметры>);

// далее следуют стандартные операции
инициализации;

// проверка наличия ошибочных условий

if ( <условие возникновения ошибки> )

{

ExitThread( ERROR_CODE ); // прекратить работу потока

}

// ошибки нет, работа продолжается

return( SUCCESS_CODE
);
// эта строка
программы

// заставляет систему вызвать функцию
ExitThread

}

Параметры ERROR_CODE
и SUCCESS_CODE определяются по вашему усмотре-

нию. В нашем простом примере поток
нетрудно прервать с помощью команды
return:

if( <условие возникновения ошибки> )

{

return( ERROR_CODE ); // прекратить работу
потока

}

В данном случае,
команда return приводит к тому же результату,
что и функция Ex-

itThread, так как при ее выполнении осуществляется неявный вызов последней. Эта

функция особенно полезна при необходимости
прервать поток из любой под програм-

мы, вызываемой внутри функции типа
ThreadFunction.

Когда поток
завершается с помощью оператора return,
32-разрядный код заверше-

ния
автоматически передается функции
ExitThread. После прекращения работы потока

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

// один поток вызывает эту функцию для
получения кода завершения другого
потока

BOOL GetExitCodeThread( HANDLE hThread, LPDWORD lpdwExitCode );

Функция
GetExitCodeThread возвращает значение FALSE в том
случае, если опре-

делить код завершения помешала ошибка.

69

Независимо от того, как — явно или неявно
(в результате выполнения оператора re-

turn) — вызывается функция
ExitThread, она удаляет поток из очереди программы-

планировщика и
уничтожает его стек. Однако сам объект
при этом сохраняется. Поэто-

му даже после
прекращения выполнения потока вы можете
запросить его код заверше-

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

CloseHandle), с тем чтобы поток не
занимал лишний объем памяти. При
закрытии по-

следнего дескриптора система автоматически уничтожает поток. Система не может

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

случае поток
будет уничтожен сразу же после завершения
выполнения. Если по завер-

шении
процесса остаются незакрытые дескрипторы,
система закрывает их автоматиче-

ски и удаляет все
«подвешенные» объекты, которые не
принадлежат ни одному процес-

су.

С помощью команды
ExitThread поток может остановить
себя самостоятельно в

том месте программы,
где это необходимо. Кроме того, один
поток способен по своему

усмотрению мгновенно остановить другой
поток.

// с помощью вызова этой функции один
поток может остановить другой

BOOL TerminateThread ( HANDLE hThread, DWORD
dwExitCode );

Поток не в состоянии
защитить себя от прерывания. Имея
соответствующий деск-

риптор, любой
объект может мгновенно остановить поток
вне зависимости от его теку-

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

току). Если при вызове функции
CreateThread использовать набор атрибутов

безопасности,
заданный по умолчанию, то результирующий
дескриптор обеспечит пол-

ные привилегии доступа к созданному
потоку.

Функция TerminateThread не уничтожает
стек потока, а только возвращает код
его

завершения. Функции
ExitThread и TerminateThread переводят объект в сигнальное со-

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

завершения. После выполнения любой из двух указанных функций поток продолжает

существовать
в сигнальном состоянии до тех пор, пока
не будут закрыты все его деск-

рипторы.

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

При работе с
потоками необходимо иметь возможность
координировать их дейст-

вия. Часто координация действий подразумевает определенный порядок выполнения

операций. Кроме
функций, предназначенных для создания
потоков и изменения их пла-

нового приоритета, Win32 API содержит
функции, которые переводят потоки в
режим

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

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

частности семафоров и исключающих
семафоров.

Лучше всего проиллюстрировать применение
синхронизирующих объектов можно

на примере функций,
ожидающих сигнала от объекта. С помощью
одного набора обоб-

щенных команд можно организовать ожидание сигналов от процессов,
семафоров, ис-

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

ожидает поступления сигнала от указанного
объекта:

DWORD WaitForSingleObject( HANDLE hObject,

DWORD dwMilliseconds );

70

// объект, сигнал от

// которого ожидается

// максимальное время ожидания

Функция WaitForSingleObject позволяет приостановить
выполнение потока до тех

пор, пока не поступит
сигнал от заданного объекта. Кроме того,
в этой команде указыва-

ется максимальное
время ожидания. Чтобы обеспечить
бесконечное ожидание, в качест-

ве временного интервала следует задать
значение INFINITE. Если объект уже доступен

или если он
подает сигнал в течение заданного времени,
функция WaitForSingleObject

возвращает значение
0 и выполнение потока возобновляется.
Но если заданный интер-

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

WAIT_TIMEOUT.

Для того чтобы заставить поток ожидать сигналы
сразу от нескольких объектов,

воспользуйтесь функцией
WaitForMultipleObjects. Функция возвратит управление
пото-

ку при поступлении
сигнала либо от одного из указанных
объектов, либо от всех объек-

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

DWORD WaitForMultipleObjects(

DWORD dwNumObjects,

LPHANDLE lpHandles,

BOOL bWaitAll,

DWORD dwMilliseconds );

// количество ожидаемых объектов

// массив дескрипторов

// TRUE — ожидание сигналов

// сразу от всех объектов;

// FALSE — ожидание сигнала от

// любого из объектов

// максимальный период ожидания

Результирующее значение WAIT_TIMEOUT,
опять-таки, говорит о том, что задан-

ный интервал
времени прошел, а сигнал от объектов
не поступил. Если флаг bWaitAll

имеет значение FALSE, соответствующее
ожиданию сигнала от любого из указанных

объектов,
в случае успешного завершения функция
WaitForMultipleObjects возвращает

код, который указывает, от какого из элементов массива
lpHandles поступил сигнал.

(Первый элемент
массива соответствует значению 0, второй
— значению 1 и т.д.). Если

флаг bWaitАll имеет значение TRUE, функция
не возвращает результат до тех пор,
пока

не
будут установлены флаги всех объектов
(т.е. пока не завершите выполнение всех
по-

токов).

Две расширенные
версии функций ожидания содержат
дополнительный флаг ста-

туса оповещения, который позволяет возобновить выполнение потока, если в течение

периода ожидания были завершены асинхронные
операции чтения или записи. Работу

этих функций
можно представить так, как будто они
просят «разбудить» их в одном из

трех случаев: если
становится доступным указанный объект,
если заканчивается задан-

ный период времени или если завершилось выполнение фоновой операции вво-

да/вывода.

DWORD WaitForSingleObjectEx (

HANDLE hObject,

DWORD dwMilliseconds,

BOOL bAlertable );

DWORD WaitForMultipleObjectsEx(

DWORD dwNumObjects,

LPHANDLE lpHandles,

BOOL bWaitAll,

// объект, сигнал от которого ожидается

// максимальное время ожидания

// TRUE — прекращение ожидания

// при завершении операции ввода/вывода

// количество ожидаемых объектов

// массив дескрипторов

// TRUE — ожидание сигналов

// сразу от всех объектов;

// FALSE — ожидание сигнала от

71

DWORD dwMilliseconds,

BOOL bAlertable );

// любого из объектов

// максимальный период ожидания

// TRUE — прекращение ожидания

// при завершении операции ввода/вывода

При успешном выполнении функции ожидания
объект, сигнал от которого ожи-

дался, обычно
определенным образом изменяется.
Например, если поток ожидал и по-

лучил сигнал от исключающего
семафора, функция восстанавливает несигнальное со-

стояние исключающего
семафора, чтобы остальные потоки знали о том, что он
занят.

Кроме того, функции
ожидания декрементируют значение
счетчика семафора и сбрасы-

вают информацию о некоторых событиях.

Функции ожидания
не изменяют состояния указанного
объекта до тех пор, пока не

поступит сигнал
от одного или нескольких других объектов.
В частности, поток не за-

хватывает
исключающий семафор сразу же после
поступления сигнала от него, а ожида-

ет сигналов от других объектов. Кроме
того, в течение времени ожидания
исключающий

семафор снова
может быть захвачен другим потоком,
который еще больше продлит со-

стояние ожидания.

Конечно, ожидать
поступления сигнала от объекта можно
лишь в том случае, если

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

скольку для работы
с ними существуют параллельные
API-команды, позволяющие соз-

давать и уничтожать
эти объекты, захватывать их и освобождать, а
также получать их

дескрипторы.

Функциям,
создающим исключающие
семафоры
и семафоры,
нужно указать тре-

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

указать его имя, но это необязательно)
[12].

HANDLE CreateMutex (

LPSECURITY_ATTRIBUTES lpsa,

сти

// необязательные атрибуты безопасно-

BOOL bInitialOwner

LPTSTR lpszMutexName )

HANDLE CreateSemaphore(

// TRUE — создатель хочет

// завладеть полученным объектом

// имя объекта

LPSECURITY_ATTRIBUTES lpsa,

//необязательные атрибуты безопасности

LONG lInitialCount,

LONG lMaxCount,

LPTSTR lpszSemName );

// исходное значение счетчика (обычно
0)

// максимальное значение

// счетчика (ограничивает число потоков)

// имя семафора (может иметь значение
NULL)

Если в качестве атрибута безопасности задано
значение NULL, результирующий

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

цессами. Имена
объектов являются необязательными,
однако они становятся полезными

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

Если флагу
bInitialOwner присвоить значение TRUE,
поток сразу после создания

объекта завладеет
им. Созданный исключающий семафор не
станет подавать сигналы до

тех пор, пока поток не освободит его.

В отличие от исключающего
семафора, который может принадлежать
только од-

ному потоку,
неисключающий семафор остается в
сигнальном состоянии до тех пор, по-

ка его счетчик
захватов не получит значения iMaxCount.
Если другие потоки в этот мо-

72

мент попытаются завладеть семафором, они будут приостановлены до тех пор,
пока

счетчик захватов не будет декрементирован
до значения ниже максимального.

Пока семафор (или исключающий семафор)
существует, поток взаимодействует с

ним посредством
операций захвата и освобождения. Для
захвата любого объекта поток

вызывает функцию
WaitForSingleObject (или одну из ее разновидностей).
Завершив вы-

полнение задачи, которая синхронизировалась захваченным объектом,
поток освобож-

дает этот объект с помощью одной из
следующих функций:

BOOL ReleaseMutex( HANDLE hMutex ) ;

BOOL ReleaseSemaphore(

HANDLE hSemaphore,

LONG lRelease,

LPLONG lplPrevious );

// величина, на которую

// инкрементируется значение счетчика

// при освобождении объекта (обычно 1)

// переменная, которой присваивается

// предыдущее значение счетчика

При освобождении семафора: или
исключающего семафора значение счетчика
за-

хватов инкрементируется.
Значение счетчика, превышающее
0, воспринимается систе-

мой как сигнал объекта ожидающим его
потокам.

Освободить
исключающий семафор может только тот
поток, который завладел им.

Однако любой поток
может вызвать функцию ReleaseSemaphore, которая
инкрементиру-

ет значение
счетчика захватов обычного семафора
вплоть до его максимального значе-

ния.
Изменение значения счетчика дает
возможность в процессе выполнения
программы

произвольным
образом задать количество потоков,
которые могут завладеть семафором.

Обратите внимание,
что функция CreateSemaphore позволяет при
создании нового сема-

фора
присвоить его счетчику значение, меньшее
максимального. Например, при разра-

ботке
нового семафора его счетчику можно задать начальное
значение 0. Такой прием

позволит
заблокировать все потоки до тех пор,
пока программа не произведет инициали-

зацию, а затем не увеличит значение
счетчика с помощью команды ReleaseSemaphore.

Не забывайте
вовремя освобождать синхронизирующие объекты.
Не задав макси-

мального времени ожидания и забыв освободить исключающий семафор,
вы заблоки-

руете все ожидающие его потоки.

Поток может ожидать несколько сигналов
от одного и того же объекта, не будучи

заблокированным,
однако после завершения каждого из
процессов ожидания необходи-

мо, выполнять операцию освобождения. Это требование справедливо для семафоров,

исключающих семафоров и критических
разделов.

Событие
представляет собой объект, который
создается программой при необхо-

димости информировать потоки о выполнении
определенных действий. В простейшем

случае (ручной
сброс) событие переключает свое состояние
с помощью команд SetEvent

(сигнал включен) и
ResetEvent (сигнал выключен). Когда сигнал
включается, его полу-

чают все потоки,
которые ожидают появления соответствующего
события. Если сигнал

выключается, все
такие потоки блокируются. В отличие от
семафоров и исключающих

семафоров, события
данного типа изменяют свое состояние только
при подаче соответ-

ствующей команды каким-нибудь потоком.

События целесообразно использовать
при условии, что поток должен выполняться

только после того,
как программа обновит свое окно или пользователь введет опреде-

ленную информацию [12]. Ниже представлены
основные функции, предназначенные для

работы с событиями:

73

HANDLE CreateEvent(

LPSECURITY_ATTRIBUTES lpsa,

BOOL bManualReset,

BOOL bInitialState,

LPTSTR lpszEventName );

BOOL SetEvent ( HANDLE hEvent );

BOOL ResetEvent( HANDLE hEvent );

// привилегии доступа

// (по умолчанию = NULL)

// TRUE — событие должно быть

// сброшено вручную

// TRUE — создание события в

// сигнальном состоянии

//имя события (допускается

// значение NULL)

При установке параметра bInitialState функция
CreateEvent создает событие, кото-

рое сразу же будет находиться в сигнальное
состоянии. Функции SetEvent и ResetEvent в

случае успешного
завершения возвращают значение TRUE, при
возникновении ошибки

— значение FALSE.

Параметр
bManualReset функции
CreateEvent позволяет создать событие, сбрасы-

ваемое не вручную, а автоматически.
Автоматически сбрасываемое событие
переходит в

несигнальное
состояние сразу же после выполнения
функции SetEvent. Для таких собы-

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

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

сываемые события целесообразно применять
в таких программах, где один основной

поток подготавливает данные для других, вспомогательных потоков. При готовности

нового набора
данных основной поток устанавливает
событие, по которому освобожда-

ется один вспомогательный поток. Остальные вспомогательные потоки продолжают

ожидать подготовки новых данных.

Наряду с выполнением операций
установки и сброса события можно сгенериро-

вать импульсное событие:

BOOL PulseEvent( hEvent ) ;

Импульсное событие включает
сигнал на короткий промежуток времени.
Приме-

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

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

ваемого автоматически, дает возможность оповестить только один ожидающий поток.

Если не было ни
одного ожидающего потока, то никакой другой
поток не будет опове-

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

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

тока событие сбрасывается автоматически.

Семафоры, исключающие семафоры и события могут совместно использоваться

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

Путем совместного
задействования синхронизирующих
объектов процессы могут коор-

динировать свои действия по аналогии
с тем, как это делают потоки. Существует
три

механизма
совместного использования. Первый из
них — это наследование, при котором

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

ского процесса.
Копируются только те дескрипторы,
которые при создании были поме-

чены как доступные для наследования.

Два других метода
сводятся к созданию второго дескриптора
существующего объ-

екта с помощью
вызова функций. Какая из функций будет вызвана, зависит
от имею-

щейся информации.
При наличии дескрипторов как исходного
процесса, так и процесса,

назначения следует
вызывать функцию DuplicateHandle, при наличии
только имени объ-

74

екта — одну из функций Openxxx. Две программы
могут заранее определить имя совме-

стно используемого объекта.
Кроме того, одна из программ способна передать
другой

это
имя посредством совместно используемой
области памяти функций DDEML (DDE

Management Library —
библиотека управления динамическим
обменом данных) или кана-

ла.

BOOL DuplicateHandle(

HANDLE hSourceProcess,

HANDLE hSource,

HANDLE hTargetProcess,

LPHANDLE lphTarget,

DWORD fdwAccess,

BOOL bInherit,

DWORD fdwOptions );

HANDLE OpenMutex(

DWORD fdwAccess,

BOOL binherit,

LPTSTR lpszName );

HANDLE OpenSemaphore(

DWORD fdwAccess,

BOOL bInherit,

LPTSTR lpszName );

HANDLE OpenEvent(

DWORD fdwAccess,

BOOL bInherit,

LPTSTR lpszName );

// процесс, которому
принадлежит

// исходный объект

// дескриптор исходного объекта

// процесс, который хочет создать

// копию дескриптора

// переменная для записи копии дескриптора

// запрашиваемые привилегии доступа

// может ли наследоваться копия
дескриптора?

// дополнительные операции, например

// закрытие исходного дескриптора

//запрашиваемые привилегии доступа

// TRUE — дочерний процесс может

// наследовать этот дескриптор

// имя исключающего семафора

// запрашиваемые привилегии доступа

//TRUE — дочерний процесс может

// наследовать этот дескриптор

// имя семафора

// запрашиваемые привилегии доступа

// TRUE — дочерний процесс может

// наследовать этот дескриптор

// имя события

Используемый в этом примере тип данных
LPTSTR — это обобщенный текстовый

тип, который компилируется по-разному
в зависимости от того, какой стандарт,
Unicode

или ASCII, поддерживается приложением.

Семафоры, исключающие
семафоры и объекты событий будут
сохраняться в памя-

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

функции CloseHandle не будут закрыты все
дескрипторы соответствующего объекта:

BOOLCloseHandle(hObject);

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

что и исключающий
семафор, но в отличие от последнего
критический раздел не может

наследоваться.
Оба объекта доступны только для
одного процесса. Преимущество кри-

тических разделов перед исключающими
семафорами состоит в том, что они проще
в

управлении и гораздо быстрее работают
[12].

Терминология,
принятая для функций, которые используются
при работе с крити-

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

ния
семафорами, исключающими семафорами и
событиями, однако сами функции вы-

полняют одни и те же операции. В частности, принято говорить не о создании

75

критического раздела, а о
его инициализации.
Процесс не ожидает критический раздел,
а

входит в него, и
не освобождает критический раздел, а
покидает его; к тому же вы не

закрываете дескриптор, а удаляете
объект.

VOID InitializeCriticalSection ( LPCRITICAL_SECTION lpcs );

VOID EnterCriticalSection( LPCRITICAL_SECTION lpcs );

VOID LeaveCriticalSection( LPCRITICAL_SECTION lpcs );

VOID DeleteCriticalSection( LPCRlTICAL_SECTION lpcs );

Переменная типа
LPCRITICAL_SECTION содержит указатель
(а не дескриптор)

критического раздела. Функция
InitializeCriticalSection должна получить указатель на

пустой объект (&cs), который можно
создать следующим образом:

CRITICAL SECTION cs;

2.2.4.
Использование
классов MFC
для
создания
потоков

Способ создания потоков с помощью
функций библиотеки MFC, заключается в

создании
класса, порожденного от класса CWinThread.
Схема этого процесса выглядит

следующим образом:

// Класс CThreadExample

IMPLEMENT_DYNCREATE(CThreadExample, CWinThread)

CThreadExample::CThreadExample()

{

}

// инициализация переменных-членов
класса

CThreadExample::~CThreadExample()

{

}

BOOL CThreadExample::InitInstance()

{

// TODO: здесь следует выполнить инициализацию
потока

//здесь должны выполняться операции
инициализации,

//не связанные с переменными, например
создание

// экземпляров других объектов класса

return TRUE;

}

int CThreadExample::ExitInstance()

{

// TODO: здесь выполняются все операции
очистки для потока

return CWinThread::ExitInstance();

}

BEGIN_MESSAGE_MAP(CThreadExample, CWinThread)

//{{AFX_MSG_MAP(CThreadExample)

76

// ПРИМЕЧАНИЕ — Мастер ClassWizard будет
добавлять/удалять в

// этом месте макросы обработки сообщений

//}}AFX_MSG_MAP

END_MESSAGE_MAP ()

Объект класса CWinThread представляет поток
выполнения в рамках приложения.

Хотя основной поток
выполнения приложения обычно задается
объектом класса, поро-

жденного от
CWinApp, сам класс CWinApp является производным от класса

CWinThread.

Для обеспечения
безопасности потоков в MFC-приложениях
должны применяться

классы, являющиеся
производными от класса CWinThread, библиотека
MFC использует

переменные-члены
этого класса. Потоки, созданные с помощью
функции_beqinthreadex,

не могут использовать ни одной из
API-функций библиотеки MFC.

Поддерживаются два основных типов потоков, а именно рабочие и интерфейс-

ные.
Для рабочих потоков не нужно создавать
цикл обработки сообщений. Такие потоки

могут выполнять фоновые вычисления в электронной таблице без взаимодействия с

пользователем и не должны реагировать
на сообщения.

Интерфейсные потоки,
в отличие от рабочих, обрабатывают
сообщения, получен-

ные от системы
(или от пользователя). Для них необходима
специальная процедура об-

работки сообщений.
Создаются интерфейсные потоки на базе
класса CWinApp или не-

посредственно класса CWinThread.

Объект класса
CWinThread обычно существует в течение всего
времени существо-

вания потока,
однако такой способ функционирования
можно изменить, присвоив пере-

менной-члену m_bAutoDelete значение
FALSE.

Потоки создаются
с помощью функции AfxBeqinThread. Для создания
интерфейс-

ного потока функции AfxBeqinThread следует
передать указатель на класс CRuntimeC-

lass объекта, производного от класса
CWinThread. В случае рабочих потоков функция

AfxBeqinThread вызывается с указанием управляющей функции и параметра, переда-

ваемого последней.

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

параметры,
изменяющие приоритет, размер стека, флаги
создания и атрибуты безопас-

ности потока. Функция
AfxBeqinThread возвращает указатель на новый объект класса

CWinThread.

В качестве альтернативного
варианта можно определить, а затем создать объект,

производный от класса
CWinThread, вызвав функцию
CreateThread данного класса. В

этом случае производный объект может многократно использоваться при последова-

тельных созданиях и уничтожениях потока.

77

Организация многозадачности в ядре ОС

Время на прочтение
22 мин

Количество просмотров 74K

Волею судеб мне довелось разбираться с организацией многозадачности, точнее псевдо-многозадачности, поскольку задачи делят время на одном ядре процессора. Я уже несколько раз встречала на хабре статьи по данной теме, и мне показалось, что данная тема сообществу интересна, поэтому я позволю себе внести свою скромную лепту в освещение данного вопроса.
Сначала я попытаюсь рассказать о типах многозадачности (кооперативной и вытесняющей). Затем перейду к принципам планирования для вытесняющей многозадачности. Рассказ рассчитан скорее на начинающего читателя, который хочет разобраться, как работает многозадачность на уровне ядра ОС. Но поскольку все будет сопровождаться примерами, которые можно скомпилировать, запустить, и с которыми при желании можно поиграться, то, возможно, статья заинтересует и тех, кто уже знаком с теорией, но никогда не пробовал планировщик “на вкус”. Кому лень читать, может сразу перейти к изучению кода, поскольку код примеров будет взят из нашего проекта.
Ну, и многопоточные котики для привлечения внимания.

Введение

Сперва определимся, что означает термин “многозадачность”. Вот определение из русской Википедии:

Многозада́чность (англ. multitasking) — свойство операционной системы или среды программирования обеспечивать возможность параллельной (или псевдопараллельной) обработки нескольких процессов.

Английская дает, на мой взгляд, менее понятное, но более развернутое определение:

In computing, multitasking is a method where multiple tasks, also known as processes, are performed during the same period of time. The tasks share common processing resources, such as a CPU and main memory. In the case of a computer with a single CPU, only one task is said to be running at any point in time, meaning that the CPU is actively executing instructions for that task. Multitasking solves the problem by scheduling which task may be the one running at any given time, and when another waiting task gets a turn. The act of reassigning a CPU from one task to another one is called a context switch.

В нем вводится понятие разделение ресурсов (resources sharing) и, собственно, планирование (scheduling). Именно о планировании (в первую очередь, процессорного времени) и пойдет речь в данной статье. В обоих определениях речь идет о планировании процессов, но я буду рассказывать о планировании на основе потоков.

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

Существует много различных методов планирования. Большинство из них можно отнести к двум основным типам:

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

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

Невытесняющий планировщик

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

Простейший невытесняющий планировщик

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

#include <stdio.h>

#define TASK_COUNT 2

struct task {
   void (*func)(void *);
   void *data;
};

static struct task tasks[TASK_COUNT];

static void scheduler(void) {
   int i;
   for (i = 0; i < TASK_COUNT; i++) {
   	tasks[i].func(tasks[i].data);
   }
}

static void worker(void *data) {
   printf("%sn", (char *) data);
}

static struct task *task_create(void (*func)(void *), void *data) {
   static int i = 0;

   tasks[i].func = func;
   tasks[i].data = data;


   return &tasks[i++];
}


int main(void) {
   task_create(&worker, "First");
   task_create(&worker, "Second");

   scheduler();

   return 0;
}
Результаты вывода:

First
Second

График занятости процессора:

Невытесняющий планировщик на основе событий

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


#include <stdio.h>

#define TASK_COUNT 2

struct task {
	void (*func)(void *);
	void *data;
	int activated;
};

static struct task tasks[TASK_COUNT];

struct task_data {
	char *str;
	struct task *next_task;
};

static struct task *task_create(void (*func)(void *), void *data) {
	static int i = 0;

	tasks[i].func = func;
	tasks[i].data = data;

	return &tasks[i++];
}

static int task_activate(struct task *task, void *data) {
	task->data = data;
	task->activated = 1;

	return 0;
}

static int task_run(struct task *task, void *data) {
	task->activated = 0;
	task->func(data);

	return 0;
}

static void scheduler(void) {
	int i;
	int fl = 1;

	while (fl) {
    	fl = 0;

    	for (i = 0; i < TASK_COUNT; i++) {
        	if (tasks[i].activated) {
            	fl = 1;
            	task_run(&tasks[i], tasks[i].data);
        	}
    	}
	}
}


static void worker1(void *data) {
	printf("%sn", (char *) data);
}

static void worker2(void *data) {
	struct task_data *task_data;

	task_data = data;

	printf("%sn", task_data->str);

	task_activate(task_data->next_task, "First activated");
}

int main(void) {
	struct task *t1, *t2;
	struct task_data task_data;

	t1 = task_create(&worker1, "First create");
	t2 = task_create(&worker2, "Second create");

	task_data.next_task = t1;
	task_data.str = "Second activated";

	task_activate(t2, &task_data);

	scheduler();

	return 0;
}
Результаты вывода:

Second activated
First activated

График занятости процессора

Невытесняющий планировщик на основе очереди сообщений

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

#include <stdio.h>
#include <stdlib.h>

#define TASK_COUNT 2

struct message {
	void *data;
	struct message *next;
};

struct task {
	void (*func)(void *);
	struct message *first;
};

struct task_data {
	char *str;
	struct task *next_task;
};

static struct task tasks[TASK_COUNT];


static struct task *task_create(void (*func)(void *), void *data) {
   static int i = 0;

   tasks[i].func = func;
   tasks[i].first = NULL;

   return &tasks[i++];
}

static int task_activate(struct task *task, void *data) {
	struct message *msg;

	msg = malloc(sizeof(struct message));
	msg->data = data;
	msg->next = task->first;

	task->first = msg;

	return 0;
}

static int task_run(struct task *task, void *data) {
   struct message *msg = data;

   task->first = msg->next;

	task->func(msg->data);

	free(data);

   return 0;
}

static void scheduler(void) {
   int i;
   int fl = 1;
	struct message *msg;

	while (fl) {
   	fl = 0;

   	for (i = 0; i < TASK_COUNT; i++) {
       	while (tasks[i].first) {
           	fl = 1;
           	msg = tasks[i].first;
           	task_run(&tasks[i], msg);
       	}
   	}
	}
}


static void worker1(void *data) {
   printf("%sn", (char *) data);
}

static void worker2(void *data) {
   struct task_data *task_data;

   task_data = data;

   printf("%sn", task_data->str);

   task_activate(task_data->next_task, "Message 1 to first");
   task_activate(task_data->next_task, "Message 2 to first");
}

int main(void) {
   struct task *t1, *t2;
   struct task_data task_data;

   t1 = task_create(&worker1, "First create");
   t2 = task_create(&worker2, "Second create");

   task_data.next_task = t1;
   task_data.str = "Second activated";

   task_activate(t2, &task_data);

   scheduler();

   return 0;
}

Результаты работы:

Second activated
Message 2 to first
Message 1 to first

График занятости процессора

Невытесняющий планировщик с сохранением порядка вызовов

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

#include <stdio.h>
#include <stdlib.h>

#define TASK_COUNT 2

struct task {
	void (*func)(void *);
	void *data;
	struct task *next;
};

static struct task *first = NULL, *last = NULL;

static struct task *task_create(void (*func)(void *), void *data) {
	struct task *task;

	task = malloc(sizeof(struct task));
	task->func = func;
	task->data = data;
	task->next = NULL;

	if (last) {
   	last->next = task;
	} else {
   	first = task;
	}

	last = task;

	return task;
}

static int task_run(struct task *task, void *data) {

	task->func(data);

	free(task);

   return 0;
}

static struct task *task_get_next(void) {
	struct task *task = first;

	if (!first) {
    	return task;
	}

	first = first->next;
	if (first == NULL) {
   	last = NULL;
	}

	return task;
}

static void scheduler(void) {
	struct task *task;

	while ((task = task_get_next())) {
   	task_run(task, task->data);
	}
}

static void worker2(void *data) {
	printf("%sn", (char *) data);
}

static void worker1(void *data) {
	printf("%sn", (char *) data);

	task_create(worker2, "Second create");
	task_create(worker2, "Second create again");
}


int main(void) {
   struct task *t1;

   t1 = task_create(&worker1, "First create");

   scheduler();

   return 0;
}
Результаты работы:

First create
Second create
Second create again

График занятости процессора

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

Вытесняющий планировщик

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

Эти данные — instruction pointer и stack pointer — хранятся в регистрах процессора. Кроме них для корректной работы необходима и другая информация, содержащаяся в регистрах: флаги состояния, различные регистры общего назначения, в которых содержатся временные переменные, и так далее. Все это называется контекстом процессора.

Контекст процессора

Контекст процессора (CPU context) — это структура данных, которая хранит внутреннее состояние регистров процессора. Контекст должен позволять привести процессор в корректное состояние для выполнения вычислительного потока. Процесс замены одного вычислительного потока другим принято называть переключением контекста (context switch).

Описание структуры контекста для архитектуры x86 из нашего проекта:

struct context {
	/* 0x00 */uint32_t eip; /**< instruction pointer */
	/* 0x04 */uint32_t ebx; /**< base register */
	/* 0x08 */uint32_t edi; /**< Destination index register */
	/* 0x0c */uint32_t esi; /**< Source index register */
	/* 0x10 */uint32_t ebp; /**< Stack pointer register */
	/* 0x14 */uint32_t esp; /**< Stack Base pointer register */
	/* 0x18 */uint32_t eflags; /**< EFLAGS register hold the state of the processor */
};

Понятия контекста процессора и переключения контекста — основополагающие в понимании принципа вытесняющего планирования.

Переключение контекста

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

Процедура переключения контекста для архитектуры x86:

	.global context_switch
context_switch:
	movl 0x04(%esp), %ecx      	/* Point ecx to previous registers */
	movl (%esp), %eax          	/* Get return address */
	movl %eax, CTX_X86_EIP(%ecx)   /* Save it as eip */
	movl %ebx, CTX_X86_EBX(%ecx)   /* Save ebx */
	movl %edi, CTX_X86_EDI(%ecx)   /* Save edi */
	movl %esi, CTX_X86_ESI(%ecx)   /* Save esi */
	movl %ebp, CTX_X86_EBP(%ecx)   /* Save ebp */
	add $4, %esp               	/* Move esp in state corresponding to eip */
	movl %esp, CTX_X86_ESP(%ecx)   /* Save esp */
	pushf                      	/* Push flags */
	pop  CTX_X86_EFLAGS(%ecx)  	/* ...and save them */

	movl 0x04(%esp), %ecx      	/* Point ecx to next registers */
	movl CTX_X86_EBX(%ecx), %ebx   /* Restore ebx */
	movl CTX_X86_EDI(%ecx), %edi   /* Restore edi */
	movl CTX_X86_ESP(%ecx), %esi   /* Restore esp */
	movl CTX_X86_EBP(%ecx), %ebp   /* Restore ebp */
	movl CTX_X86_ESP(%ecx), %esp   /* Restore esp */
	push CTX_X86_EFLAGS(%ecx)  	/* Push saved flags */
	popf                       	/* Restore flags */
	movl CTX_X86_EIP(%ecx), %eax   /* Get eip */
	push %eax                  	/* Restore it as return address */

	ret
Машина состояний потока

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

  • Состояния init отвечает за то, что поток создан, но не добавлялся еще в очередь к планировщику, а exit говорит о том, что поток завершил свое исполнение, но еще не освободил выделенную ему память.
  • Состояние run тоже должно быть очевидно — поток в таком состоянии исполняется на процессоре.
  • Состояние ready же говорит о том, что поток не исполняется, но ждет, когда планировщик предоставит ему время, то есть находится в очереди планировщика.

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

Вот так можно представить обобщенную машину состояний:

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

Реализация состояний

Если посмотреть на схему состояний внимательнее, то можно увидеть, что состояния init и wait почти не отличаются: оба могут перейти только в состояние ready, то есть сказать планировщику, что они готовы получить свой квант времени. Таким образом состояние init избыточное.

Теперь посмотрим на состояние exit. У этого состояния есть свои тонкости. Оно выставляется планировщику в завершающей функции, о ней речь пойдет ниже. Завершение потока может проходить по двум сценариям: первый — поток завершает свою основную функцию и освобождает занятые им ресурсы, второй — другой поток берет на себя ответственность по освобождению ресурсов. Во втором случае поток видит, что другой поток освободит его ресурсы, сообщает ему о том, что завершился, и передает управление планировщику. В первом случае поток освобождает ресурсы и также передает управление планировщику. После того, как планировщик получил управление, поток никогда не должен возобновить работу. То есть в обоих случаях состояние exit имеет одно и то же значение — поток в этом состоянии не хочет получить новый квант времени, его не нужно помещать в очередь планировщика. Идейно это также ничем не отличается от состояния wait, так что можно не заводить отдельное состояние.

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

  • active — запущен и исполняется на процессоре
  • waiting — ожидает какого-то события. Кроме того заменяет собой состояния init и exit
  • ready — находится под управлением планировщика, т.е. лежит в очереди готовых потоков в планировщике или запущен на процессоре. Это состояние несколько шире того ready, что мы видим на картинке. В большинстве случаев active и ready, а ready и waiting теоретически ортогональны, но есть специфичные переходные состояния, где эти правила нарушаются. Про эти случаи я расскажу ниже.
Создание

Создание потока включает в себя необходимую инициализацию (функция thread_init) и возможный запуск потока. При инициализации выделяется память для стека, задается контекст процессора, выставляются нужные флаги и прочие начальные значения. Поскольку при создании мы работаем с очередью готовых потоков, которую использует планировщик в произвольное время, мы должны заблокировать работу планировщика со структурой потока, пока вся структура не будет инициализирована полностью. После инициализации поток оказывается в состоянии waiting, которое, как мы помним, в том числе отвечает и за начальное состояние. После этого, в зависимости от переданных параметров, либо запускаем поток, либо нет. Функция запуска потока — это функция запуска/пробуждения в планировщике, она подробно описана ниже. Сейчас же скажем только, что эта функция помещает поток в очередь планировщика и меняет состояние waiting на ready.
Итак, код функции thread_create и thread_init:

struct thread *thread_create(unsigned int flags, void *(*run)(void *), void *arg) {
	int ret;
	struct thread *t;

//…

	/* below we are going work with thread instances and therefore we need to
 	* lock the scheduler (disable scheduling) to prevent the structure being
 	* corrupted
 	*/
	sched_lock();
	{
    	/* allocate memory */
    	if (!(t = thread_alloc())) {
        	t = err_ptr(ENOMEM);
        	goto out;
    	}

    	/* initialize internal thread structure */
    	thread_init(t, flags, run, arg);

	//…

	}
out:
	sched_unlock();

	return t;
}
void thread_init(struct thread *t, unsigned int flags,
		void *(*run)(void *), void *arg) {
	sched_priority_t priority;

	assert(t);
	assert(run);
	assert(thread_stack_get(t));
	assert(thread_stack_get_size(t));

	t->id = id_counter++; /* setup thread ID */

	dlist_init(&t->thread_link); /* default unlink value */

	t->critical_count = __CRITICAL_COUNT(CRITICAL_SCHED_LOCK);
	t->siglock = 0;
	t->lock = SPIN_UNLOCKED;
	t->ready = false;
	t->active = false;
	t->waiting = true;
	t->state = TS_INIT;

	/* set executive function and arguments pointer */
	t->run = run;
	t->run_arg = arg;

	t->joining = NULL;

//...

	/* cpu context init */
	context_init(&t->context, true); /* setup default value of CPU registers */
	context_set_entry(&t->context, thread_trampoline);/*set entry (IP register*/
	/* setup stack pointer to the top of allocated memory
	 * The structure of kernel thread stack follow:
	 * +++++++++++++++ top
	 *                  |
	 *                  v
	 * the thread structure
	 * xxxxxxx
	 * the end
	 * +++++++++++++++ bottom (t->stack - allocated memory for the stack)
	 */
	context_set_stack(&t->context,
			thread_stack_get(t) + thread_stack_get_size(t));

	sigstate_init(&t->sigstate);

	/* Initializes scheduler strategy data of the thread */
	runq_item_init(&t->sched_attr.runq_link);
	sched_affinity_init(t);
	sched_timing_init(t);
}
Режим ожидания

Поток может отдать свое время другому потоку по каким-либо причинам, например, вызвав функцию sleep. То есть текущий поток переходит из рабочего режима в режим ожидания. Если в случае с невытесняющим планировщиком мы просто ставили флаг активности, то здесь мы сохраним наш поток в другой очереди. Ждущий поток не кладется в очередь планировщика. Чтобы не потерять поток, он, как правило, сохраняется в специальную очередь. Например, при попытке захватить занятый мьютекс поток, перед тем как заснуть, помещает себя в очередь ждущих потоков мьютекса. И когда произойдет событие, которое ожидает поток, например, освобождение мьютекса, оно его разбудит и мы сможем вернуть поток обратно в очередь готовых. Подробнее про ожидание и подводные камни расскажем ниже, уже после того, как разберемся с кодом самого планировщика.

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

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

void __attribute__((noreturn)) thread_exit(void *ret) {
	struct thread *current = thread_self();
	struct task *task = task_self();
	struct thread *joining;

	/* We can not free the main thread */
	if (task->main_thread == current) {
    	/* We are last thread. */
    	task_exit(ret);
    	/* NOTREACHED */
	}

	sched_lock();

	current->waiting = true;
	current->state |= TS_EXITED;

	/* Wake up a joining thread (if any).
 	* Note that joining and run_ret are both in a union. */
	joining = current->joining;
	if (joining) {
    	current->run_ret = ret;
    	sched_wakeup(joining);
	}

	if (current->state & TS_DETACHED)
    	/* No one references this thread anymore. Time to delete it. */
    	thread_delete(current);

	schedule();

	/* NOTREACHED */
	sched_unlock();  /* just to be honest */
	panic("Returning from thread_exit()");
}

Трамплин для вызова функции обработки

Мы уже не раз говорили, что, когда поток завершает исполнение, он должен освободить ресурсы. Вызывать функцию thread_exit самостоятельно не хочется — очень редко нужно завершить поток в исключительном порядке, а не естественным образом, после выполнения своей функции. Кроме того, нам нужно подготовить начальный контекст, что тоже делать каждый раз — излишне. Поэтому поток начинает не с той функции, что мы указали при создании, а с функции-обертки thread_trampoline. Она как раз служит для подготовки начального контекста и корректного завершения потока.

static void __attribute__((noreturn)) thread_trampoline(void) {
	struct thread *current = thread_self();
	void *res;

	assert(!critical_allows(CRITICAL_SCHED_LOCK), "0x%x", (uint32_t)__critical_count);

	sched_ack_switched();

	assert(!critical_inside(CRITICAL_SCHED_LOCK));

	/* execute user function handler */
	res = current->run(current->run_arg);
	thread_exit(res);
	/* NOTREACHED */
}
Резюме: описание структуры потока

Итак, для описания задачи в случае с вытесняющим планировщиком нам понадобится достаточно сложная структура. Она содержит в себе:

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

Cоотвественно, описание структуры у нас в проекте выглядит следующим образом:

struct thread {
	unsigned int   	critical_count;
	unsigned int   	siglock;

	spinlock_t     	lock;     	/**< Protects wait state and others. */

	unsigned int   	active;   	/**< Running on a CPU. TODO SMP-only. */
	unsigned int   	ready;    	/**< Managed by the scheduler. */
	unsigned int   	waiting;  	/**< Waiting for an event. */

	unsigned int   	state;    	/**< Thread-specific state. */

	struct context 	context;  	/**< Architecture-dependent CPU state. */

	void        	*(*run)(void *); /**< Start routine. */
	void          	*run_arg;  	/**< Argument to pass to start routine. */
	union {
    	void      	*run_ret;  	/**< Return value of the routine. */
    	void      	*joining;  	/**< A joining thread (if any). */
	} /* unnamed */;

	thread_stack_t 	stack;    	/**< Handler for work with thread stack */

	__thread_id_t  	id;       	/**< Unique identifier. */

	struct task   	*task;     	/**< Task belong to. */
	struct dlist_head  thread_link;  /**< list's link holding task threads. */

	struct sigstate	sigstate; 	/**< Pending signal(s). */

	struct sched_attr  sched_attr;   /**< Scheduler-private data. */
	thread_local_t 	local;
	thread_cancel_t	cleanups;
};

В структуре есть поля не описанные в статье (sigstate, local, cleanups) они нужны для поддержки полноценных POSIX потоков (pthread) и в рамках данной статьи не принципиальны.

Планировщик и стратегия планирования

Напомним, что теперь у нас есть структура потока, включающая в том числе контекст, этот контекст мы умеем переключать. Кроме того, у нас есть системный таймер, который отмеряет кванты времени. Иными словами, у нас готово окружение для работы планировщика.
Задача планировщика — распределять время процессора между потоками. У планировщика есть очередь готовых потоков, которой он оперирует для определения следующего активного потока. Правила, по которым планировщик выбирает очередной поток для исполнения, будем называть стратегией планирования. Основная функция стратегии планирования — работа с очередью готовых потоков: добавление, удаление и извлечение следующего готового потока. От того, как будут реализованы эти функции, будет зависеть поведение планировщика. Поскольку мы смогли определить отдельное понятие — стратегию планирования, вынесем его в отдельную сущность. Интерфейс мы описали следующим образом:

extern void runq_init(runq_t *queue);
extern void runq_insert(runq_t *queue, struct thread *thread);
extern void runq_remove(runq_t *queue, struct thread *thread);
extern struct thread *runq_extract(runq_t *queue);
extern void runq_item_init(runq_item_t *runq_link);

Рассмотрим реализацию стратегии планирования поподробнее.

Пример стратегии планирования

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

void runq_item_init(runq_item_t *runq_link) {
	dlist_head_init(runq_link);
}

void runq_init(runq_t *queue) {
	dlist_init(queue);
}

void runq_insert(runq_t *queue, struct thread *thread) {
	dlist_add_prev(&thread->sched_attr.runq_link, queue);
}

void runq_remove(runq_t *queue, struct thread *thread) {
	dlist_del(&thread->sched_attr.runq_link);
}

struct thread *runq_extract(runq_t *queue) {
	struct thread *thread;

	thread = dlist_entry(queue->next, struct thread, sched_attr.runq_link);
	runq_remove(queue, thread);

	return thread;
}
Планировщик

Теперь мы перейдем к самому интересному — описанию планировщика.

Запуск планировщика

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

int sched_init(struct thread *idle, struct thread *current) {

	runq_init(&rq.queue);
	rq.lock = SPIN_UNLOCKED;

	sched_wakeup(idle);

	sched_ticker_init();

	return 0;
}
Пробуждение и запуск потока

Как мы помним из описания машины состояний, пробуждение и запуск потока для планировщика — это один и тот же процесс. Вызов этой функции есть в запуске планировщика, там мы запускаем поток idle. Что, по сути дела, происходит при пробуждении? Во-первых, снимается пометка о том, что мы ждем, то есть поток больше не находится в состоянии waiting. Затем возможны два варианта: успели мы уже уснуть или еще нет. Почему это происходит, я опишу в следующем разделе “Ожидание”. Если не успели, то поток еще находится в состоянии ready, и в таком случае пробуждение завершено. Иначе мы кладем поток в очередь планировщика, снимаем пометку о состоянии waiting, ставим ready. Кроме того, вызывается перепланирование, если приоритет пробужденного потока больше текущего. Обратите внимание на различные блокировки: все действо происходит при отключенных прерываниях. Для того, чтобы посмотреть, как пробуждение и запуск потока происходит в случае SMP, советую вам обратиться к коду проекта.

/** Locks: IPL, thread. */
static int __sched_wakeup_ready(struct thread *t) {
	int ready;

	spin_protected_if (&rq.lock, (ready = t->ready))
    	t->waiting = false;

	return ready;
}

/** Locks: IPL, thread. */
static void __sched_wakeup_waiting(struct thread *t) {
	assert(t && t->waiting);

	spin_lock(&rq.lock);
	__sched_enqueue_set_ready(t);
	__sched_wokenup_clear_waiting(t);
	spin_unlock(&rq.lock);
}


static inline void __sched_wakeup_smp_inactive(struct thread *t) {
	__sched_wakeup_waiting(t);
}

/** Called with IRQs off and thread lock held. */
int __sched_wakeup(struct thread *t) {
	int was_waiting = (t->waiting && t->waiting != TW_SMP_WAKING);

	if (was_waiting)
    	if (!__sched_wakeup_ready(t))
        	__sched_wakeup_smp_inactive(t);

	return was_waiting;
}

int sched_wakeup(struct thread *t) {
	assert(t);
	return SPIN_IPL_PROTECTED_DO(&t->lock, __sched_wakeup(t));
}

Ожидание

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

#define SCHED_WAIT_TIMEOUT(cond_expr, timeout) 
	((cond_expr) ? 0 : ({                                            
		int __wait_ret = 0;                                          
		clock_t __wait_timeout = timeout == SCHED_TIMEOUT_INFINITE ? 
			SCHED_TIMEOUT_INFINITE : ms2jiffies(timeout);            
		                                                             
		threadsig_lock();                                            
		do {                                                         
			sched_wait_prepare();                                    
			                                                         
			if (cond_expr)                                           
				break;                                               
			                                                         
			__wait_ret = sched_wait_timeout(__wait_timeout,          
											&__wait_timeout);        
		} while (!__wait_ret);                                       
		                                                             
		sched_wait_cleanup();                                        
		                                                             
		threadsig_unlock();                                          
		__wait_ret;                                                  
	}))

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

A — active
R — ready
W — wait

На картинке буквами обозначено наличие состояний. Светло-зеленый цвет — состояние потока до wait_prepare, зеленый — после wait_prepare, а темно-зеленый — вызов потоком перепланирования.
Если событие не успеет произойти до перепланирования, то все просто — поток уснет и будет ждать пробуждения:

Перепланирование

Основная задача планировщика — планирование, прошу прощения за тавтологию. И мы наконец подошли к моменту когда можно разобрать как этот процесс реализован у нас в проекте.
Во-первых перепланирование должно выполняться при заблокированном планировщике. Во-вторых мы хотим дать возможность разрешить вытеснение потока или нет. Поэтому мы вынесли логику работы в отдельную функцию окружили ее вызов блокировками и вызвали ее указав, что в этом месте мы не позволяем вытеснение.
Далее идут действия с очередью готовых потоков. Если активный на момент перепланирования потока не собирается уснуть, то есть если у него не выставлено состояние waiting, мы просто добавим его в очередь потоков планировщика. Затем мы достаем самый приоритетный поток из очереди. Правила нахождения этого потока реализуются с помощью стратегии планирования.
Затем если текущий активный поток совпадает с тем который мы достали из очереди, нам не нужно перепланирование и мы можем просто выйти и продолжить выполнение потока. В случае же если требуется перепланирование, вызывается функция sched_switch, в которой выполняются действия необходимые планировщику и главное вызывается context_switch который мы рассматривали выше.
Если же поток собирается уснуть, находится в состоянии waiting, то он не попадает в очередь планировщика, и с него снимают метку ready.
В конце происходит обработка сигналов, но как я отмечала выше, это выходит за рамки данной статьи.


static void sched_switch(struct thread *prev, struct thread *next) {
    sched_prepare_switch(prev, next);

    trace_point(__func__);

    /* Preserve initial semantics of prev/next. */
    cpudata_var(saved_prev) = prev;
    thread_set_current(next);
    context_switch(&prev->context, &next->context);  /* implies cc barrier */
    prev = cpudata_var(saved_prev);

    sched_finish_switch(prev);
}


static void __schedule(int preempt) {
	struct thread *prev, *next;
	ipl_t ipl;

	prev = thread_self();

	assert(!sched_in_interrupt());
	ipl = spin_lock_ipl(&rq.lock);

	if (!preempt && prev->waiting)
    	prev->ready = false;
	else
    	__sched_enqueue(prev);

	next = runq_extract(&rq.queue);

	spin_unlock(&rq.lock);

	if (prev != next)
    	sched_switch(prev, next);

	ipl_restore(ipl);

	assert(thread_self() == prev);

	if (!prev->siglock) {
    	thread_signal_handle();
	}
}

void schedule(void) {
	sched_lock();
	__schedule(0);
	sched_unlock();
}

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

В качестве примера я использовала следующий код:

#include <stdint.h>
#include <errno.h>
#include <stdio.h>
#include <util/array.h>

#include <kernel/thread.h>

#include <framework/example/self.h>


/**
 * This macro is used to register this example at the system.
 */
EMBOX_EXAMPLE(run);

/* configs */
#define CONF_THREADS_QUANTITY  	0x8 /* number of executing threads */
#define CONF_HANDLER_REPEAT_NUMBER 300  /* number of circle loop repeats*/

/** The thread handler function. It's used for each started thread */
static void *thread_handler(void *args) {
    int i;
    /* print a thread structure address and a thread's ID */
    for(i = 0; i < CONF_HANDLER_REPEAT_NUMBER; i ++) {
   	 printf("%d", *(int *)args);
    }
    return thread_self();
}

/**
 * Example's executing routine
 * It has been declared by the macro EMBOX_EXAMPLE
 */
static int run(int argc, char **argv) {
    struct thread *thr[CONF_THREADS_QUANTITY];
    int data[CONF_THREADS_QUANTITY];
    void *ret;
    int i;

    /* starting all threads */
    for(i = 0; i < ARRAY_SIZE(thr); i ++) {
   	 data[i] = i;   	
   	 thr[i] = thread_create(0, thread_handler, &data[i]);   	
    }

    /* waiting until all threads finish and print return value*/
    for(i = 0; i < ARRAY_SIZE(thr); i ++) {
   	 thread_join(thr[i], &ret);   	
    }
    printf("n");

    return ENOERR;
}

Собственно, это почти обычное приложение. Макрос EMBOX_EXAMPLE(run) задает точку входа в специфичный тип наших модулей. Функция thread_join дожидается завершения потока, пока я ее тоже не рассматривала. И так уже очень много получилось для одной статьи.
Результат запуска этого примера на qemu в составе нашего проекта следующий

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

2.2.1.
Основные
принципы
многозадачности
в Windows

В
Windows каждый процесс имеет свое собственное виртуальное адресное про-

странство (4Gb).
Процесс состоит из кода, данных и других
системных ресурсов, таких

как открытые файлы, каналы
(pipes), синхронизирующие объекты. Однако процесс

статический объект,
который сам по себе действия не производит.
Поток (thread) — базо-

вый объект,
которому операционная система
распределяет время центрального процес-

сора. Поток выполняет
команды программы с учетом заданного
ему маршрута. Каждый

57

процесс представляет собой
один начальный поток, который иногда
называют первич-

ным
потоком.

Первичный поток способен создать вторичные потоки. Все потоки, принадлежа-

щие одному процессу,
имеют совместный доступ к его ресурсам.
Все они работают под

управлением команд
одной и той же программы, обращаются к
одним и тем же глобаль-

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

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

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

запуска
«дочернего» процесса необходимо временно приостанавливать основной про-

цесс, что приводит
к замедлению выполнения программы в
целом. Дополнительные по-

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

асинхронные операции, работает
одновременно с несколькими окнами.

Организация многозадачности в
MS Windows различается в линейках 9x и NT. В

Windows
9x реализована приоритетная многозадачность.
В данном случае каждому

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

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

дующему потоку.
Это не дает возможность программам
полностью захватывать ресурсы

процессора.

Windows
NT использует вытесняющую
многозадачность.
Выполнение всех про-

цессов строго
контролируется операционной системой.
Операционная система выделяет

каждой из программ
некоторое количество процессорного
времени и периодически про-

изводит переключение
между запущенными на компьютере
программами. Обратившись

к специальному
системному вызову, вы можете как бы
приостановить (sleep) выполне-

ние программы,
однако если вы этого не сделаете, со
временем операционная система

сделает это за
вас. Подвисание одной из программ не
приведет к подвисанию всей сис-

темы.

В общем и целом,
вытесняющая многозадачность, которая
ранее рассмотрена бо-

лее подробно, выглядит
привлекательней. Однако
за все приходится платить. И в пер-

вую очередь расплачиваться приходится программистам, которые
разрабатывают при-

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

Представьте себе, что на компьютере
работает несколько программ, использующих
один

и тот же файл журнала ошибок. Если вам
необходимо сделать запись в этом файле,
то в

Windows 9x вы можете
без лишних сложностей открыть файл,
записать в него данные, а

затем закрыть его.
Если при этом вы ни разу не обратились
к специальным системным

функциям, вы можете
быть уверенными, что в то время, пока вы
работали с файлом, ни

одна другая
программа не обратилась к этому же файлу
(так как фактически в ходе ра-

боты с файлом
на компьютере работает только одна
ваша программа, а все остальные

программы находятся в состоянии
ожидания).

В среде Windows NT все
не так просто. Предположим, что один из
потоков открыл

файл и начал в него
запись, но в этот момент операционная система
передала управле-

ние другому потоку.
Что произойдет, если другой поток
попытается открыть тот же са-

мый файл? Либо
этого сделать не удастся, либо другой
поток откроет файл в режиме со-

вместного доступа
(for sharing), что может не соответствовать
вашим ожиданиям. Даже

если в ходе работы с
файлом операционная система не осуществила
передачу управле-

ния другому потоку,
получить доступ к файлу может попытаться
поток, работающий на

другом процессоре. В результате вы
столкнетесь с той же проблемой.

58

На системном уровне каждый поток представляет собой
объект, созданный сис-

темным менеджером
объектов. Аналогично с остальными
системными объектами, поток

содержит данные
(атрибуты) и методы
(функции). Схематически объект-поток может

быть представлен в следующем виде
(рис.2.5.) [12]:

Стандартный
объект
заголовка

Атрибуты
потока

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

Контекст

Динамический приоритет

Базовый приоритет

Привязанность к архитектуре

процессора

Время выполнения

Статус оповещения

Счетчик прерываний

Маркер передачи прав доступа

Порт завершения

Код завершения

Методы
потока

Создание потока

Открытие потока

Запрос информации о потоке

Установка информации о потоке

Текущий поток

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

Получение контекста

Установление контекста

Прерывание

Возобновление

Предупреждение

Проверка поступления преду-

преждения

Регистрация порта завершения

Рис.2.5.
Схема
объекта
потока.

Для большинства методов потока имеются
соответствующие API — функции Win32.

Windows защищает свои внутренние структуры от прямого вмешательства пользова-

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

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

ровать или изменять
параметры системных объектов. Все
операции с ними выполняются

посредством функций
Win32 API. Windows предоставляет дескриптор, идентифици-

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

честве аргумента одной из
API-функций. Свои дескрипторы имеют потоки, процессы,

семафоры, файлы и др. объекты.
Внутренняя структура объектов
доступна только ме-

неджеру объектов.
Функция, создающая поток, возвращает
дескриптор нового объекта.

С помощью этого дескриптора можно
выполнить следующие операции:

• повысить или понизить плановый
приоритет потока;

• приостановить поток и возобновить
его выполнение;

• прекратить выполнение потока;

• определить код завершения потока.

В ОС
Windows потоки, процессы, семафоры и исключающие семафоры могут

иметь несколько разных дескрипторов. Завершив работу с объектом, необходимо вы-

звать функцию
CloseHandle, которая закрыв последний дескриптор, сама уничтожит

объект. В целом в
Windows каждый процесс не может одновременно
поддерживать бо-

лее 65536 открытых дескрипторов.

Работа
с потоками не сводится только к их
запуску и остановке. Необходимо обес-

печить совместное
функционирование потоков. Для организации
эффективного взаимо-

действия между несколькими потоками необходимо производить контроль за их вре-

менными параметрами. Контроль
осуществляется [12]:

59

• установлением приоритетов;

• синхронизацией.

Приоритет потока определяет, насколько часто
данный поток получает доступ к

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

общим ресурсам.
Когда системная программа-планировщик
останавливает один поток и

ищет другой, который
должен быть запущен следующим, она
отдает предпочтение по-

токам, имеющим наиболее высокий приоритет. Обработчики системных прерываний

всегда имеют более
высокий приоритет по сравнению с
пользовательскими процессами.

Каждому процессу
присущ собственный приоритет. Базовый
плановый приоритет пото-

ка определяется
на основе приоритета процесса, который
является владельцем этого по-

тока. Всего различают
32 уровня приоритета от 0 до 31. При этом
приоритеты уровня от

0 до 15 называются
переменными приоритетами, а от 16 до 31 –
фиксированными при-

оритетами. Схема наследования приоритетов
потока показана на рис.2.6 [12].

31

30

29

28

27

26

25

24

23

22

21

20

19

18

17

16

15

14

13

12

11

10

9

8

7

6

5

4

3

2

1

0

Уровни

приоритета

Класс
реального

времени

Наивысший

класс

Класс
переднего

плана

Фоновый
класс

Класс
простоя

Базовый

приоритет

процесса

Диапазон
базового

приоритета
потока,

владельцем
которо-

го
является
процесс

с
приоритетом
пе-

реднего
плана

Диапазон
дина-

мического
при-

оритета
для
то-

го
же
самого

потока

Рис.2.6
Схема
наследования
приоритета
потоков
от
исходного
приоритета
процесса

60

Среди атрибутов объекта потока различают
базовый и динамический приоритеты.

При вызове команды
для изменения приоритета потока меняется
его базовый приоритет,

который не может
быть выше или ниже приоритета
процесса-владельца более чем на 2

уровня.
Операционная система способствует
«продвижению» потока для его выполне-

ния. Для этого система поддерживает динамический приоритет потоков, которые вы-

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

считывает
данные с диска временно повышается
приоритет всех потоков такого процес-

са. Эти временные
приращения в сумме с базовым приоритетом
образуют динамический

приоритет процесса.
Планировщик определяет очередность
выполнения потоков на ос-

новании их динамического приоритета. Со следующим тактом процесса приращение

приоритета начинает
уменьшаться на один уровень, постепенно
достигая уровня базово-

го приоритета.

Выбирая поток,
который будет выполняться следующим,
программа-планировщик

начинает просмотр
очереди заданий с потоков, имеющий
наивысший приоритет, выпол-

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

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

приостановлены
или заблокированы. В каждый момент
времени поток может находить-

ся в одном из шести состояний.

Ready(готов) — поставлен в очередь и
ожидает выполнения.

Standby(ближайший) — готов быть выполненным
следующим.

Running
(выполнение) — находится в режиме
выполнения и взаимодействует с централь-

ным процессором.

Waiting(ожидание) не выполняется, ожидая
сигнала выполнения.

Transition(промежуточное) будет выполняться после того, как система загрузит его

контекст.

Terminated(завершен) — выполнение
завершено, однако объект не удален.

Когда
программа-планировщик выбирает из
очереди готовый к выполнению поток,

она
загружает его контекст.
В состав контекста входит набор значений
регистров про-

цессора, стек ядра,
блок параметров окружения потока и
пользовательский стек в адрес-

ном пространстве процесса, который является владельцем данного потока. Если часть

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

пока система соберет все составные
части контекста.

Чтобы потоки могли надежно работать,
их необходимо синхронизировать. Пред-

ставьте себе, что
один поток создает кисть, а затем создает
несколько потоков, которые

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

должен уничтожать
кисть до тех пор, пока другие потоки не
завершат операции рисова-

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

и записывает их в
файл, а другие потоки считывают данные
из этого файла и образовы-

вают веденный
текст. Считывание не должно происходить
в тот момент, когда идет за-

пись. В обоих случаях
надо принять меры по
координации последовательности опера-

ций в нескольких потоков.

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

Boolean, которую один из потоков будет
использовать с целью информирования
других

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

присвоить
переменной bDone
значение TRUE,
а потоки, считывающие данные из файла,

будут циклически
просматривать эту переменную до тех
пор, пока ее значение не изме-

нится. Такая схема вполне работоспособна, однако циклический просмотр флага не-

сколькими потоками
занимает много времени процессора. Вот
почему в Win32 поддер-

живается набор синхронизирующих объектов
[9, 12]. Перечислим их.

Объект типа
исключающий семафор функционирует
подобно узкой двери, «про-

пуская» (т.е. давая допуск) одновременно
по одному потоку.

61

Объект типа семафор
функционирует подобно многостворчатой
двери, ограничи-

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

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

ком, для которого он адресован.

Объект
типа критический
раздел
аналогичен исключающему семафору, но
рабо-

тает только в пределах одного процесса.

Все указные объекты являются системными и создаются менеджером объектов.

Хотя каждый
синхронизирующий объект координирует
определенный вид взаимодейст-

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

ределенную
синхронизируемую операцию, ожидает
ответ от одного из этих объектов и

осуществляет свои функции только после получения на то разрешения. Программа-

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

нимали времени
центрального процессора. После получения
соответствующего сигнала

планировщик
возобновляет работу потока. Место и
способ получения сигнала зависит

от конкретного объекта.

Исключающие семафоры, обычные семафоры и события позволяют координиро-

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

разделы воспринимаются лишь потоками одного процесса. Если
один процесс создает

другой процесс,
дочерний процесс часто наследует
дескрипторы имеющихся синхрони-

зирующих объектов. Объекты критических
разделов не могу быть унаследованы.

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

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

екты могут находиться
в двух состояниях: при наличии сигнала
и при отсутствии тако-

вого. Потоки
взаимодействуют с синхронизирующими объектами
путем установки сиг-

нала или путем его
ожидания. Поток, находящейся в состоянии
ожидания, блокируется

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

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

включает сигнал и освобождает объект.

Потоки могут
ожидать разрешения не только от
исключающих и обычных семафо-

ров, событий и
критических разделов, но и от других
объектов. Иногда возникает ситуа-

ция,
когда необходимо ожидать разрешения от процесса,
другого потока, таймера или

файлового объекта.
Все эти объекты имеют свое предназначение,
но подобно синхрони-

зирующим объектом
они способны давать сигналы разрешения.
Процессы и потоки сиг-

нализируют о своем завершении, объекты-таймеры
— об истечении определенного ин-

тервала времени, а файловые объекты
— о завершении операций чтения или записей

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

2.2.2. API-функции
для
реализации
механизма
многозадачности

В Win32 API определены
следующие функции работы с процессами
и потоками [4]

(таблица 2.1.)

Таблица
2.1

Название функции

1

AttachThreadInput

CommandLineToArgvW

CreateProcess

CreateRemoteThread

CreateThread

Выполняемое действие

2

Переключение механизмов ввода с одной
нити на дру-

гую

Производит разбор командной строки в
Unicode

Создает процесс

Создает поток в адресном пространстве
другого про-

цесса

Создает поток

62

1

2

Продолжение
табл. 2.1

ExitProcess

ExitThread

FreeEnvironmentStrings

GetCommandLine

GetCurrentProcess

GetCurrentProcessId

GetCurrentThread

GetCurrentThreadId

GetEnvironmentStrings

GetEnvironmentVariable

GetExitCodeProcess

GetExitCodeThread

GetPriorityClass

GetProcessAffinityMask

GetProcessShutdownParame-

ters

GetProcessTimes

GetCurrentProcess

GetProcessWorkingSetSize

GetStartupInfo

GetThreadPriority

GetThreadTimes

OpenProcess

ResumeThread

SetEnvironmentVariable

SetPriorityClass

Завершает процесс и все его потоки

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

Освобождает память области переменных
среды

Возвращает указатель на командную
строку

Возвращает описатель (handle) текущего
процесса

Возвращает идентификатор текущего
процесса

Возвращает описатель
(handle) текущего потока

Возвращает идентификатор текущего
потока

Возвращает строку переменных среды

Возвращает значение указанной переменной
среды

Возвращает код завершения процесса

Возвращает код завершения потока

Возвращает класс приоритета процесса

Сообщает, на каких процессорах разрешено
исполне-

ние процесса

Сообщает параметры поведения процесса
при завер-

шении работы системы

Возвращает временные характеристики
процесса

Сообщает версию Windows, для которой
предназна-

чен процесс

Возвращает характеристики доступного
процессу ад-

ресного пространства

Возвращает параметры процесса, полученные
им при

создании

Сообщает приоритет указанного потока

Возвращает временные характеристики
указанного

потока

Возвращает описатель (handle) указанного
процесса

Уменьшает счетчик задержаний потока
(или запуска-

ет его)

Устанавливает значение указанной
переменной среды

Устанавливает класс приоритета процесса

SetProcessShutdownParameters Устанавливает параметры
поведения процесса при

завершении работы системы

SetThreadAffinityMask

SetThreadPriority

Sleep

SleepEx

SetProcessWorkingSetSize

SuspendThread

TerminateProcess

TerminateThread

TlsAlloc

Устанавливает, на каких процессорах
разрешено ис-

полнение потока

Устанавливает приоритет указанного
потока

Задерживает исполнение потока на
указанное коли-

чество миллисекунд

Задерживает исполнение до наступления
события

ввода/вывода или на время

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

адресного пространства

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

Завершает указанный процесс

Завершает указанный поток

Распределяет индекс локальной памяти
потока

(thread local storage TLS)

63

TlsFree

1

Освобождает индекс TLS

2

Окончание
табл. 2.1

TlsGetValue

TlsSetValue

Возвращает данные, размещенные в TLS с
указанным индексом

Помещает данные в TLS с указанным индексом

WaitForInputIdle Ждет, пока не начнется ввод
для указанного процесса

WinExec

Выполняет указанное приложение

Подробное описание
функций приведено в Win32 Programmer’s
Reference. Далее

рассмотрим только некоторые основные
функции [4].

Функция
CreateProcess() — создает новый процесс и его первичный поток. Новый

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

BOOL CreateProcess(LPCTSTR lpApplicationName,
// имя исполняемого файла

LPTSTR lpCommandLine, // командная строка

LPSECURITY_ATTRIBUTES lpProcessAttributes, // атрибуты
защиты процесса

LPSECURITY_ATTRIBUTES lpThreadAttributes, // атрибуты защиты
потока

BOOL bInheritHandles, // флаг наследования
описателей

DWORD dwCreationFlags, // флаги создания

LPVOID lpEnvironment, // указатель блока переменных
среды

LPCTSTR lpCurrentDirectory, // текущий каталог

LPSTARTUPINFO lpStartupInfo, // блок начальных
параметров

LPPROCESS_INFORMATION lpProcessInformation //
указатель

// структуры, описывающей порожденный
процесс

);

Функция возвращает TRUE в случае успеха
и FALSE — в случае неудачи.

Параметры:

lpApplicationName
— указатель на строку, содержащую имя исполняемой программы.

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

щем каталоге. Параметру может быть
присвоено значение NULL. В этом случае в

качестве имени
файла выступает первая выделенная пробелами
лексема из строки

lpCommandLine;

lpCommandLine — указатель
командной строки. Если параметр
lpApplicationName имеет

значение NULL, то
имя исполняемого файла выделяется из
lpCommandLine, а по-

иск исполняемого
файла производится в соответствии с
правилами, действующими

в системе;

lpProcessAttributes —
указатель на структуру, описывающую
параметры защиты процесса.

Если параметру присвоено значение
NULL, то устанавливаются атрибуты “по

умолчанию”;

lpThreadAttributes- указатель
на структуру, описывающую параметры
защиты первично-

го
потока. Если параметру присвоено
значение NULL, то устанавливаются атрибу-

ты “по умолчанию”;

bInheritHandles
— определяет, будет ли порожденный процесс наследовать описатели

(handles) объектов
родительского процесса. Например, если
родительский процесс

AB, то он получил
описатель процесса B и может им
манипулировать. Если теперь

он порождает
процесс C с параметром bInheritHandles равным
TRUE, то и процесс

C сможет работать с описателем процесса
B;

dwCreationFlags
— определяет некоторые дополнительные условия
создания процесса и

его класс приоритета;

lpEnvironment- указатель на блок переменных
среды порожденного процесса. Если этот

параметр равен
NULL, то порожденный процесс наследует
среду родителя. Иначе

64

он должен указывать на завершающийся
нулем блок строк, каждая из которых за-

вершается нулем (аналогично
DOS);

lpCurrentDirectory — указатель
на строку, содержащую полное имя текущего
каталога по-

рожденного процесса.
Если этот параметр равен NULL, то
порожденный процесс

наследует каталог родителя;

lpStartupInfo
— указатель на структуру
STARTUPINFO, которая определяет параметры

главного окна порожденного процесса;

lpProcessInformation —
указатель на структуру, которая
будет заполнена информацией о

порожденном процессе после возврата
из функции.

Пример: программа, запускающая
Microsoft Word

#include <windows.h>

#include <conio.h>

#include <stdio.h>

main()

{

PROCESS_INFORMATION pi ;

STARTUPINFO si ;

ZeroMemory( &si, sizeof(si)) ;

si.cb = sizeof( si ) ;

printf( «Press any key to start WinWord — » );

getch() ;

CreateProcess( NULL, «WinWord», NULL, NULL, FALSE, 0,

NULL, NULL, &si, &pi ) ;

return 0 ;

}

Функция CreateThread()
— создает новый поток в адресном пространстве
процесса.

Формат функции [8]:

HANDLE CreateThread(

LPSECURITY_ATTRIBUTES lpThreadAttributes, // атрибуты защиты
потока

DWORD dwStackSize,

// размер стека в байтах

LPTHREAD_START_ROUTINE lpStartAddress,
//указатель на функцию потока

LPVOID lpParameter,

BDWORD dwCreationFlags,

LPDWORD lpThreadId );

// аргумент, передаваемый в функцию
потока

// флаги управления созданием потока

// область памяти для возвращения

//идентификатора потока

Функция возвращает описатель порожденного
потока. Параметры:

lpThreadAttributes — указатель на структуру,
описывающую параметры защиты потока.

Если параметру присвоено значение
NULL, то устанавливаются атрибуты “по

умолчанию”;

dwStackSize — устанавливает
размер стека, который отводится потоку.
Если параметр ра-

вен нулю, то устанавливается стек,
равный стеку первичного потока;

lpStartAddress — адрес функции, которую
будет исполнять поток. Функция имеет
один

32-битный аргумент и возвращает 32 битное
значение;

lpParameter — параметр, передаваемый в функцию,
которую будет исполнять поток;

dwCreationFlags
— дополнительный флаг, который управляет созданием потока. Если

этот параметр
равен CREATE_SUSPENDED, то поток после порождения
не запус-

кается на исполнение до вызова функции
ResumeThread;

65

lpThreadId — указатель на
32-битную переменную, которой будет присвоено значение

уникального идентификатора потока.

Пример: программа, порождающая поток

#include <stdio.h>

#include <conio.h>

#include <windows.h>

DWORD WINAPI Output( LPVOID Param )

{

while( TRUE )

{

printf( «A» ) ;

Sleep(100) ;

}

return( 0 ) ;

}

main()

{

HANDLE hThread ;

DWORD ThreadId ;

hThread = CreateThread( NULL, 0, Output, NULL, 0, &ThreadId ) ;

getch() ;

TerminateThread( hThread, 0 ) ;

return(0) ;

}

Потоки, обладающие высоким приоритетом, занимают большую часть времени

центрального
процессора, раньше завершают свою работу
и способны быстрее реагиро-

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

приоритет, ничего хорошего не выйдет.
Дело в том, что если нескольким потокам
будет

присвоен один и
тот же приоритет (не имеет значения,
высокий или низкий), программа

— планировщик выделит им одинаковое
время работы центрального процессора,
и сама

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

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

равной степени
и применимо и к процессом. Старайтесь
ограничивать приоритет всех

потоков и процессов
низким или средним уровнем и присваивайте
им высоких приори-

тет только по мере необходимости [8, 12].

Приведенные ниже функции проверяют или
изменяют базовый приоритет потока.

BOOL SetThreadPriority(

HANDLE hThread
// дескриптор потока

int iPriority );
// новый уровень приоритета

int GetThreadPriority ( HANDLE hThread );

Функция SetThreadPriority
возвращает значение TRUE
в случае успешного завер-

шения потока, а значение FALSE
-при возникновении ошибки. Функция GetThread-

Priority возвращает значение, определяющее приоритет. Для обозначения возможных

значений приоритета в обеих функциях
используется набор констант.

66

THREAD_PRIORITY_LOWEST

са

На два уровня ниже приоритета процес-

THREAD_PRIORITY_BELOW_NORMALНа один уровень ниже
приоритета про-

цесса

THREAD_PRIORITY_NORMAL

цесса

THREAD_PRIORITY_ABOVE_NORMAL

цесса

THREAD_PRIORITY_HIGHEST

са

THREAD_PRIORITY_TIME_CRITICAL

ских процессов)

THREAD_PRIORITY_IDLE

ских процессов)

Тот же уровень приоритета, что и у про-

На один уровень выше приоритета про-

На два уровня выше приоритета процес-

Уровень 15 (для обычных пользователь-

Уровень 1 (для обычных пользователь-

Прерванный поток приостанавливает
свое выполнение и не учитывается при
рас-

пределении времени
центрального процессора. Поток остается
в таком состоянии до тех

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

вести, в частности,
в том случае, если пользователь прерывает выполнение
определен-

ной задачи. До тех
пор, пока задание не будет отменено,
поток можно перевести в со-

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

выполнение с той
точки, где он был остановлен. Для
приостановки и возобновления вы-

полнения потоков служат функции

DWORD SuspendThread ( HANDLE hThread ) ;

DWORD ResumeThread( HANDLE hThread );

Один и тот же
поток можно последовательно остановить
несколько раз, не возоб-

новляя его выполнения, однако каждой последовательной команде SuspendThread

должна соответствовать ответная команда
ResumeThread.
Система отчитывает количе-

ство отмененных команд с помощью счетчика прерываний.
Каждая команда Suspend-

Thread
инкрементирует значения счетчика, а
каждая команда ResumeThread
декремен-

тирует его. Обе
функции возвращают предыдущее значение
счетчика в виде параметра

типа DWORD.
Поток возобновит свое выполнение только
в том случае, если счетчик

примет значение 0.

Поток способен остановить
себя, но он не в состоянии самостоятельно
возобновить

свое выполнение.
Однако он может на нужное время перенести
себя в режим ожидание.

Команда
Sleep задерживает выполнения потока, удаляя его из очереди программы-

планировщика до
тех пор, пока не пройдет заданный
интервал времени. Интерактивные

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

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

режима ожидания
предпочтительнее задействования
«пустого» цикла, поскольку в этом

случае не используется время центрального
процессора.

Для осуществления паузы в течение заданного времени поток вызывает следую-

щие функции:

VOID Sleep ( DWORD dwMilliseconds ) ;

DWORD SleepEx(DWORD dwMilliseconds,

BOOL bAlertable );

// продолжительность паузы

// TRUE — возобновить работу

// при завершении операции ввода/вывода

67

Расширенная функция SleepEx обычно работает
совместно с функциями фонового

ввода/вывода и
может использоваться для инициации
команд чтения или записи, не тре-

буя их завершения.
Эти операции выполняются в фоновом
режиме. По завершении опе-

рации система извещает об этом пользователя, обращаясь к предусмотренной в про-

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

ввод/вывод
далее будет рассмотрен более подробно)
чаще всего применяется в интерак-

тивных программах,
которые должны реагировать на команды
пользователя, не преры-

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

магнитной ленте или с сетевым диском.

Параметр bAlertable
функции SleepEx имеет тип Boolean. Если этот
параметр равен

TRUE, система может преждевременно возобновить выполнение потока при условии,

что перекрывающаяся
операция ввода/вывода завершилась до
истечения заданного вре-

мени
ожидания. В случае досрочного прерывания
функция sleepEx возвращает значение

WAIT_IO_COMPLETION, по истечении указанного
времени — значение 0.

По специальному запросу поток возвращает свои дескриптор и идентификатор.

Указанные ниже функции позволяют
получить информацию о текущем потоке:

DWORD GetCurrentThreadId( VOID ) ;

HANDLE GetCurrentThread( VOID );

Результирующее
значение функции GetCurrentThreadId совпадает
со значением па-

раметра lpIDThread
после выполнения функции CreateThread и
однозначно идентифици-

рует поток в
системе. Хотя идентификатор потока
нужен лишь для незначительного ко-

личества функций
Win32 API, он может использоваться с целью мониторинга

системных потоков
без необходимости поддерживать открытый
дескриптор для каждого

потока. Открытый дескриптор защищает
поток от уничтожения.

Дескриптор, полученный в результате выполнения функции
GetCurrentThread,

служит для тех же целей, что и дескриптор, возвращенный функцией
CreateThread. И

несмотря на то,
что он может использоваться аналогично другим
дескрипторам, на са-

мом деле этот параметр является псевдодескриптором. Псевдодескриптор
— это специ-

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

тому,
как одиночная точка (.) в DOS всегда
указывает на текущий каталог, а параметр

this в C++ определяет
текущий объект. Константа — псевдодескриптор,
полученная в ре-

зультате выполнения
функции GetCurrentThread, указывает на текущий
поток. В отличие

от настоящих
дескрипторов, псевдодескриптор не может
передаваться другим потокам.

Чтобы получить настоящий переносимый дескриптор потока, необходимо выполнить

следующие действия:

HANDLE hThread;

hThread = DuplicateHandle(

GetCurrentProcess(),

GetCurrentThread(),

GetCurrentProcess(),

&hThread,

0,

FALSE,

DUPLICATE_SAME_ACCESS );

68

// процесс-источник

// исходный дескриптор

// целевой процесс

// новый дублирующийся дескриптор

//привилегии доступа (подавляемые

// последним параметром)

// дочерние объекты не унаследуют

// дескриптор

// привилегии доступа копируются у

// исходного дескриптора

Хотя функция CloseHandle не влияет на
псевдодескрипторы, дескриптор, созданный с

помощью функции
DuplicateHandle, является настоящим и в конце
концов должен быть

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

GetCurrentThread, поскольку в этом случае подразумевается, что поток имеет полный

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

По аналогии с тем, как
Windows-программа завершается по достижении конца

функции WinMain, поток
обычно прекращает свое существование
при достижении кон-

ца функции, в
которой был начат. Когда он достигает
конца стартовой функции, система

автоматически вызывает команду
ExitThread, имеющую такой синтаксис:

VOID ExitThread( DWORD dwExitCode );

Хотя операционная
система вызывает функцию ExitThread
автоматически, при не-

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

разом:

DWORD ThreadFunction( LPDWORD lpdwParam )

{

HANDLE hThread = CreateThread( <параметры>);

// далее следуют стандартные операции
инициализации;

// проверка наличия ошибочных условий

if ( <условие возникновения ошибки> )

{

ExitThread( ERROR_CODE ); // прекратить работу потока

}

// ошибки нет, работа продолжается

return( SUCCESS_CODE
);
// эта строка
программы

// заставляет систему вызвать функцию
ExitThread

}

Параметры ERROR_CODE
и SUCCESS_CODE определяются по вашему усмотре-

нию. В нашем простом примере поток
нетрудно прервать с помощью команды
return:

if( <условие возникновения ошибки> )

{

return( ERROR_CODE ); // прекратить работу
потока

}

В данном случае,
команда return приводит к тому же результату,
что и функция Ex-

itThread, так как при ее выполнении осуществляется неявный вызов последней. Эта

функция особенно полезна при необходимости
прервать поток из любой под програм-

мы, вызываемой внутри функции типа
ThreadFunction.

Когда поток
завершается с помощью оператора return,
32-разрядный код заверше-

ния
автоматически передается функции
ExitThread. После прекращения работы потока

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

// один поток вызывает эту функцию для
получения кода завершения другого
потока

BOOL GetExitCodeThread( HANDLE hThread, LPDWORD lpdwExitCode );

Функция
GetExitCodeThread возвращает значение FALSE в том
случае, если опре-

делить код завершения помешала ошибка.

69

Независимо от того, как — явно или неявно
(в результате выполнения оператора re-

turn) — вызывается функция
ExitThread, она удаляет поток из очереди программы-

планировщика и
уничтожает его стек. Однако сам объект
при этом сохраняется. Поэто-

му даже после
прекращения выполнения потока вы можете
запросить его код заверше-

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

CloseHandle), с тем чтобы поток не
занимал лишний объем памяти. При
закрытии по-

следнего дескриптора система автоматически уничтожает поток. Система не может

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

случае поток
будет уничтожен сразу же после завершения
выполнения. Если по завер-

шении
процесса остаются незакрытые дескрипторы,
система закрывает их автоматиче-

ски и удаляет все
«подвешенные» объекты, которые не
принадлежат ни одному процес-

су.

С помощью команды
ExitThread поток может остановить
себя самостоятельно в

том месте программы,
где это необходимо. Кроме того, один
поток способен по своему

усмотрению мгновенно остановить другой
поток.

// с помощью вызова этой функции один
поток может остановить другой

BOOL TerminateThread ( HANDLE hThread, DWORD
dwExitCode );

Поток не в состоянии
защитить себя от прерывания. Имея
соответствующий деск-

риптор, любой
объект может мгновенно остановить поток
вне зависимости от его теку-

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

току). Если при вызове функции
CreateThread использовать набор атрибутов

безопасности,
заданный по умолчанию, то результирующий
дескриптор обеспечит пол-

ные привилегии доступа к созданному
потоку.

Функция TerminateThread не уничтожает
стек потока, а только возвращает код
его

завершения. Функции
ExitThread и TerminateThread переводят объект в сигнальное со-

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

завершения. После выполнения любой из двух указанных функций поток продолжает

существовать
в сигнальном состоянии до тех пор, пока
не будут закрыты все его деск-

рипторы.

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

При работе с
потоками необходимо иметь возможность
координировать их дейст-

вия. Часто координация действий подразумевает определенный порядок выполнения

операций. Кроме
функций, предназначенных для создания
потоков и изменения их пла-

нового приоритета, Win32 API содержит
функции, которые переводят потоки в
режим

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

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

частности семафоров и исключающих
семафоров.

Лучше всего проиллюстрировать применение
синхронизирующих объектов можно

на примере функций,
ожидающих сигнала от объекта. С помощью
одного набора обоб-

щенных команд можно организовать ожидание сигналов от процессов,
семафоров, ис-

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

ожидает поступления сигнала от указанного
объекта:

DWORD WaitForSingleObject( HANDLE hObject,

DWORD dwMilliseconds );

70

// объект, сигнал от

// которого ожидается

// максимальное время ожидания

Функция WaitForSingleObject позволяет приостановить
выполнение потока до тех

пор, пока не поступит
сигнал от заданного объекта. Кроме того,
в этой команде указыва-

ется максимальное
время ожидания. Чтобы обеспечить
бесконечное ожидание, в качест-

ве временного интервала следует задать
значение INFINITE. Если объект уже доступен

или если он
подает сигнал в течение заданного времени,
функция WaitForSingleObject

возвращает значение
0 и выполнение потока возобновляется.
Но если заданный интер-

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

WAIT_TIMEOUT.

Для того чтобы заставить поток ожидать сигналы
сразу от нескольких объектов,

воспользуйтесь функцией
WaitForMultipleObjects. Функция возвратит управление
пото-

ку при поступлении
сигнала либо от одного из указанных
объектов, либо от всех объек-

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

DWORD WaitForMultipleObjects(

DWORD dwNumObjects,

LPHANDLE lpHandles,

BOOL bWaitAll,

DWORD dwMilliseconds );

// количество ожидаемых объектов

// массив дескрипторов

// TRUE — ожидание сигналов

// сразу от всех объектов;

// FALSE — ожидание сигнала от

// любого из объектов

// максимальный период ожидания

Результирующее значение WAIT_TIMEOUT,
опять-таки, говорит о том, что задан-

ный интервал
времени прошел, а сигнал от объектов
не поступил. Если флаг bWaitAll

имеет значение FALSE, соответствующее
ожиданию сигнала от любого из указанных

объектов,
в случае успешного завершения функция
WaitForMultipleObjects возвращает

код, который указывает, от какого из элементов массива
lpHandles поступил сигнал.

(Первый элемент
массива соответствует значению 0, второй
— значению 1 и т.д.). Если

флаг bWaitАll имеет значение TRUE, функция
не возвращает результат до тех пор,
пока

не
будут установлены флаги всех объектов
(т.е. пока не завершите выполнение всех
по-

токов).

Две расширенные
версии функций ожидания содержат
дополнительный флаг ста-

туса оповещения, который позволяет возобновить выполнение потока, если в течение

периода ожидания были завершены асинхронные
операции чтения или записи. Работу

этих функций
можно представить так, как будто они
просят «разбудить» их в одном из

трех случаев: если
становится доступным указанный объект,
если заканчивается задан-

ный период времени или если завершилось выполнение фоновой операции вво-

да/вывода.

DWORD WaitForSingleObjectEx (

HANDLE hObject,

DWORD dwMilliseconds,

BOOL bAlertable );

DWORD WaitForMultipleObjectsEx(

DWORD dwNumObjects,

LPHANDLE lpHandles,

BOOL bWaitAll,

// объект, сигнал от которого ожидается

// максимальное время ожидания

// TRUE — прекращение ожидания

// при завершении операции ввода/вывода

// количество ожидаемых объектов

// массив дескрипторов

// TRUE — ожидание сигналов

// сразу от всех объектов;

// FALSE — ожидание сигнала от

71

DWORD dwMilliseconds,

BOOL bAlertable );

// любого из объектов

// максимальный период ожидания

// TRUE — прекращение ожидания

// при завершении операции ввода/вывода

При успешном выполнении функции ожидания
объект, сигнал от которого ожи-

дался, обычно
определенным образом изменяется.
Например, если поток ожидал и по-

лучил сигнал от исключающего
семафора, функция восстанавливает несигнальное со-

стояние исключающего
семафора, чтобы остальные потоки знали о том, что он
занят.

Кроме того, функции
ожидания декрементируют значение
счетчика семафора и сбрасы-

вают информацию о некоторых событиях.

Функции ожидания
не изменяют состояния указанного
объекта до тех пор, пока не

поступит сигнал
от одного или нескольких других объектов.
В частности, поток не за-

хватывает
исключающий семафор сразу же после
поступления сигнала от него, а ожида-

ет сигналов от других объектов. Кроме
того, в течение времени ожидания
исключающий

семафор снова
может быть захвачен другим потоком,
который еще больше продлит со-

стояние ожидания.

Конечно, ожидать
поступления сигнала от объекта можно
лишь в том случае, если

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

скольку для работы
с ними существуют параллельные
API-команды, позволяющие соз-

давать и уничтожать
эти объекты, захватывать их и освобождать, а
также получать их

дескрипторы.

Функциям,
создающим исключающие
семафоры
и семафоры,
нужно указать тре-

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

указать его имя, но это необязательно)
[12].

HANDLE CreateMutex (

LPSECURITY_ATTRIBUTES lpsa,

сти

// необязательные атрибуты безопасно-

BOOL bInitialOwner

LPTSTR lpszMutexName )

HANDLE CreateSemaphore(

// TRUE — создатель хочет

// завладеть полученным объектом

// имя объекта

LPSECURITY_ATTRIBUTES lpsa,

//необязательные атрибуты безопасности

LONG lInitialCount,

LONG lMaxCount,

LPTSTR lpszSemName );

// исходное значение счетчика (обычно
0)

// максимальное значение

// счетчика (ограничивает число потоков)

// имя семафора (может иметь значение
NULL)

Если в качестве атрибута безопасности задано
значение NULL, результирующий

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

цессами. Имена
объектов являются необязательными,
однако они становятся полезными

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

Если флагу
bInitialOwner присвоить значение TRUE,
поток сразу после создания

объекта завладеет
им. Созданный исключающий семафор не
станет подавать сигналы до

тех пор, пока поток не освободит его.

В отличие от исключающего
семафора, который может принадлежать
только од-

ному потоку,
неисключающий семафор остается в
сигнальном состоянии до тех пор, по-

ка его счетчик
захватов не получит значения iMaxCount.
Если другие потоки в этот мо-

72

мент попытаются завладеть семафором, они будут приостановлены до тех пор,
пока

счетчик захватов не будет декрементирован
до значения ниже максимального.

Пока семафор (или исключающий семафор)
существует, поток взаимодействует с

ним посредством
операций захвата и освобождения. Для
захвата любого объекта поток

вызывает функцию
WaitForSingleObject (или одну из ее разновидностей).
Завершив вы-

полнение задачи, которая синхронизировалась захваченным объектом,
поток освобож-

дает этот объект с помощью одной из
следующих функций:

BOOL ReleaseMutex( HANDLE hMutex ) ;

BOOL ReleaseSemaphore(

HANDLE hSemaphore,

LONG lRelease,

LPLONG lplPrevious );

// величина, на которую

// инкрементируется значение счетчика

// при освобождении объекта (обычно 1)

// переменная, которой присваивается

// предыдущее значение счетчика

При освобождении семафора: или
исключающего семафора значение счетчика
за-

хватов инкрементируется.
Значение счетчика, превышающее
0, воспринимается систе-

мой как сигнал объекта ожидающим его
потокам.

Освободить
исключающий семафор может только тот
поток, который завладел им.

Однако любой поток
может вызвать функцию ReleaseSemaphore, которая
инкрементиру-

ет значение
счетчика захватов обычного семафора
вплоть до его максимального значе-

ния.
Изменение значения счетчика дает
возможность в процессе выполнения
программы

произвольным
образом задать количество потоков,
которые могут завладеть семафором.

Обратите внимание,
что функция CreateSemaphore позволяет при
создании нового сема-

фора
присвоить его счетчику значение, меньшее
максимального. Например, при разра-

ботке
нового семафора его счетчику можно задать начальное
значение 0. Такой прием

позволит
заблокировать все потоки до тех пор,
пока программа не произведет инициали-

зацию, а затем не увеличит значение
счетчика с помощью команды ReleaseSemaphore.

Не забывайте
вовремя освобождать синхронизирующие объекты.
Не задав макси-

мального времени ожидания и забыв освободить исключающий семафор,
вы заблоки-

руете все ожидающие его потоки.

Поток может ожидать несколько сигналов
от одного и того же объекта, не будучи

заблокированным,
однако после завершения каждого из
процессов ожидания необходи-

мо, выполнять операцию освобождения. Это требование справедливо для семафоров,

исключающих семафоров и критических
разделов.

Событие
представляет собой объект, который
создается программой при необхо-

димости информировать потоки о выполнении
определенных действий. В простейшем

случае (ручной
сброс) событие переключает свое состояние
с помощью команд SetEvent

(сигнал включен) и
ResetEvent (сигнал выключен). Когда сигнал
включается, его полу-

чают все потоки,
которые ожидают появления соответствующего
события. Если сигнал

выключается, все
такие потоки блокируются. В отличие от
семафоров и исключающих

семафоров, события
данного типа изменяют свое состояние только
при подаче соответ-

ствующей команды каким-нибудь потоком.

События целесообразно использовать
при условии, что поток должен выполняться

только после того,
как программа обновит свое окно или пользователь введет опреде-

ленную информацию [12]. Ниже представлены
основные функции, предназначенные для

работы с событиями:

73

HANDLE CreateEvent(

LPSECURITY_ATTRIBUTES lpsa,

BOOL bManualReset,

BOOL bInitialState,

LPTSTR lpszEventName );

BOOL SetEvent ( HANDLE hEvent );

BOOL ResetEvent( HANDLE hEvent );

// привилегии доступа

// (по умолчанию = NULL)

// TRUE — событие должно быть

// сброшено вручную

// TRUE — создание события в

// сигнальном состоянии

//имя события (допускается

// значение NULL)

При установке параметра bInitialState функция
CreateEvent создает событие, кото-

рое сразу же будет находиться в сигнальное
состоянии. Функции SetEvent и ResetEvent в

случае успешного
завершения возвращают значение TRUE, при
возникновении ошибки

— значение FALSE.

Параметр
bManualReset функции
CreateEvent позволяет создать событие, сбрасы-

ваемое не вручную, а автоматически.
Автоматически сбрасываемое событие
переходит в

несигнальное
состояние сразу же после выполнения
функции SetEvent. Для таких собы-

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

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

сываемые события целесообразно применять
в таких программах, где один основной

поток подготавливает данные для других, вспомогательных потоков. При готовности

нового набора
данных основной поток устанавливает
событие, по которому освобожда-

ется один вспомогательный поток. Остальные вспомогательные потоки продолжают

ожидать подготовки новых данных.

Наряду с выполнением операций
установки и сброса события можно сгенериро-

вать импульсное событие:

BOOL PulseEvent( hEvent ) ;

Импульсное событие включает
сигнал на короткий промежуток времени.
Приме-

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

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

ваемого автоматически, дает возможность оповестить только один ожидающий поток.

Если не было ни
одного ожидающего потока, то никакой другой
поток не будет опове-

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

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

тока событие сбрасывается автоматически.

Семафоры, исключающие семафоры и события могут совместно использоваться

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

Путем совместного
задействования синхронизирующих
объектов процессы могут коор-

динировать свои действия по аналогии
с тем, как это делают потоки. Существует
три

механизма
совместного использования. Первый из
них — это наследование, при котором

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

ского процесса.
Копируются только те дескрипторы,
которые при создании были поме-

чены как доступные для наследования.

Два других метода
сводятся к созданию второго дескриптора
существующего объ-

екта с помощью
вызова функций. Какая из функций будет вызвана, зависит
от имею-

щейся информации.
При наличии дескрипторов как исходного
процесса, так и процесса,

назначения следует
вызывать функцию DuplicateHandle, при наличии
только имени объ-

74

екта — одну из функций Openxxx. Две программы
могут заранее определить имя совме-

стно используемого объекта.
Кроме того, одна из программ способна передать
другой

это
имя посредством совместно используемой
области памяти функций DDEML (DDE

Management Library —
библиотека управления динамическим
обменом данных) или кана-

ла.

BOOL DuplicateHandle(

HANDLE hSourceProcess,

HANDLE hSource,

HANDLE hTargetProcess,

LPHANDLE lphTarget,

DWORD fdwAccess,

BOOL bInherit,

DWORD fdwOptions );

HANDLE OpenMutex(

DWORD fdwAccess,

BOOL binherit,

LPTSTR lpszName );

HANDLE OpenSemaphore(

DWORD fdwAccess,

BOOL bInherit,

LPTSTR lpszName );

HANDLE OpenEvent(

DWORD fdwAccess,

BOOL bInherit,

LPTSTR lpszName );

// процесс, которому
принадлежит

// исходный объект

// дескриптор исходного объекта

// процесс, который хочет создать

// копию дескриптора

// переменная для записи копии дескриптора

// запрашиваемые привилегии доступа

// может ли наследоваться копия
дескриптора?

// дополнительные операции, например

// закрытие исходного дескриптора

//запрашиваемые привилегии доступа

// TRUE — дочерний процесс может

// наследовать этот дескриптор

// имя исключающего семафора

// запрашиваемые привилегии доступа

//TRUE — дочерний процесс может

// наследовать этот дескриптор

// имя семафора

// запрашиваемые привилегии доступа

// TRUE — дочерний процесс может

// наследовать этот дескриптор

// имя события

Используемый в этом примере тип данных
LPTSTR — это обобщенный текстовый

тип, который компилируется по-разному
в зависимости от того, какой стандарт,
Unicode

или ASCII, поддерживается приложением.

Семафоры, исключающие
семафоры и объекты событий будут
сохраняться в памя-

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

функции CloseHandle не будут закрыты все
дескрипторы соответствующего объекта:

BOOLCloseHandle(hObject);

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

что и исключающий
семафор, но в отличие от последнего
критический раздел не может

наследоваться.
Оба объекта доступны только для
одного процесса. Преимущество кри-

тических разделов перед исключающими
семафорами состоит в том, что они проще
в

управлении и гораздо быстрее работают
[12].

Терминология,
принятая для функций, которые используются
при работе с крити-

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

ния
семафорами, исключающими семафорами и
событиями, однако сами функции вы-

полняют одни и те же операции. В частности, принято говорить не о создании

75

критического раздела, а о
его инициализации.
Процесс не ожидает критический раздел,
а

входит в него, и
не освобождает критический раздел, а
покидает его; к тому же вы не

закрываете дескриптор, а удаляете
объект.

VOID InitializeCriticalSection ( LPCRITICAL_SECTION lpcs );

VOID EnterCriticalSection( LPCRITICAL_SECTION lpcs );

VOID LeaveCriticalSection( LPCRITICAL_SECTION lpcs );

VOID DeleteCriticalSection( LPCRlTICAL_SECTION lpcs );

Переменная типа
LPCRITICAL_SECTION содержит указатель
(а не дескриптор)

критического раздела. Функция
InitializeCriticalSection должна получить указатель на

пустой объект (&cs), который можно
создать следующим образом:

CRITICAL SECTION cs;

2.2.4.
Использование
классов MFC
для
создания
потоков

Способ создания потоков с помощью
функций библиотеки MFC, заключается в

создании
класса, порожденного от класса CWinThread.
Схема этого процесса выглядит

следующим образом:

// Класс CThreadExample

IMPLEMENT_DYNCREATE(CThreadExample, CWinThread)

CThreadExample::CThreadExample()

{

}

// инициализация переменных-членов
класса

CThreadExample::~CThreadExample()

{

}

BOOL CThreadExample::InitInstance()

{

// TODO: здесь следует выполнить инициализацию
потока

//здесь должны выполняться операции
инициализации,

//не связанные с переменными, например
создание

// экземпляров других объектов класса

return TRUE;

}

int CThreadExample::ExitInstance()

{

// TODO: здесь выполняются все операции
очистки для потока

return CWinThread::ExitInstance();

}

BEGIN_MESSAGE_MAP(CThreadExample, CWinThread)

//{{AFX_MSG_MAP(CThreadExample)

76

// ПРИМЕЧАНИЕ — Мастер ClassWizard будет
добавлять/удалять в

// этом месте макросы обработки сообщений

//}}AFX_MSG_MAP

END_MESSAGE_MAP ()

Объект класса CWinThread представляет поток
выполнения в рамках приложения.

Хотя основной поток
выполнения приложения обычно задается
объектом класса, поро-

жденного от
CWinApp, сам класс CWinApp является производным от класса

CWinThread.

Для обеспечения
безопасности потоков в MFC-приложениях
должны применяться

классы, являющиеся
производными от класса CWinThread, библиотека
MFC использует

переменные-члены
этого класса. Потоки, созданные с помощью
функции_beqinthreadex,

не могут использовать ни одной из
API-функций библиотеки MFC.

Поддерживаются два основных типов потоков, а именно рабочие и интерфейс-

ные.
Для рабочих потоков не нужно создавать
цикл обработки сообщений. Такие потоки

могут выполнять фоновые вычисления в электронной таблице без взаимодействия с

пользователем и не должны реагировать
на сообщения.

Интерфейсные потоки,
в отличие от рабочих, обрабатывают
сообщения, получен-

ные от системы
(или от пользователя). Для них необходима
специальная процедура об-

работки сообщений.
Создаются интерфейсные потоки на базе
класса CWinApp или не-

посредственно класса CWinThread.

Объект класса
CWinThread обычно существует в течение всего
времени существо-

вания потока,
однако такой способ функционирования
можно изменить, присвоив пере-

менной-члену m_bAutoDelete значение
FALSE.

Потоки создаются
с помощью функции AfxBeqinThread. Для создания
интерфейс-

ного потока функции AfxBeqinThread следует
передать указатель на класс CRuntimeC-

lass объекта, производного от класса
CWinThread. В случае рабочих потоков функция

AfxBeqinThread вызывается с указанием управляющей функции и параметра, переда-

ваемого последней.

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

параметры,
изменяющие приоритет, размер стека, флаги
создания и атрибуты безопас-

ности потока. Функция
AfxBeqinThread возвращает указатель на новый объект класса

CWinThread.

В качестве альтернативного
варианта можно определить, а затем создать объект,

производный от класса
CWinThread, вызвав функцию
CreateThread данного класса. В

этом случае производный объект может многократно использоваться при последова-

тельных созданиях и уничтожениях потока.

77

Ниже представлена не простая расшифровка доклада с семинара CLRium, а переработанная версия для книги .NET Platform Architecture. Той её части, что относится к потокам.

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

Что такое поток? Давайте дадим краткое определение. По своей сути поток это:

  • Средство параллельного относительно других потоков исполнения кода;
  • Имеющего общий доступ ко всем ресурсам процесса.

Очень часто часто слышишь такое мнение, что потоки в .NET — они какие-то абсолютно свои. И наши .NET потоки являются чем-то более облегчённым чем есть в Windows. Но на самом деле потоки в .NET являются самыми обычными потоками Windows (хоть Windows thread id и скрыто так, что сложно достать). И если Вас удивляет, почему я буду рассказывать не-.NET вещи в хабе .NET, скажу вам так: если нет понимания этого уровня, можно забыть о хорошем понимании того, как и почему именно так работает код. Почему мы должны ставить volatile, использовать Interlocked и SpinWait. Дальше обычного lock дело не уйдёт. И очень даже зря.

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

Задача процессора — просто исполнять код. Поэтому с точки зрения процессора есть только один поток: последовательное исполнение команд. А задача операционной системы каким-либо образом менять поток т.о. чтобы эмулировать несколько потоков.

Поток в физическом понимании

«Но как же так?», — скажите вы, — «во многих магазинах и на различных сайтах я вижу запись «Intel Xeon 8 ядер 16 потоков». Говоря по-правде это — либо скудность в терминологии либо — чисто маркетинговый ход. На самом деле внутри одного большого процессора есть в данном случае 8 ядер и каждое ядро состоит из двух логических процессоров. Такое доступно при наличии в процессоре технологии Hyper-Threading, когда каждое ядро эмулирует поведение двух процессоров (но не потоков). Делается это для повышения производительности, да. Но по большому счёту если нет понимания, на каких потоках идут расчёты, можно получить очень не приятный сценарий, когда код выполняется со скоростью, ниже чем если бы расчёты шли на одном ядре. Именно поэтому раздача ядер идёт +=2 в случае Hyper-Threading. Т.е. пропуская парные ядра.

Технология эта — достаточно спорная: если вы работаете на двух таких псевдо-ядрах (логических процессорах, которые эмулируются технологией Hyper-Threading), которые при этом находятся на одном физическом ядре и работают с одной и той-же памятью, то вы будете постоянно попадать в ситуацию, когда второй логический процессор так же пытается обратиться к данной памяти, создавая блокировку либо попадая в блокировку, т.к. поток, находящийся на первом ядре работает с той же памятью.

Возникает блокировка совместного доступа: хоть и идёт эмуляция двух ядер, на самом-то деле оно одно. Поэтому в наихудшем сценарии эти потоки исполняются по очереди, а не параллельно.

Так если процессор ничего не знает о потоках, как же достигается параллельное исполнение потоков на каждом из его ядер? Как было сказано, поток — средство операционной системы выполнять на одном процессоре несколько задач одновременно. Достигается параллелизм очень быстрым переключением между потоками в течение очень короткого промежутка времени. Последовательно запуская на выполнение код каждого из потоков и делая это достаточно часто, операционная система достигает цели: делает их исполнение псевдопараллельным, но параллельным с точки зрения восприятия человека. Второе обоснование существования потоков — это утверждение, что программа не так часто срывается в математические расчёты. Чаще всего она взаимодействует с окружающим её миром: различным оборудованием. Это и работа с жёстким диском и вывод на экран и работа с клавиатурой и мышью. Поэтому чтобы процессор не простаивал, пока оборудование сделает то, чего хочет от него программа, поток можно на это время установить в состояние блокировки: ожидания сигнала от операционной системы, что оборудование сделало то, что от него просили. Простейший пример этого — вызов метода Console.ReadKey().

Если заглянуть в диспетчер задач Windows 10, то можно заметить, что в данный момент в вашей системе существует около 1,5 тысячи потоков. И если учесть, что квант на десктопе равен 20 мс, а ядер, например, 4, то можно сделать вывод, что каждый поток получает 20 мс работы 1 раз в 7,5 сек… Ну конечно же, нет. Просто почти все потоки чего-то ждут. То ввода пользователя, то изменения ключей реестра… В операционной системе существует очень много причин, чтобы что-либо ждать.

Так что пока одни потоки в блокировке, другие — что-то делают.

Создание потоков

Простейшая функция создания потоков в пользовательском режиме операционной системы — CreateThread. Эта функция создаёт поток в текущем процессе. Вариантов параметризации CreateThread очень много и когда мы вызываем new Thread(), то из нашего .NET кода вызывается данная функция операционной системы.

В эту функцию передаются следующие атрибуты:

1) Необязательная структура с атрибутами безопасности:

  • Дескриптор безопасности (SECURITY_ATTRIBUTES) + признак наследуемости дескриптора.

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

2) Необязательный размер стека:

  • Начальный размер стека, в байтах (система округляет это значение до размера страницы памяти)

    Т.к. за нас размер стека передаёт .NET, нам это делать не нужно. Это необходимо для вызовов методов и поддержки памяти.

3) Указатель на функцию — точка входа нового потоками
4) Необязательный аргумент для передачи данных функции потока.

Из того, что мы не имеем в .NET явно — это структура безопасности с атрибутами безопасности и размер стэка. Размер стэка нас мало интересует, но атрибуты безопасности нас могут заинтересовать, т.к. сталкиваемся мы с ними впервые. Сейчас мы рассмотривать их не будем. Скажу только, что они влияют на возможность изменения информации о потоке средствами операционной системы.

Если мы создаём любым способом: из .NET или же вручную, средствами ОС, мы как итог имеем и ManageThreadId и экземпляр класса Thread.

Также у этой функции есть необязательный флаг: CREATE_SUSPENDED — поток после создания не стартует. Для .NET это поведение по умолчанию.

Помимо всего прочего существует дополнительный метод CreateRemoteThread, который создаёт поток в чужом процессе. Он часто используется для мониторинга состояния чужого процесса (например программа Snoop). Этот метод создаёт в другом процессе поток и там наш поток начинает исполнение. Приложения .NET так же могут заливать свои потоки в чужие процессы, однако тут могут возникнуть проблемы. Первая и самая главная — это отсутствие в целевом потоке .NET runtime. Это значит, что ни одного метод фреймворка там не будет: только WinAPI и то, что вы написали сами. Однако, если там .NET есть, то возникает вторая проблема (которой не было раньше). Это — версия runtime. Необходимо: понять, что там запущено (для этого необходимо импортировать не-.NET методы runtime, которые написаны на C/C++ и разобраться, с чем мы имеем дело). На основании полученной информации подгрузить необходимые версии наших .NET библиотек и каким-то образом передать им управление.

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

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

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

Итак, начнём. Организация планирования в операционной системе Windows является: гибридной. С одной стороны моделируются условия вытесняющей многозадачности, когда операционная система сама решает, когда и на основе каких условия вытеснить потоки. С другой стороны — кооперативной многозадачности, когда потоки сами решают, когда они всё сделали и можно переключаться на следующий (UMS планировщик). Режим вытесняющей многозадачности является приоритетным, т.к. решает, что будет исполняться на основе приоритетов. Почему так? Потому что у каждого потока есть свой приоритет и операционная система планирует к исполнению более приоритетные потоки. А вытесняющей потому, что если возникает более приоритетный поток, он вытесняет тот, который сейчас исполнялся. Однако во многих случаях это бы означало, что часть потоков никогда не доберется до исполнения. Поэтому в операционной системе есть много механик, позволяющих потокам, которым необходимо время на исполнение его получить несмотря на свой более низкий по сравнению с остальными, приоритет.

Уровни приоритета

Windows имеет 32 уровня приоритета (0-31)

  • 1 уровень (00 — 00) — это Zero Page Thread;
  • 15 уровней (01 — 15) — обычные динамические приоритеты;
  • 16 уровней (16 — 31) — реального времени.

Самый низкий приоритет имеет Zero Page Thread. Это — специальный поток операционной системы, который обнуляет страницы оперативной памяти, вычищая тем самым данные, которые там находились, но более не нужны, т.к. страница была освобождена. Необходимо это по одной простой причине: когда приложение освобождает память, оно может ненароком отдать кому-то чувствительные данные. Личные данные, пароли, что-то ещё. Поэтому как операционная система так и runtime языков программирования (а у нас — .NET CLR) обнуляют получаемые участки памяти. Если операционная система понимает, что заняться особо нечем: потоки либо стоят в блокировке в ожидании чего-либо либо нет потоков, которые исполняются, то она запускает самый низко приоритетный поток: поток обнуления памяти. Если она не доберется этим потоком до каких-либо участков, не страшно: их обнулят по требованию. Когда их запросят. Но если есть время, почему бы это не сделать заранее?

Продолжая говорить о том, что к нам не относится, стоит отметить приоритеты реального времени, которые когда-то давным-давно таковыми являлись, но быстро потеряли свой статус приоритетов реального времени и от этого статуса осталось лишь название. Другими словами, Real Time приоритеты на самом деле не являются таковыми. Они являются приоритетами с исключительно высоким значением приоритета. Т.е. если операционная система будет по какой-то причине повышать приоритет потока с приоритетом из динамической группы (об этом — позже, но, например, потому, что потоку освободили блокировку) и при этом значение до повышения было равно 15, то повысить приоритет операционная система не сможет: следующее значение равно 16, а оно — из диапазона реального времени. Туда повышать такими вот «твиками» нельзя.

Уровень приоритетов процессов с позиции Windows API.

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

Однако, изменение уровня приоритета процесса не меняет относительных приоритетов внутри приложения: их значения сдвигаются, но не меняется внутренняя модель приоритетов: внутри по-прежнему будет поток с пониженным приоритетом и поток — с обычным. Так, как этого хотел разработчик приложения. Как же это работает?

Существует 6 классов приоритетов процессов. Класс приоритетов процессов — это то, относительно чего будут создаваться приоритеты потоков. Все эти классы приоритетов можно увидеть в «Диспетчере задач», при изменении приоритета какого-либо процесса.

Другими словами класс приоритета — это то, относительно чего будут задаваться приоритеты потоков внутри приложения. Чтобы задать точку отсчёта, было введено понятие базового приоритета. Базовый приоритет — это то значение, чем будет являться приоритет потока с типом приоритета Normal:

  • Если процесс создаётся с классом Normal и внутри этого процесса создаётся поток с приоритетом Normal, то его реальный приоритет Normal будет равен 8 (строка №4 в таблице);
  • Если Вы создаёте процесс и у него класс приоритета Above Normal, то базовый приоритет будет равен 10. Это значит, что потоки внутри этого процесса будут создаваться с более повышенным приоритетом: Normal будет равен 10.

Для чего это необходимо? Вы как программисты знаете модель многопоточности, которая у вас присутствует.
Потоков может быть много и вы решаете, что один поток должен быть фоновым, так как он производит вычисления и вам
не столь важно, когда данные станут доступны: важно чтобы поток завершил вычисления (например поток обхода и анализа дерева). Поэтому, вы устанавливаете пониженный приоритет данного потока. Аналогично может сложится ситуация когда необходимо запустить поток с повышенным приоритетом.

Представим, что ваше приложение запускает пользователь и он решает, что ваше приложение потребляет слишком много процессорных ресурсов. Пользователь считает, что ваше приложение не столь важное в системе, как какие-нибудь другие приложения и понижает приоритет вашего приложения до Below Normal. Это означает, что он задаёт базовый приоритет 6 относительно которого будут рассчитываться приоритеты потоков внутри вашего приложения. Но в системе общий приоритет упадёт. Как при этом меняются приоритеты потоков внутри приложения?

Таблица 3

Normal остаётся на уровне +0 относительно уровня базового приоритета процесса. Below normal — это (-1) относительно уровня базового. Т.е. в нашем примере с понижением уровня приоритета процесса до класса Below Normal приоритет потока ‘Below Normal’ пересчитается и будет не 8 - 1 = 7 (каким он был при классе Normal), а 6 - 1 = 5. Lowest (-2) станет равным 4.

Idle и Time Critical — это уровни насыщения (-15 и +15). Почему Normal — это 0 и относительно него всего два шага: -2, -1, +1 и +2? Легко провести параллель с обучением. Мы ходим в школу, получаем оценки наших знаний (5,4,3,2,1) и нам понятно, что это за оценки: 5 — молодец, 4 — хорошо, 3 — вообще не постарался, 2 — это не делал ни чего, а 1 — это то, что можно исправить потом на 4. Но если у нас вводится 10-ти бальная система оценок (или что вообще ужас — 100-бальная), то возникает неясность: что такое 9 баллов или 7? Как понять, что вам поставили 3 или 4?

Тоже самое и с приоритетами. У нас есть Normal. Дальше, относительно Normal у нас есть чуть повыше
Normal (Normal above), чуть пониже Normal (Normal below). Также есть шаг на два вверх
или на два вниз (Higest и Lowest). Нам, поверьте, нет никакой необходимости в более подробной градации. Единственное, очень редко, может раз в жизни, нам понадобится сказать: выше чем любой приоритет в системе. Тогда мы выставляем уровень Time Critical. Либо наоборот: это надо делать, когда во всей системе делать нечего. Тогда мы выставляем уровень Idle. Это значения — так называемые уровни насыщения.

Как рассчитываются уровни приоритета?

У нас бал класс приоритета процесса Normal (Таблица 3) и приоритет потоков Normal — это 8. Если процесс Above Normal то поток Normal получается равен 9. Если же процесс выставлен в Higest, то поток Normal получается равен 10.

Поскольку для планировщика потоков Windows все потоки процессов равнозначны, то:

  • Для процесса класса Normal и потока Above-Normal
  • Для процесса класса Higest и потока Normal
    конечные приоритеты будут одинаковыми и равны 10.

Если мы имеем два процесса: один с приоритетом Normal, а второй — с приоритетом Higest, но при этом
первый имел поток Higest а второй Normal, то система их приоритеты будет рассматривать как одинаковые.

Как уже обсуждалось, группа приоритетов Real-Time на самом деле не является таковой, поскольку настоящий Real-Time — это гарантированная доставка сообщения за определённое время либо обработка его получения. Т.е., другими словами, если на конкретном ядре есть такой поток, других там быть не должно. Однако это ведь не так: система может решить, что низко приоритетный поток давно не работал и дать ему время, отключив real-time. Вернее его назвать классом приоритетов который работает над обычными приоритетами и куда обычные приоритеты не могут уйти, попав под ситуации, когда Windows временно повышает им приоритет.

Но так как поток повышенным приоритетом исполняется только один на группе ядер, то получается,
что если у вас даже Real-Time потоки, не факт, что им будет выделено время.

Если перевести в графический вид, то можно заметить, что классы приоритетов пересекаются. Например, существует пересечение Above-Normal Normal Below-Normal (столбик с квадратиками):

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

Поэтому, когда процессу выдаётся более высокий класс приоритета, это повышает приоритет потоков процесса относительно обычных – с классом Normal.

Кстати говоря, мы стартовали продажи на CLRium #7, в котором мы с огромным удовольствием будем говорить про практику работы с многопоточным кодом. Будут и домашние задания и даже возможность работы с личным ментором.

Загляните к нам на сайт: мы сильно постарались, чтобы его было интересно изучить.

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

Многозадачность в Windows

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

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

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

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

Основные понятия

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

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

В завершение знакомства укажем некоторые термины, определяющие базовые понятия, которыми оперируют операционные системы.

Мультипроцессирование

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

Разные подходы к реализации мультипроцессирования

Рис.
6.1.
Разные подходы к реализации мультипроцессирования

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

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

  • наиболее массовыми являются так называемые SMP (Shared Memory Processor или Symmetric MultiProcessor) машины. В таких компьютерах несколько процессоров подключены к общей оперативной памяти и имеют к ней равноправный и конкурентный доступ (см. рис. 6.2). По мере увеличения числа процессоров производительность оперативной памяти и коммутаторов, связывающих процессоры с памятью, становится критически важной. Обычно в SMP используются 2-8 процессоров; реже число процессоров достигает десятков. Взаимодействие одновременно выполняющихся процессов осуществляется посредством использования общей памяти, к которой имеют равноправный доступ все процессоры.

    SMP компьютер

    Рис.
    6.2.
    SMP компьютер

  • при необходимости создания систем с качественно большим числом процессоров прибегают к MPP (Massively Parallel Processors) системам. Для этого используют несколько однопроцессорных или SMP-систем, объединяемых с помощью некоторого коммуникационного оборудования в единую сеть (см. рис. 6.3). При этом может применяться как специализированная высокопроизводительная среда передачи данных, так и обычные сетевые средства — типа Ethernet. В MPP системах оперативная память каждого узла обычно изолирована от других узлов, и для обмена данными требуется специально организованная пересылка данных по сети. Для MPP систем критической становится среда передачи данных; однако в случае мало связанных между собой процессов возможно одновременное использование большого числа процессоров. Число процессоров в MPP системах может измеряться сотнями и тысячами.

    MPP система

    Рис.
    6.3.
    MPP система

  • иногда используют так называемые NUMA и cc-NUMA архитектуры; они являются компромиссом между SMP и MPP системами: оперативная память является общей и разделяемой между всеми процессорами, но при этом память неоднородна по времени доступа. Каждый процессорный узел имеет некоторый объем оперативной памяти, доступ к которой осуществляется максимально быстро; для доступа к памяти другого узла потребуется значительно больше времени (см. рис. 6.4). cc-NUMA отличается от NUMA тем, что в ней на аппаратном уровне решены вопросы когерентности кэш-памяти (cache-coherent) различных процессоров. Формально на NUMA системах могут работать обычные операционные системы, созданные для SMP систем, хотя для обеспечения высокой производительности приходится решать нетипичные для SMP задачи оптимального размещения данных и планирования с учетом неоднородности памяти.

    NUMA или cc-NUMA система

    Рис.
    6.4.
    NUMA или cc-NUMA система

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

Windows содержит встроенные механизмы, необходимые для работы на SMP; также возможна установка этой ОС на cc-NUMA системах (современные версии Windows имеют механизмы поддержки cc-NUMA систем). Специальных, встроенных в ОС средств для исполнения приложений на MPP системах в Windows не предусмотрено. Windows предполагает альтернативное применение MPP систем, построенных на обычных сетях, для реализации web- или файловых серверов с балансировкой нагрузки по узлам кластера.

Мультипрограммирование

Мультипрограммирование (то есть одновременное выполнение разного кода на одном или нескольких процессорах) возможно и без реального мультипроцессирования. Конечно, при наличии только одного процессора должен существовать некоторый механизм, обеспечивающий переключение процессора между разными выполняемыми потоками. Такой режим разделения процессорного времени позволяет одному процессору обслуживать несколько задач «как бы одновременно»: осуществляя быстрое переключение между разными задачами и выполняя в данный момент времени код только одной задачи, процессор создает иллюзию одновременного выполнения кода разных задач. Более того, даже на многопроцессорных системах при реальной возможности распараллеливания задач по разным процессорам, обычно используют механизм разделения времени на каждом из доступных процессоров. Формально мультипрограммирование предполагает именно разделение процессорного времени, поэтому иногда его противопоставляют мультипроцессированию: реализация многозадачности на одном процессоре в противовес использованию многих процессоров.

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

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

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

Планирование задач

Рис.
6.5.
Планирование задач

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

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

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

На приведенном графе состояний задачи (см. рис. 6.6) прямая линия от состояния «выполнение» к состоянию «готовность» нарисована пунктиром, чтобы выделить отличие невытесняющей многозадачности от вытесняющей. В случае невытесняющей многозадачности выполняющаяся задача может либо завершиться, либо перейти в состояние «ожидание».

Граф состояния задачи

Рис.
6.6.
Граф состояния задачи

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

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

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

Характерный пример невытесняющей многозадачности — 16-ти разрядные Windows (включая собственно 16-ти разрядные версии Windows, выполнение 16-ти разрядных приложений в Windows-95, 98, ME и выполнение 16-ти разрядных приложений в рамках одной Windows-машины в NT, 2000, XP и 2003). В таких приложениях операционная система не прерывает выполнение текущей задачи до вызова ею функций типа GetMessage или WaitMessage, во время которых Windows осуществляет при необходимости переключение на другую задачу.

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

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

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

Большинство современных операционных систем используют комбинированные планировщики, одновременно применяющие квантование с переменной продолжительностью кванта и абсолютные или относительные приоритеты (см. рис. 6.7).

Моменты перепланирования задач

Рис.
6.7.
Моменты перепланирования задач

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

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

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

Реализации Windows NT, 2000, XP, 2003+ предусматривают достаточно развитый и сложный планировщик, учитывающий множество факторов и корректирующий как назначение и длительность отводимых задаче квантов, так и приоритеты задач. При этом планировщик является настраиваемым, и его логика работы несколько отличается в зависимости от настроек системы (некоторые доступны через панель управления) и от назначения системы (работа планировщика различна у серверов и рабочих станций).

Важно отметить, что Windows является гибкой системой разделения времени с вытесняющей многозадачностью и не может рассматриваться в качестве системы реального времени. Даже те процессы, которые с точки зрения Windows относятся к классу процессов так называемого «реального времени», на самом деле требованиям, предъявляемым к системам реального времени, не удовлетворяют. Такие процессы получат приоритетное распределение процессорного времени и будут обрабатываться планировщиком с учетом их «особого статуса»; однако при этом нельзя гарантировать строгого выполнения временных ограничений. Более того, в силу используемых механизмов управления памятью, нельзя точно предсказать время, необходимое для выполнения той или иной операции. В любой момент времени при самом невинном обращении к какой-либо переменной или функции может потребоваться обработка ошибок доступа, подкачка выгруженных страниц, освобождение памяти и т.д. — то есть действия, время завершения которых предсказать крайне трудно. Фактически можно давать лишь вероятностные прогнозы по времени выполнения той или иной операции. До определенных рамок Windows можно применять в мягких системах реального времени — с достаточно свободными ограничениями, но даже незначительная вероятность превышения временных ограничений иногда просто недопустима.

Базовая терминология

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

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

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

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

В Windows для обозначения этих понятий использованы термины process (процесс), thread (поток) и fiber (волокно). Достаточно часто термин «thread» переводится на русский язык как «нить», а не «поток». Термин «fiber» также может переводиться либо как «нить», либо как «волокно». Поток соответствует потоку ядра и планируется ядром операционной системы, а волокно соответствует потоку пользователя и планируется в приложении пользователя.

В операционной системе для описания потоков используются объекты двух типов — так называемые дескрипторы и контекст. Дескрипторы содержат информацию, описывающую поток, но не его текущее состояние исполнения. Контекст потока содержит информацию, описывающую непосредственно состояние исполнения потока. Так, например, дескриптор должен содержать переменные окружения, права доступа, назначенные потоку, приоритет, величину кванта и так далее, тогда как контекст должен сохранять информацию о состоянии стека, регистров процессора и т.д. Дескрипторы содержат актуальную в каждый момент информацию, а контекст обновляется в тот момент, когда поток выходит из исполняемого состояния. Из контекста восстанавливается состояние потока при возобновлении исполнения. Пока поток выполняется, содержимое контекста не является актуальным и не соответствует его реальному состоянию.

  • Операционная система windows это какая система
  • Операционная система windows имеет текстовый интерфейс
  • Операционная система windows xp условно бесплатная
  • Операционная система windows это информатика
  • Операционная система windows сервер 2019