Написание многопотоковой программы Win32
При написании программы, выполняющейся в нескольких потоках, необходимо скоординировать их поведение и использование ресурсов программы. Необходимо также убедиться, что каждый поток получает собственный стек.
Совместное использование ресурсов потоками
Примечание
Аналогичные вопросы с точки зрения MFC рассматриваются в разделах Многопоточность. Советы по программированию и Многопоточность. Использование классов синхронизации.
У каждого потока есть свой стек и копия регистра процессора. Другие ресурсы, такие как файлы, статические данные и динамическая память используются совместно всеми потоками процесса. Потоки, использующие общие ресурсы, должны быть синхронизованы. Win32 предоставляет несколько способов синхронизации ресурсов, включая семафоры, критические секции, события и мьютексы.
Если несколько потоков обращаются к статическим данным, необходимо предусмотреть в программе возможное возникновение конфликта ресурсов. Рассмотрим программу, в которой в одном потоке происходит обновление структуры статических данных, содержащей координаты по оси X и Y элементов, которые должны отображаться в другом потоке. Если поток обновления успевает изменить координату по оси X и прерывается до изменения координаты по оси Y, отображение во втором потоке может быть выполнено раньше, чем будет обновлена координата по оси Y. Поэтому элемент может быть отображен в неправильном месте. Этой проблемы можно избежать, если использовать семафоры для управления доступом к структуре.
Мьютекс (сокращение от mutual exclusion — "взаимное исключение") — это способ взаимодействия между потоками и процессами, которые выполняются асинхронно. Это взаимодействие обычно используется для координации действий, выполняющихся в нескольких потоках или процессах, обычно с помощью управления доступом к общему ресурсу (открывая и закрывая доступ к нему). Чтобы решить проблему обновления координат X и Y, перед выполнением обновления поток обновления должен задать мьютекс, обозначающий, что структура данных уже используется. Он сбросит мьютекс после того, как обе координаты будут обработаны. Прежде чем выполнять обновление отображения, поток отображения должен дождаться сброса мьютекса. Процесс ожидания мьютекса часто называют блокировкой мьютекса, поскольку процесс блокируется и не может быть продолжен до тех пор, пока мьютекс не будет сброшен.
В программе Bounce.c, показанной в разделе Образец многопотоковой программы на C, используется мьютекс ScreenMutex для управления обновлениями экрана. Каждый раз, когда один из потоков отображения готов к выводу изображения на экран, он вызывает функцию WaitForSingleObject с дескриптором для мьютекса ScreenMutex и константой INFINITE, которые указывают, что функция WaitForSingleObject должна прерваться на мьютексе и не вызвать тайм-аут. Если мьютекс ScreenMutex сброшен, функция ожидания устанавливает его таким образом, что другие потоки не смогут взаимодействовать с дисплеем и продолжает выполнение потока. В противном случае выполнение потока блокируется до тех пор, пока мьютекс не будет сброшен. После завершения обновления дисплея поток освобождает мьютекс с помощью функции ReleaseMutex.
Отображение на дисплее и статические данные — это не все ресурсы, требующие аккуратного управления. Например, в программе может быть несколько потоков, обращающихся к одному и тому же файлу. Поскольку любой поток может перемещать указатель файла, каждый поток должен сбрасывать указатель файла перед выполнением чтения или записи. Кроме того, необходимо обеспечить, чтобы в промежуток времени между моментом, когда поток устанавливает указатель, и моментом, когда он обращается к файлу, другие потоки не осуществляли доступ к этому файлу. Потоки должны использовать семафор для управления доступом к файлу. Для этого каждый доступ к файлу должен быть заключен между вызовами функций WaitForSingleObject и ReleaseMutex. Данный метод демонстрируется в следующем примере кода:
HANDLE hIOMutex= CreateMutex (NULL, FALSE, NULL);
WaitForSingleObject( hIOMutex, INFINITE );
fseek( fp, desired_position, 0L );
fwrite( data, sizeof( data ), 1, fp );
ReleaseMutex( hIOMutex);
Стеки потоков
Все пространство для стеков, зарезервированное в приложении по умолчанию, выделяется для первого потока выполнения, называемого поток 1. Поэтому необходимо указать, сколько памяти должно быть выделено для стека в каждом дополнительном потоке, необходимом для программы. Операционная система при необходимости выделяет дополнительное пространство под стеки потоков, однако значение по умолчанию должно быть задано.
Первый аргумент функции _beginthread является указателем на функцию BounceProc, которая выполняет потоки. Второй аргумент указывает размер стека потока по умолчанию. Последний аргумент является идентификатором, передаваемым в функцию BounceProc. Функция BounceProc использует этот идентификатор для запуска генератора случайных чисел и выбора атрибута цвета и отображаемого символа в потоке.
Потоки, осуществляющие вызовы библиотеки среды выполнения C или API-интерфейса Win32, должны иметь достаточно стекового пространства для вызываемой библиотеки и функций API. Для функции printf в языке C требуется более 500 байт стекового пространства, поэтому при вызове подпрограмм API-интерфейса Win32 необходимо, чтобы было доступно 2 КБ стекового пространства.
Поскольку у каждого потока имеется собственный стек, можно избежать потенциальных конфликтов из-за элементов данных, если использовать как можно меньше статических данных. Можно разработать программу таким образом, чтобы она применяла автоматические переменные стека для всех закрытых данных потока. Единственными глобальными переменными в программе Bounce.c являются мьютексы и переменные, которые не изменяются после инициализации.
Win32 также предоставляет локальную память потока (TLS) для хранения данных для каждого потока. Дополнительные сведения см. в разделе Локальное хранилище потока (TLS).
См. также
Основные понятия
Реализация многопоточности на языке C с помощью функций Win32