
- •WINDOWS
- •Джеффри Рихтер
- •ЧАCTЬ I МАТЕРИАЛЫ ДЛЯ ОБЯЗАТЕЛЬНОГО ЧТЕНИЯ
- •ГЛАВА 1. Обработка ошибок
- •Вы тоже можете это сделать
- •Программа-пример ErrorShow
- •ГЛАВА 2 Unicode
- •Наборы символов
- •Одно- и двухбайтовые наборы символов
- •Unicode: набор широких символов
- •Почему Unicode?
- •Windows 2000 и Unicode
- •Windows 98 и Unicode
- •Windows CE и Unicode
- •В чью пользу счет?
- •Unicode и СОМ
- •Как писать программу с использованием Unicode
- •Unicode и библиотека С
- •Типы данных, определенные в Windows для Unicode
- •Unicode- и ANSI-функции в Windows
- •Строковые функции Windows
- •Ресурсы
- •Текстовые файлы
- •Перекодировка строк из Unicode в ANSI и обратно
- •ГЛАВА 3 Объекты ядра
- •Что такое объект ядра
- •Учет пользователей объектов ядра
- •Защита
- •Таблица описателей объектов ядра
- •Создание объекта ядра
- •Закрытие объекта ядра
- •Совместное использование объектов ядра несколькими процессами
- •Наследование описателя объекта
- •Изменение флагов описателя
- •Именованные объекты
- •Пространства имен Terminal Server
- •Дублирование описателей объектов
- •ЧАСТЬ II НАЧИНАЕМ РАБОТАТЬ
- •ГЛАВА 4 Процессы
- •Ваше первое Windows-приложение
- •Описатель экземпляра процесса
- •Описатель предыдущего экземпляра процесса
- •Командная строка процесса
- •Переменные окружения
- •Привязка к процессорам
- •Режим обработки ошибок
- •Текущие диск и каталог для процесса
- •Текущие каталоги для процесса
- •Определение версии системы
- •Функция CreateProcess
- •Параметры pszApplicationName и pszCommandLine
- •Параметры psaProcess, psaThread и blnheritHandles
- •Параметр fdwCreate
- •Параметр pvEnvironment
- •Параметр pszCurDir
- •Параметр psiStartlnfo
- •Параметр ppiProclnfo
- •Завершение процесса
- •Возврат управления входной функцией первичного потока
- •Функция ExitProcess
- •Функция TerminateProcess
- •Когда все потоки процесса уходят
- •Что происходит при завершении процесса
- •Дочерние процессы
- •Запуск обособленных дочерних процессов
- •Перечисление процессов, выполняемых в системе
- •Программа-пример Processlnfo
- •ГЛАВА 5 Задания
- •Определение ограничений, налагаемых на процессы в задании
- •Включение процесса в задание
- •Завершение всех процессов в задании
- •Получение статистической информации о задании
- •Уведомления заданий
- •Программа-пример JobLab
- •ГЛАВА 6 Базовые сведения о потоках
- •В каких случаях потоки создаются
- •И в каких случаях потоки не создаются
- •Ваша первая функция потока
- •Функция CreateThread
- •Параметр psa
- •Параметр cbStack
- •Параметры pfnStartAddr и pvParam
- •Параметр fdwCreate
- •Параметр pdwThreadlD
- •Завершение потока
- •Возврат управления функцией потока
- •Функция ExitThread
- •Функция TerminateThread
- •Если завершается процесс
- •Что происходит при завершении потока
- •Кое-что о внутреннем устройстве потока
- •Некоторые соображения по библиотеке С/С++
- •Ой, вместо _beginthreadex я по ошибке вызвал CreateThread
- •Библиотечные функции, которые лучше не вызывать
- •Как узнать о себе
- •Преобразование псевдоописателя в настоящий описатель
- •ГЛАВА 7 Планирование потоков, приоритет и привязка к процессорам
- •Приостановка и возобновление потоков
- •Приостановка и возобновление процессов
- •Функция Sleep
- •Переключение потоков
- •Определение периодов выполнения потока
- •Структура CONTEXT
- •Приоритеты потоков
- •Абстрагирование приоритетов
- •Программирование приоритетов
- •Динамическое изменение уровня приоритета потока
- •Подстройка планировщика для активного процесса
- •Программа-пример Scheduling Lab
- •Привязка потоков к процессорам
- •ГЛАВА 8 Синхронизация потоков в пользовательском режиме
- •Кэш-линии
- •Более сложные методы синхронизации потоков
- •Худшее, что можно сделать
- •Критические секции
- •Критические секции: важное дополнение
- •Критические секции и спин-блокировка
- •Критические секции и обработка ошибок
- •Несколько полезных приемов
- •Не занимайте критические секции надолго
- •ГЛАВА 9 Синхронизация потоков с использованием объектов ядра
- •Wait-функции
- •Побочные эффекты успешного ожидания
- •События
- •Программа-пример Handshake
- •Ожидаемые таймеры
- •Ожидаемые таймеры и АРС-очередь
- •И еще кое-что о таймерах
- •Семафоры
- •Мьютексы
- •Отказ от объекта-мьютекса
- •Мьютексы и критические секции
- •Программа-пример Queue
- •Сводная таблица объектов, используемых для синхронизации потоков
- •Другие функции, применяемые в синхронизации потоков
- •Асинхронный ввод-вывод на устройствах
- •Функция WaitForlnputldle
- •Функция MsgWaitForMultipleObjects(Ex)
- •Функция WaitForDebugEvent
- •Функция SignalObjectAndWait
- •ГЛАВА 10 Полезные средства для синхронизации потоков
- •Реализация критической секции: объект-оптекс
- •Программа-пример Optex
- •Создание инверсных семафоров и типов данных, безопасных в многопоточной среде
- •Программа-пример lnterlockedType
- •Синхронизация в сценарии "один писатель/группа читателей"
- •Программа-пример SWMRG
- •Реализация функции WaitForMultipleExpressions
- •Программа-пример WaitForMultExp
- •ГЛАВА 11 Пулы потоков
- •Сценарий 1: асинхронный вызов функций
- •Сценарий 2: вызов функций через определенные интервалы времени
- •Программа-пример TimedMsgBox
- •Сценарий 3: вызов функций при освобождении отдельных объектов ядра
- •Сценарий 4; вызов функций по завершении запросов на асинхронный ввод-вывод
- •ГЛАВА 12 Волокна
- •Работа с волокнами
- •Программа-пример Counter
- •ЧАСТЬ III УПРАВЛЕНИЕ ПАМЯТЬЮ
- •Виртуальное адресное пространство процесса
- •Как адресное пространство разбивается на разделы
- •Увеличение раздела для кода и данных пользовательского режима до 3 Гб на процессорах x86 (только Windows 2000)
- •Закрытый раздел размером 64 Кб (только Windows 2000)
- •Раздел для общих MMF (только Windows 98)
- •Регионы в адресном пространстве
- •Передача региону физической памяти
- •Физическая память и страничный файл
- •Физическая память в страничном файле не хранится
- •Атрибуты защиты
- •Защита типа «копирование при записи»
- •Специальные флаги атрибутов защиты
- •Подводя итоги
- •Блоки внутри регионов
- •Особенности адресного пространства в Windows 98
- •Выравнивание данных
- •ГЛАВА 14 Исследование виртуальной памяти
- •Системная информация
- •Программа-пример Syslnfo
- •Статус виртуальной памяти
- •Программа-пример VMStat
- •Определение состояния адресного пространства
- •Функция VMQuery
- •Программа-пример VMMap
- •ГЛАВА 15 Использование виртуальной памяти в приложениях
- •Резервирование региона в адресном пространстве
- •Передача памяти зарезервированному региону
- •Резервирование региона с одновременной передачей физической памяти
- •В какой момент региону передают физическую память
- •Возврат физической памяти и освобождение региона
- •В какой момент физическую память возвращают системе
- •Программа-пример VMAIloc
- •Изменение атрибутов защиты
- •Сброс содержимого физической памяти
- •Программа-пример MemReset
- •Механизм Address Windowing Extensions (только Windows 2000)
- •Программа-пример AWE
- •ГЛАВА 16 Стек потока
- •Стек потока в Windows 98
- •Функция из библиотеки С/С++ для контроля стека
- •Программа-пример Summation
- •ГЛАВА 17 Проецируемые в память файлы
- •Проецирование в память EXE- и DLL-файлов
- •Статические данные не разделяются несколькими экземплярами EXE или DLL
- •Программа-пример Applnst
- •Файлы данных, проецируемые в память
- •Метод 1: один файл, один буфер
- •Метод 2: два файла, один буфер
- •Метод 3: один файл, два буфера
- •Метод 4: один файл и никаких буферов
- •Использование проецируемых в память файлов
- •Этап1: создание или открытие объекта ядра «файл»
- •Этап 2: создание объекта ядра «проекция файла»
- •Этап 3: проецирование файловых данных на адресное пространство процесса
- •Этап 4: отключение файла данных от адресного пространства процесса
- •Этапы 5 и 6: закрытие объектов «проекция файла» и «файл»
- •Программа-пример FileRev
- •Обработка больших файлов
- •Проецируемые файлы и когерентность
- •Базовый адрес файла, проецируемого в память
- •Особенности проецирования файлов на разных платформах
- •Совместный доступ процессов к данным через механизм проецирования
- •Файлы, проецируемые на физическую память из страничного файла
- •Программа-пример MMFShare
- •Частичная передача физической памяти проецируемым файлам
- •Программа-пример MMFSparse
- •ГЛАВА 18 Динамически распределяемая память
- •Стандартная куча процесса
- •Дополнительные кучи в процессе
- •Защита компонентов
- •Более эффективное управление памятью
- •Локальный доступ
- •Исключение издержек, связанных с синхронизацией потоков
- •Быстрое освобождение всей памяти в куче
- •Создание дополнительной кучи
- •Выделение блока памяти из кучи
- •Изменение размера блока
- •Определение размера блока
- •Освобождение блока
- •Уничтожение кучи
- •Использование куч в программах на С++
- •Другие функции управления кучами
- •ЧАСТЬ IV ДИНАМИЧЕСКИ ПОДКЛЮЧАЕМЫЕ БИБЛИОТЕКИ
- •ГЛАВА 19 DLL: основы
- •DLL и адресное пространство процесса
- •Общая картина
- •Создание DLL-модуля
- •Что такое экспорт
- •Создание DLL для использования с другими средствами разработки (отличными от Visual C++)
- •Создание ЕХЕ-модуля
- •Что такое импорт
- •Выполнение ЕХЕ-модуля
- •ГЛАВА 20 DLL: более сложные методы программирования
- •Явная загрузка DLL и связывание идентификаторов
- •Явная загрузка DLL
- •Явная выгрузка DLL
- •Явное подключение экспортируемого идентификатора
- •Функция входа/выхода
- •Уведомление DLL_PROCESS_ATTACH
- •Уведомление DLL_PROCESS_DETACH
- •Уведомление DLL_THREAD_ATTACH
- •Уведомление DLL_THREAD_DETACH
- •Как система упорядочивает вызовы DIIMain
- •Функция DllMain и библиотека С/С++
- •Отложенная загрузка DLL
- •Программа-пример DelayLoadApp
- •Переадресация вызовов функций
- •Известные DLL
- •Перенаправление DLL
- •Модификация базовых адресов модулей
- •Связывание модулей
- •ГЛАВА 21 Локальная память потока
- •Динамическая локальная память потока
- •Использование динамической TLS
- •Статическая локальная память потока
- •Пример внедрения DLL
- •Внедрение DLL c использованием реестра
- •Внедрение DLL с помощью ловушек
- •Утилита для сохранения позиций элементов на рабочем столе
- •Внедрение DLL с помощью удаленных потоков
- •Программа-пример lnjLib
- •Библиотека lmgWalk.dll
- •Внедрение троянской DLL
- •Внедрение DLL как отладчика
- •Внедрение кода в среде Windows 98 через проецируемый в память файл
- •Внедрение кода через функцию CreateProcess
- •Перехват API-вызовов: пример
- •Перехват API-вызовов подменой кода
- •Перехват API-вызовов с использованием раздела импорта
- •Программа-пример LastMsgBoxlnfo
- •ЧАСТЬ V СТРУКТУРНАЯ ОБРАБОТКА ИСКЛЮЧЕНИЙ
- •ГЛАВА 23 Обработчики завершения
- •Примеры использования обработчиков завершения
- •Funcenstein1
- •Funcenstein2
- •Funcenstein3
- •Funcfurter1
- •Проверьте себя: FuncaDoodleDoo
- •Funcenstein4
- •Funcarama1
- •Funcarama2
- •Funcarama3
- •Funcarama4: последний рубеж
- •И еще о блоке finally
- •Funcfurter2
- •Программа-пример SEHTerm
- •ГЛАВА 24 Фильтры и обработчики исключений
- •Примеры использования фильтров и обработчиков исключений
- •Funcmeister1
- •Funcmeister2
- •EXCEPTION_EXECUTE_HANDLER
- •Некоторые полезные примеры
- •Глобальная раскрутка
- •Остановка глобальной раскрутки
- •EXCEPTION_CONTINUE_EXECUTION
- •Будьте осторожны с EXCEPTION_CONTINUE_EXECUTION
- •EXCEPTION_CONTINUE_SEARCH
- •Функция GetExceptionCode
- •Функция GetExceptionlnformation
- •Программные исключения
- •ГЛАВА 25 Необработанные исключения и исключения С++
- •Отладка по запросу
- •Отключение вывода сообщений об исключении
- •Принудительное завершение процесса
- •Создание оболочки вокруг функции потока
- •Создание оболочки вокруг всех функций потоков
- •Автоматический вызов отладчика
- •Явный вызов функции UnhandledExceptionFilter
- •Функция UnhandledExceptionFilter изнутри
- •Исключения и отладчик
- •Программа-пример Spreadsheet
- •Исключения С++ и структурные исключения
- •Перехват структурных исключений в С++
- •ЧАСТЬ VI ОПЕРАЦИИ С ОКНАМИ
- •ГЛАВА 26 Оконные сообщения
- •Очередь сообщений потока
- •Посылка асинхронных сообщений в очередь потока
- •Посылка синхронных сообщений окну
- •Пробуждение потока
- •Флаги состояния очереди
- •Алгоритм выборки сообщений из очереди потока
- •Пробуждение потока с использованием объектов ядра или флагов состояния очереди
- •Передача данных через сообщения
- •Программа-пример CopyData
- •ГЛАВА 27 Модель аппаратного ввода и локальное состояние ввода
- •Поток необработанного ввода
- •Локальное состояние ввода
- •Ввод с клавиатуры и фокус
- •Управление курсором мыши
- •Подключение к очередям виртуального ввода и переменным локального состояния ввода
- •Программа-пример LISLab
- •Программа-пример LISWatch

