![]() |
Многозадачность в Windows
Многозадачность в Windows.
-Для кого эта статья? -В первую очередь для начинающих программистов, решивших освоить основные приемы и концепции программирования приложений для Windows. Объекты ядра Процессы и потоки Синхронизация __Синхронизация в пользовательском режиме ____Критические секции ____Interlocked-функции __Синхронизация в режиме ядра ____Wait-функции ____События ____Ожидаемые таймеры ____Семафоры ____Мьютексы Обмен данными между процессами Outro Объекты ядра. (Процессы, потоки, семафоры, события, мьютексы, файлы, проекции файлов, задания, каналы, потоки завершения ввода и вывода, почтовые ящики, фабрики пула потоков) Объект ядра (kernel object) - это блок памяти, выделенный ядром. Доступ к нему напрямую в целях безопасности, а также для обеспечения механизмов абстракции, может осуществить только ядро, однако существуют специальные функции Windows, которые могут строго определенным способом воздействовать на объект и его поля. Все эти функции оперируют дескриптором (handle, описатель) объекта, который возвращается после его создания функцией CreateObjName(). Например, CreateMutex() указывает системе на то, что нужно сформировать новый объект - мьютекс, и возвращает его дескриптор. При инициализации процесса система создает специальную таблицу дескрипторов, используемую только для объектов ядра. При создании объекта система, проверяет, не существует ли уже объект с таким именем. Если объект данного типа с таким именем уже существует, проверяет права доступа и, если с правами все в порядке, заносит дескриптор объекта в таблицу дескрипторов данного процесса. При попытке создания объекта с именем, которое уже используется в качестве имени другого объекта другого типа, функция возвращает значение NULL или INVALID_HANDLE_VALUE. Полученный дескриптор может быть использован любым потоком вашего процесса, а также с помощью механизмов синхронизации, о которых будет рассказано ниже, потоком другого процесса. Если же существует объект с таким же именем, но другого типа, функция возвращает NULL. Существуют также так называемые дескрипторы защиты, которые позволяют указать объекту кто его породил и кто может с ним работать. Для завершения работы с объектом ядра вызывается функция CloseObjName(), аргументом которой является дескриптор объекта. После вызова функция проверяет таблицу дескрипторов вызвавшего ее процесса и, в случае если соответствующий дескриптору объект находится там, система уменьшает значение счетчика пользователей. Каждый объект имеет свой собственные счетчик пользователей объекта ядра. При обращении процесса к объекту ядра счетчик пользователей объекта инкрементируется, после завершения процесса - декрементируется. В момент, когда счетчик становится равным нулю, ядро удаляет объект. Процессы и потоки. Процесс (process) предстваляет собой совокупность ресурсов, необходимых для выполнения программы, или же просто ее экземпляр, загруженный в память. Процесс - барин. Он владеет всеми данными, дескрипторами, виртуальным пространством, однако сам по себе ничего не делает. Всю работу за него выполняет поток (thread). Каждый процесс имеет хотя бы один поток, называемый главным (первичным), который в свою очередь может создавать другие потоки. Так как физических процессоров, как правило, значительно меньше, нежели чем потоков, потоки выполняются вовсе не одновременно, просто переключение между ними происходит очень часто, создавая ощущение одновременности. Каждому потоку система отводит свое процессорное время, которое он и работает. Потоки могут находиться в трех состояниях: выполнения, ожидания и блокировки. Ключевым моментом при изучении потоков является способ организации очередности потоков. Windows поддерживает 32 приоритета потоков (0-31), который определяется исходя из приоритета создавшего данный поток процесса и относительного приоритета самого потока. В Windows реализована система вытесняющего планирования на основе приоритетов, то есть освободившийся процессор продолжит обслуживать тот процесс, который обладает наибольшим приоритетом. Процессам, запускаемым пользователем, изначально присваивается класс Normal. Процессы этого класса делятся на процессы переднего плана (foreground) и фоновые (background). Уровень процесса, с которым в данный момент работает пользователь, автоматически поднимается на две единицы. Для изменения класса приоритета процесса может использоваться функция SetPriorityClass(). Изначально поток наследует приоритет создавшего его процесса (базовый приоритет), однако он может быть изменен функцией SetThreadPriority(). Иногда возникает необходимость в разделении объектов ядра между потоками разных процессов. Это можно осуществить с помощью наследования дескриптора объекта, именованных объектов и дублирования дескрипторов объектом. Углубляться не будем ) Пример работы с процессом: Код:
int main() {Код:
void ThreadFunc(DWORD lpv) {Синхронизация. Все потоки процесса имеют доступ к определенным ресурсам, таким как адресное пространство оперативной памяти, открытые файлы, глобальные переменные, что не может не вызвать определенных проблем. Что произойдет, если один поток еще не закончил работать с каким-либо общим ресурсом, а система переключилась на другой поток, использующий тот же ресурс? Такие конфликты могут возникать и между потоками разных процессов. Именно во избежание таких проблем и был создан механизм синхронизации потоков. Основными типами объектов, которые позволяют управлять многопоточным приложением являются: критические секции, мьютексы, события, семафоры, атомарные операции API-уровня, ожидаемые таймеры. Каждый из этих объектов реализует свой способ синхронизации, однако все они используются для координирования доступа к ресурсам. Большая часть механизмов синхронизации потоков связана с атомарным доступом (atomic access) - монопольным использованием ресурса обращающимся к нему потоком. Несомненно, кроме данных объектов синхронизатором могут выступать сами потоки и процессы, передающие управление друг другу, или пользователь, вручную следящий за ходом выполнения задачи, однако ввиду ненадежности и не распространенности данных методов, рассматриваться они в этой статье не будут. Синхронизация в пользовательском режиме. Пользовательский режим (user mode) не имеет доступа к оборудованию и имеет ограниченный доступ к памяти. Механизмы синхронизации, использующие данный режим выполняются очень быстро. Критические секции. Критическая секция (critical section) - это участок кода, в котором поток получает доступ к ресурсу, доступному для других потоков. Этим объектом может владеть лишь один поток, что и обеспечивает синхронизацию. Если "одновременно" несколько потоков попытаются получить доступ к критическому участку, то контроль над ним будет передан только одному из них, а остальные будут переведены в состояние ожидания, до тех пор, пока участок не освободится. Для использования критических секций создан специальный тип данных CRITICAL_SECTION. Инициализация критического участка происходит по средством функции InitializeCriticalSection(&CS), где CS - переменная типа CRITICAL_SECTION. Для входа в критическую секцию используется функция EnterCriticalSection(&CS). Если критический участок не используется в данный момент никаким из потоков, то секция обозначается системой как занятая, и поток продолжает выполняться. Если же критический участок уже используется, то поток блокируется до тех пор, пока участок не будет освобожден. Существует функция, вызывая которую, поток пытается войти в критическую секцию - TryEnterCriticalSection(&CS), которая очень полезна для обеспечения высокой производительности приложения. Вот пример кода, с использованием критической секции: Код:
const int COUNT = 10;Interlocked-функции. В Win32 API существует несколько функций для реализации атомарного доступа. Вот прототипы некоторых из них: Код:
LONG InterlockedIncrement ();Синхронизация в режиме ядра. Режим ядра (kernel mode) имеет доступ ко всему оборудованию и памяти. Механизмы синхронизации в данном режиме обладают значительными преимуществами по сравнению с пользовательскими, однако обеспечивают меньшее быстродействие. Большинство объектов ядра характеризуются своим состоянием: занятым или свободным. Очень часто для работы с объектом нужно определить в каком из состояний он находится и, в зависимости от состояния, выполнять те или иные действия. Wait-функции. Wait-функции (wait functions) позволяют потоку останавливаться и дожидаться освобождения объекта ядра, когда он занят. Если же объект свободен, то поток не остановится, а продолжит свою работу. Код:
DWORD WaitForSingleObject ();Вторая функция аналогична первой, но позволяет ждать освобождения сразу нескольких объектов. Принимает в качестве аргументов количество объектов, указатель на массив объектов, параметр, определяющий будет ли поток ждать освобождения всех объектов или же лишь одного, а также таймаут. Если функции указано ждать освобождения одного объекта, то в случае его освобождения она вернет WAIT_OBJECT_0 + dwCount - 1 (dwCount - количество объектов), то есть для получения индекса объекта нужно из полученного от функции значения вычесть WAIT_OBJECT_0. Пример работы wait-функций: Код:
HANDLE h[3];События. Событие (event) - это объект синхронизации, сигнализирующий об окончании какой-либо операции. События обычно используются, когда после выполнения какого-то действия один поток должен сообщить об этом другому. Создается объект функцией CreateEvent() (или ex-версия). В сигнальное положение может быть приведен функциями SetEvent() и PulseEvent(). Объекты-события могут разделяться разными процессами. Потоки могут получать доступ к уже созданному объекту несколькими путями: вызовом функции CreateEvent(), передав в качестве параметра имени уже имя созданного объекта, наследованием дескриптора, применением функции DuplicateHandle(), вызовом функции OpenEvent() с передачей в параметре имени такого же имени. События делятся на два типа: с ручным сбросом и с автоматическим сбросом. Сигнальное состояние первых объектов сохраняется до вызова функции ResetEvent(). Сигнальное состояние объектов второго типа сохраняется до тех пор, пока не будет освобожден единственный поток, после чего система устанавливает не сигнальное состояние объекта. Если нет потоков, ожидающих этого события, объект остается в сигнальном состоянии. Пример использования событий: Код:
HANDLE hEvent;Ожидаемые таймеры. Ожидаемые таймеры (waitable timers) - это объекты ядра, которые самостоятельно переходят в свободное положение через определенное время или через регулярные промежутки времени. Создается таймер функцией CreateWaitableTimer(). Настройка таймера происходит по средством вызова функции SetWaitableTimer(). Объект переходит в сигнальное состояние по истечении таймаута. Отмену можно произвести функцией CancelWaitableTimer(). По аналогии с событиями таймеры тоже бывают со сбросом вручную и авто сбросом. Пример использования ожидаемых таймеров: Код:
int main() {Семафоры. Семафоры (semaphore) позволяют нам ограничить доступ потоков к определенным ресурсам на основании их (потоков) количества. При инициализации семафору передается количество потоков, которые могут к нему обратиться. При каждом обращении к ресурсу счетчик семафора декрементируется, а после его обращения в ноль использовать ресурсы больше нельзя. Сигнальным считается состояние семафора при значении его счетчика отличном от нуля. Создается семафор функцией CreateSemaphore(), OpenSemaphore() используется для доступа к объекту, а ReleaseSemaphore() увеличивает значение счетчика текущего числа ресурсов. Мьютексы. Мьютексы (mutex, взаимоисключения) - объекты ядра, гарантирующие потокам взаимоисключающий доступ к ресурсам. Мьютексы являются аналогами критических секций в режиме ядра, что позволяет им дополнительно синхронизировать доступ к ресурсу для потоков разных процессов. Мьютекс переходит в сигнальное состояние, когда не используется ни одним из потоков. Если же мьютекс занят потоком, каждый следующий поток ждет его освобождения, так же как и в случае критических секций. Создание объекта-мьютекса происходит вызовом функции CreateMutex(), доступ - OpenMutex(). После того, как поток, работавший с мьютексом, заканчивает свои действия, он должен освободить мьютекс вызовом функции ReleaseMutex(). Из преимуществ над критическими секциями стоит отметить наличие возможности выставления таймаута для потоков и, разумеется, возможность использования его потоками разных процессов. Из недостатков - относительно низкое быстродействие. Обмен данными между процессами. Потоки одного процесса, как вы уже знаете, не имеют доступа к адресному пространству другого процесса. Однако существую специальные механизмы для передачи данных между процессами: - Буфер обмена (clipboard) - Библиотеки динамической компоновки (DLL) - Сообщение WM_COPYDATA - Разделяемая память (shared memory) - Протокол динамического обмена данными (DDE) - Удаленный вызов процедур (RPC) - ActiveX технология - Каналы (pipes) - Сокеты (sockets) - Почтовые слоты (mailslots) - Microsoft Message Queue (MSMQ) Outro. Хочу добавить, что все в этом мире хорошо в меру, поэтому старайтесь использовать приведенные здесь механизмы только в случае, если это действительно необходимо. Не имеет смысла писать многопоточное приложение для вывода на экран приветствия, незачем использовать примитивы синхронизации, когда доступ к ресурсу осуществляется лишь одним потоком. Надеюсь статья оказалась полезной для Вас. Спасибо за внимание (: (c) fata1ex 27.06.09 Копирование материала статьи только с разрешения автора. Подробнее о теме статьи вы можете прочитать в MSDN, а также в специализированной литературе, например у Джеффри Рихтера. |
Цитата:
Ведь что запрещает процессу самому всё самостоятельно выполнять? Ведь Цитата:
завершения чем процесс и поэтому его использование более выгодно |
Ты не прав. Не веришь этой статье, почитай другие.
http://www.rsdn.ru/article/baseserv/mt.xml Вот например. Цитата:
Цитата:
|
Цитата:
Просто я говорю в общем плане Да в ОС Windows так и есть,но если смотреть из самих определений процесса и потока,то строчка всё-таки некоректна |
Ну из прочитанного могу сделать следующие поправки
0) больше дело тут в многопоточности нежели многозадачности, как сказал H1Z 1) Синхронизация в режиме ядра - звучит как будто ты перечисляешь функции которые применяются только в ядре и их незя юзать в юзермоде. 2) Важно знать, что Interlocked-функции выполняются чрезвычайно быстро. - Лучше бы наглядно показал принцип работы данных функций (дизасемб kernel.dll дает большое понимание что и как делается). Цитата:
Как показал дизасем выглядит это примерно так: Код:
mov ecx, [esp+4]Потому как экономия будет на половине машинных инструкций ) А вообще для тех кому очень интересна работа с данными функциями, то советую прочитать статейку [link]http://itcentre.ru/programming/science-work/parallel-programming/369/[/link] В особенности очень полезно юзать InterlockedExchange для организации спин-блокировки, что может являться более быстрым аналогом критических секций, при условии что время ожидание минимально. 3) Обмен данными между процессами. - както вообще лишние. При том что - Библиотеки динамической компоновки (DLL) - это вообще какбы нето, потому как напрямую через них ничего не делается ) Сообщение WM_COPYDATA - это стандартная вешь, а вообще обмен данными может происходить через много других сообщений таких как WM_SETTEXT итд Удаленный вызов процедур (RPC) - тут больше дело не в обмене информацией а в её обработке. А вообще очень хорош также способ связанный с файлмаппингом. Ну а так в принципе статейка нормальная с точки зрения теоритических материалов, а вот практических - както не очень ) Но тем кому придется с этим столкнуться, те уже будут знать в какую сторону им копать. |
Спасибо )
Насчет 0) я так и не понял почему. По-моему как раз многозадачность, тем более H1Z со мной согласился, удалив свое сообщение = ) Насчет остального могу сказать, что хотел сделать лишь обзор. Вся практическая часть сосредоточена в msdn и переписывать ее смысла большого нет. Еще раз спасибо за комментарий. |
Цитата:
|
на счет 0) - дело в том что ты описываешь механизмы синхронизации между потоками, всё что ты описал применимо именно к потокам.
Потому как если будут 2 параллельных процесса (задачи) то практически все эти функции бессмысленные для синхронизации, за исключением интерлок, и то с большим ограничением. И ты не путай что многопоточность - это проше говоря параллельное выполнение задачи, а многозадачность - это параллельное выполнение нескольких разных задач (что в ОС подразумевает - разные процессы). На счет msdn - там многое на англ. и этого все пугаются и там чаще всего примеры или приметивные на столько что мало кто их поймет или наоборот слишком большие. |
События тоже бессмысленны для синхронизации параллельных процессов?
|
Можно, но для этого придется замарачиваться с копированием дискрипторов.
Если дело на то пошло, то можно синхронизацию устроить и через CreateFile открыв файл на RW в расшаренном доступе. И через это общаться ) |
| Время: 09:49 |