ГЛАВА 8 Синхронизация потоков в пользовательском режиме
Windows лучше всего работает, когда все потоки могут заниматься своим делом, не взаимодействуя друг с другом Однако такая ситуация очень редка Обычно поток создается для выполнения определенной работы, о завершении которой, вероятно, захочет узнать другой поток.
Все потоки в системе должны иметь доступ к системным ресурсам — кучам, пос ледовательным портам, файлам, окнам и т д. Если один из потоков запросит моно польный доступ к какому-либо ресурсу, другим погокам, которым тоже нужен этот ресурс, не удастся выполнить свои задачи. А с другой стороны, просто недопустимо, чтобы потоки бесконтрольно пользовались ресурсами. Иначе может получиться так, что один поток пишет в блок памяти, из которого другой что-то считывает. Представь те, Вы читаете книгу, а в это время кто-то переписывает текст на открытой Вами стра нице Ничего хорошего из этого не выйдет
Потоки должны взаимодействовать друг с другом в двух основных случаях:
совместно используя разделяемый ресурс (чтобы не разрушить его); когда нужно уведомлять другие потоки о завершении каких-либо операций
Синхронизации потоков — тематика весьма обширная, и мы рассмотрим ее в этой и следующих главах Одна новость Вас обрадует в Windows есть масса средств, упро щающих синхронизацию потоков. Но другая огорчич: точно спрогнозировать, в ка кой момент потоки будут делать то-то и то-то, крайне сложно. Наш мозг не умеет работать асинхронно, мы обдумываем свои мысли старым добрым способом — одну за другой по очереди Однако многопоточная среда ведет себя иначе.
С программированием для многопоточной среды я впервые столкнулся в 1992 г Поначалу я делал уйму ошибок, так что в главах моих книг и журнальных статьях хватало огрехов, связанных с синхронизацией потоков. Сегодня я намного опытнее и действительно считаю, что уж в этой-то книге все безукоризненно (хотя самонаде янности у меня вроде бы поубавилось) Единственный способ освоить синхрониза цию потоков — заняться этим на практике. Здесь и в следующих главах я объясню, как работает система и как правильно синхронизировать потоки. Однако Вам при дется стоически переносить трудности, приобретая опыт, ошибок не избежать
Атомарный доступ: семейство Inferlockectфункций
Большая часть синхронизации потоков связана с атомарным доступом (atomic access) — монопольным захватом ресурса обращающимся к нему потоком. Возьмем простой пример
// определяем глобальную переменную lorig g_x = 0;
DWORD WINAPI ThreadFunc1(PVOID pvParam) { g_x++;
return(0); }
DWORD WINAPI ThreadFunc2(PVOID pvParam} { g_x++;
return(0); }
Я объявил глобальную переменную g_n и инициализировал ее нулевым значени ем. Теперь представьте, что я создал два потока: один выполняет ThreadFunc1, дру гой — ThreadFunc2 Код этих функций идентичен: обе увеличивают значение глобаль ной переменной g_x па 1. Поэтому Вы, наверное, подумали: когда оба потока завер шат свою работу, значение g_x будет равно 2. Так ли это? Может быть. При таком коде заранее сказать, каким будет конечное значенис g_x, нельзя. И вот почему. Допустим, компилятор сгенерировал для строки, увеличивающей g_x на 1, следующий код:
MOV EAX, [g_x] , значение из g_x помещается в регистр
INC EAX ; значение регистра увеличивается на 1
MOV [g_x], EAX ; значение из регистра помещается обратно в g_x
Вряд ли оба потока будут выполнять этот код в одно и то же время. Если они бу дут делать это по очереди — сначала один, потом другой, тогда мы получим такую картину:
MOV EAX, [g_x] ; поток 1 в регистр помещается 0
INC EAX ; поток V значение регистра увеличивается на 1
MOV [g_x], EAX , поток 1. значение 1 помещается в g_x
MOV EAX, [g_x] ; поток 2 в регистр помещается 1
INC EAX ; поток 2. значение регистра увеличивается до 2
MOV [g_x], EAX , поток 2. значение 2 помещается в g_x
После выполнения обоих потооков значение g_x будет равно 2 Это просто заме чательно и как раз то, что мы ожидали: взяв переменную с нулевым значением, дваж ды увеличили ее на 1 и получили в результате 2. Прекрасно. Но постойте-ка, ведь Windows — это среда, которая поддерживает многопоточность и вытесняющую мно гозадачность. Значит, процессорное время в любой момент может быть отнято у од ного потока и передано другому. Тогда код, приведенный мной выше, может выпол няться и таким образом:
MOV EAX, [g_x] ; лоток V в регистр помещается 0
INC EAX ; поток 1. значение регистра увеличивается на 1
MOV EAX, [g_x] ; поток 2 в регистр помещается 0
INC EAX ; поток 2. значение регистра увеличивается на 1
MOV [g_x], EAX , поток 2. значение 1 помещается в g_x
MOV [g_x], EAX , поток V значение 1 помещается в g_x
А если код будет выполняться именно так, конечное значение g_x окажется рав ным 1, а не 2, как мы думали! Довольно пугающе, особенно если учесть, как мало у нас рычагов управления планировщиком. Фактически, даже при сотне потоков, кото рые выполняют функции, идентичные нашей, в конечном итоге вполне можно полу чить в g_x все ту же единицу! Очевидно, что в таких условиях работать просто нельзя. Мы вправе ожидать, что, дважды увеличив 0 на 1, при любых обстоятельствах полу чим 2 Кстати, результаты
могут зависеть оттого, как именно компилятор генерирует машинный код, а также от того, как процессор выполняет этот код и сколько процес соров установлено в машине. Это объективная реальность, в которой мы нс в состо янии что-либо изменить Однако в Windows есть ряд функций, которые (при правиль ном их использовании) гарантируют корректные результаты выполнения кода.
Решение этой проблемы должно быть простым. Все, что нам нужно, — это спо соб, гарантирующий приращение значения переменной на уровне атомарного дос тупа, т. e. без прерывания другими потоками. Семейство Interlocked-функций как раз и дает нам ключ к решению подобных проблем. Большинство разработчиков про граммного обеспечения недооценивает эти функции, а ведь они невероятно полез ны и очень просты для понимания. Все функции из этого семейства манипулируют переменными на уровне атомарного доступа. Взгляните на InterlockedExchangeAdd
LONG InterlockedExchangeAdd( PLONG plAddend, LONG lIncrement);
Что может быть проще? Вы вызываете эту функцию, передавая адрес переменной типа LONG и указываете добавляемое значение InterlockedExchangeAdd гарантирует, что операция будет выполнена атомарно. Перепишем наш код вот так:
// определяем глобальную переменную long g_x = 0;
DWORD WINAPI ThreadFunc1(PVOID pvParam) { InterlockedExchangeAdd(&g_x, 1); return(0); }
DWORD WINAPI ThreadFunc2(PVOID pvPararr) { InterlockedExchangeAdd(&g_x, 1); return(0); }
Теперь Вы можете быть уверены, что конечное значение g_x будет равно 2. Ну, Вам уже лучше? Заметьте: в любом потоке, где нужно модифицировать значение разделя емой (общей) переменной типа LONG, следует пользоваться лишь Interlocked-функ циями и никогда не прибегать к стандартным операторам языка С:
//переменная типа LONG, используемая несколькими потоками
LONG g_x;
//неправильный способ увеличения переменной типа LONG
g_x++;
// правильный способ увеличения переменной типа LONG
InterlockedExchangeAdd(&g_x, 1);
Как же работают Interlocked-функции? Ответ зависит от того, какую процессорную платформу Вы используете. На компьютерах с процессорами семейства x86 эти фун кции выдают по шине аппаратный сигнал, не давая другому процессору обратиться по тому же адресу памяти. На платформе Alpha Interlocked-функции действуют при мерно так:
1.Устанавливают специальный битовый флаг процессора, указывающий, что дан ный адрес памяти сейчас занят.
2.Считывают значение из памяти в регистр.
3.Изменяют значение в регистре.
4.Если битовый флаг сброшен, повторяют операции, начиная с п. 2. В ином слу чае значение из регистра помещается обратно в память.
Вас, наверное, удивило, с какой это стати битовый флаг может оказаться сброшен ным? Все очень просто. Его может сбросить другой процессор в системе, пытаясь модифицировать тот же адрес памяти, а это заставляет Interlocked-функции вернуть ся в п.
2.
Вовсе не обязательно вникать в детали работы этих функций. Вам нужно знать лишь одно: они гарантируют монопольное изменение значений переменных незави симо oт того, как именно компилятор генерирует код и сколько процессоров уста новлено в компьютере. Однако Вы должны позаботиться о выравнивании адресов переменных, передаваемых этим функциям, иначе они могут потерпеть неудачу, (О выравнивании данных я расскажу в главе 13.)
Другой важный аспект, связанный с Interlocked-функциями, состоит в том, что они выполняются чрезвычайно быстро. Вызов такой функции обычно требует не более 50 тактов процессора, и при этом не происходит перехода из пользовательского ре жима в режим ядра (а он отнимает не менее 1000 такюв).
Кстати, InterlockedExchangeAdd позволяет не только увеличить, но и уменьшить значение
— просто передайте во втором параметре отрицательную величину. Interlo ckedExchangeAdd возвращает исходное значение в *plAddend
Вот еще две функции из этого семейства:
LONG InterlockedExchange( PLONG plTarget, LONG IValue);
PVOTD InterlockedExchangePointer( PVOID* ppvTarget, PVOID* pvValue);
InterlockedExchange и InterlockedExchangePointer монопольно заменяют текущее значение переменной типа LONG, адрес которой передается в первом параметре, на значение, передаваемое во втором параметре В 32-разрядпом приложении обе фун кции работают с 32-разрядными значениями, но в 64-разрядной программе первая оперирует с 32разрядными значениями, а вторая — с 64-разрядными Все функции возвращают исходное значение переменной InterlockedExchange чрезвычайно полезна при реализации спин-
блокировки (spinlock):
// глобальная переменная, используемая как индикатор того, занят ли разделяемый ресурс
BOOL g_fResourceInUse = FALSE ;
...
void Func1() {
// ожидаем доступа к ресурсу
while (InterlockedExchange(&g_fResourceInUse, TRUE) = TRUE) Sleep(0);
//получаем ресурс в свое распоряжение
//доступ к ресурсу больше не нужен
InterlockedFxchange(&g_fResourceInUse, FALSE); }
В этой функции постоянно «крутится» цикл while, в котором переменной g_fResour ceInUse присваивается значение TRUE и проверяется ее предыдущее значение. Если оно было равно FALSE, значит, ресурс не был занят, но вызывающий поток только что занял его, на этом цикл завершается. В ином случае (значение было равно TRUE) ре сурс занимал другой поток, и цикл повторяется
Если бы подобный код выполнялся и другим потоком, его цикл while работал бы до тех пор, пока значение переменной g_fResourceInUse вновь не изменилось бы на FALSE. Вызов InterlockedExchange в конце функции показывает, как вернуть перемен ной g_fResourceInUse значение FALSE.
Применяйте эту методику с крайней осторожностью, потому что процессорное время при спин-блокировке тратится впустую Процессору приходится постоянно сравнивать два значения, пока одно из них не будет "волшебным образом» изменено другим потоком. Учтите - этот код подразумевает, что все потоки, использующие спин блокировку, имеют одинаковый уровень приоритета. К тому же. Вам, наверное, при дется отключить динамическое повышение приоритета этих потоков (вызовом SetPro cessPriorityBoost или
SetThreadPriorityBoost).
Вы должны позаботиться и о том, чтобы переменная — индикатор блокировки и данные, защищаемые такой блокировкой, не попали в одну кэш-линию (о кэш-лини ях я расскажу в следующем разделе). Иначе процессор, использующий ресурс, будет конкурировать с любыми другими процессорами, которые пытаются обратиться к тому же ресурсу. А это отрицательно cкажется на быстродействии
Избегайте спин-блокировки на однопроцессорных машинах "Крутясь" в цикле, поток впустую транжирит драгоценное процессорное время, не давая другому пото ку изменить значение неременной. Применение функции Sleep в цикле while несколь ко улучшает ситуацию. С ее помощью Вы можете отправлять свой поток в сон ня не кий случайный отрезок времени и после каждой безуспешной попытки обратиться к ресурсу увеличивать этот отрезок Тогда потоки нс будут зря отнимать процессорное время. В зависимости от ситуации вызов Sleep можно убрать или заменить на вызов SwitchToThread (эта функция в Windows 98 не доступна). Очень жаль, но, по-видимо му, Вам придется действовать здесь методом проб и ошибок.
Спин-блокировка предполагает, что защищенный ресурс не бывает занят надол го И тогда эффективнее делать так: выполнять цикл, переходить в режим ядра и ждать Многие разработчики повторяют цикл некоторое число раз (скажем, 4000) и, если ресурс к тому времени не освободился, переводят поток в режим ядра, где он спит, ожидая освобождения ресурса (и не расходуя процессорное время). По такой схеме реализуются критические секции (critical sections).
Спин-блокировка полезна на многопроцессорных машинах, гдс один поток мо жет "крутиться" в цикле, а второй — работать на другом процессоре Но даже в таких условиях надо быть осторожным. Вряд ли Вам понравится, если поток надолго вой
дет в цикл, ведь тогда он будет впустую тратить процессорное время. О спин-блоки ровке мы еще поговорим в этой главе Кроме того, в главе 10 я покажу, как использо вать спинблокировку на практике.
Последняя пара Interlocked-функций выглядит так:
PVOID InterlockedCompareExchange( PLONG pIOestination, LONG
lExchange, LONG lComparand);
PVOID InterlockedCompareExchangePointer( PVOID* ppvDestination, PVOID pvExchange, PVOID pvComparand);
Они выполняют операцию сравнения и присвоения на уровне атомарного досту па. В 32разрядном приложении обе функции работают с 32-разрядными значения ми, но в 64разрядном приложении InterlockedCompareExchange используется для 32 разрядных значений, a InterlockedCompareExcbangePointer - для 64-разрядных. Вот как они действуют, если представить это в псевдокоде.
LONG InterlockedCompareExchange(PLONG plDestination, LONG lExchange, LONG lComparand) {
LONG lRet = *plDestination; // исходное значение
if (*plDestination == lComparand) *plDestination = lExchange;
return(lRet); }
Функция сравнивает текущее значение переменной типа LONG (на которую ука зывает параметр plDestination) со значением, передаваемым в параметре lComparand. Если значения совпадают, *plDestination получает значение параметра lExchange; в ином случае *pUDestination остается без изменений. Функция возвращает исходное значение *plDestination. И не забывайте, что все эти действия выполняются как еди ная атомарная операция.
Обратите внимание на отсутствие Interlocked-функции, позволяющей просто счи тывать значение какой-то переменной, не меняя его. Она и не нужна Если один по ток модифицирует переменную с помощью какой-либо Interlocked-функции в тот момент, когда другой читает содержимое той же переменной, ее значение, прочитан ное вторым потоком, всегда будет достоверным. Он получит либо исходное, либо измененное значение переменной Поток, конечно, не знает, какое именно значение он считал, но главное, что оно корректно и не является некоей произвольной вели чиной. В большинстве приложений этого вполне достаточно.
Interlocked-функции можно также использовать в потоках различных процессов для синхронизации доступа к переменной, которая находится в разделяемой облас ти памяти, например в проекции файла. (Правильное применение Interlocked-функ ций демонстрирует несколько программ-примеров из главы 9 )
В Windows есть и другие функции из этого семейства, но ничего нового по срав нению с тем, что мы уже рассмотрели, они нс делают Вот еще две из них.
LONG Interlockedlncrernent(PLONG plAddend);
LONG IntorlockedDecrcment(PLONG plAddend);
InterlockedExchangeAdd полностью заменяет обе эти устаревшие функции. Новая функция умеет добавлять и вычитать произвольные значения, а функции Interlocked Increment и InterlockedDecrement увеличивают и уменьшают значения только на 1.