Многозадачность в 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(). Иногда возникает необходимость в разделении объектов ядра между потоками разных процессов. Это можно осуществить с помощью наследования дескриптора объекта, именованных объектов и дублирования дескрипторов объектом. Углубляться не будем ) Пример работы с процессом: Code: int main() { STARTUPINFO si; ZeroMemory(&si,sizeof(STARTUPINFO)); PROCESS_INFORMATION pi; if (CreateProcess(L"C:\\WINDOWS\\notepad.exe",NULL, NULL,NULL,FALSE,NULL,NULL,NULL,&si,&pi)==TRUE) { Sleep(10000); TerminateProcess(pi.hProcess,NO_ERROR); } return 0; } Пример работы с потоком: Code: void ThreadFunc(DWORD lpv) { std::cout << "Im in thread" << std::endl; } int main() { HANDLE Thr; Thr = CreateThread(0,0,(LPTHREAD_START_ROUTINE) ThreadFunc,0,0,0); if (Thr) { std::cout << "Thread launched" << std::endl; } CloseHandle(Thr); return 0; } В реальных приложениях следует использовать другие функции, например _beginthreadex(). Синхронизация. Все потоки процесса имеют доступ к определенным ресурсам, таким как адресное пространство оперативной памяти, открытые файлы, глобальные переменные, что не может не вызвать определенных проблем. Что произойдет, если один поток еще не закончил работать с каким-либо общим ресурсом, а система переключилась на другой поток, использующий тот же ресурс? Такие конфликты могут возникать и между потоками разных процессов. Именно во избежание таких проблем и был создан механизм синхронизации потоков. Основными типами объектов, которые позволяют управлять многопоточным приложением являются: критические секции, мьютексы, события, семафоры, атомарные операции API-уровня, ожидаемые таймеры. Каждый из этих объектов реализует свой способ синхронизации, однако все они используются для координирования доступа к ресурсам. Большая часть механизмов синхронизации потоков связана с атомарным доступом (atomic access) - монопольным использованием ресурса обращающимся к нему потоком. Несомненно, кроме данных объектов синхронизатором могут выступать сами потоки и процессы, передающие управление друг другу, или пользователь, вручную следящий за ходом выполнения задачи, однако ввиду ненадежности и не распространенности данных методов, рассматриваться они в этой статье не будут. Синхронизация в пользовательском режиме. Пользовательский режим (user mode) не имеет доступа к оборудованию и имеет ограниченный доступ к памяти. Механизмы синхронизации, использующие данный режим выполняются очень быстро. Критические секции. Критическая секция (critical section) - это участок кода, в котором поток получает доступ к ресурсу, доступному для других потоков. Этим объектом может владеть лишь один поток, что и обеспечивает синхронизацию. Если "одновременно" несколько потоков попытаются получить доступ к критическому участку, то контроль над ним будет передан только одному из них, а остальные будут переведены в состояние ожидания, до тех пор, пока участок не освободится. Для использования критических секций создан специальный тип данных CRITICAL_SECTION. Инициализация критического участка происходит по средством функции InitializeCriticalSection(&CS), где CS - переменная типа CRITICAL_SECTION. Для входа в критическую секцию используется функция EnterCriticalSection(&CS). Если критический участок не используется в данный момент никаким из потоков, то секция обозначается системой как занятая, и поток продолжает выполняться. Если же критический участок уже используется, то поток блокируется до тех пор, пока участок не будет освобожден. Существует функция, вызывая которую, поток пытается войти в критическую секцию - TryEnterCriticalSection(&CS), которая очень полезна для обеспечения высокой производительности приложения. Вот пример кода, с использованием критической секции: Code: const int COUNT = 10; int Sum = 0; CRITICAL_SECTION CS; DWORD WINAPI FirstThread (PVOID pvParam) { EnterCriticalSection(&CS); Sum = 0; for (int i = 1; i <= COUNT; i++) Sum += i; LeaveCriticalSection(&CS); return (Sum); } DWORD WINAPI SecondThread (PVOID pvParam) { EnterCriticalSection(&CS); Sum = 0; for (int i = 1; i <= COUNT; i++) Sum += i; LeaveCriticalSection(&CS); return (Sum); } Interlocked-функции. В Win32 API существует несколько функций для реализации атомарного доступа. Вот прототипы некоторых из них: Code: LONG InterlockedIncrement (); LONG InterlockedDecrement (); LONG InterlockedExchange (); PVOID InterlockedExchange (); LONG InterlockedExchangeAdd (); LONG InterlockedCompareExchange (); PVOID InterlockedCompareExchangePointer (); Подробнее об этих и других функциях вы можете почитать на сайте msdn.com. Важно знать, что Interlocked-функции выполняются чрезвычайно быстро. Синхронизация в режиме ядра. Режим ядра (kernel mode) имеет доступ ко всему оборудованию и памяти. Механизмы синхронизации в данном режиме обладают значительными преимуществами по сравнению с пользовательскими, однако обеспечивают меньшее быстродействие. Большинство объектов ядра характеризуются своим состоянием: занятым или свободным. Очень часто для работы с объектом нужно определить в каком из состояний он находится и, в зависимости от состояния, выполнять те или иные действия. Wait-функции. Wait-функции (wait functions) позволяют потоку останавливаться и дожидаться освобождения объекта ядра, когда он занят. Если же объект свободен, то поток не остановится, а продолжит свою работу. Code: DWORD WaitForSingleObject (); DWORD WaitForMultipleObjects (); Первая функция принимает в качестве аргементов дескриптор объекта и таймаут. В случае, если объект свободен, она возвращает WAIT_OBJECT_0. Если время ожидания освобождения истекло - WAIT_TIMEOUT. Если допущена ошибка в вызове функции - WAIT_FAILED. Вторая функция аналогична первой, но позволяет ждать освобождения сразу нескольких объектов. Принимает в качестве аргументов количество объектов, указатель на массив объектов, параметр, определяющий будет ли поток ждать освобождения всех объектов или же лишь одного, а также таймаут. Если функции указано ждать освобождения одного объекта, то в случае его освобождения она вернет WAIT_OBJECT_0 + dwCount - 1 (dwCount - количество объектов), то есть для получения индекса объекта нужно из полученного от функции значения вычесть WAIT_OBJECT_0. Пример работы wait-функций: Code: HANDLE h[3]; h[0] = hProcess1; h[1] = hProcess2; h[2] = hProcess3; DWORD dw = WaitForMultipleObjects(3, h, FALSE, 5000); sitch(dw) { case WAIT_FAILED: // Неверный вызов функции break; case WAIT_TIMEOUT: // Ни один из объектов не освободился в течение 5000 мс break; case WAIT_OBJECT_0 + 0: // Завершился процесс h[0]; break; case WAIT_OBJECT_0 + 1: // Завершился процесс h[1]; break; case WAIT_OBJECT_0 + 2: // Завершился процесс h[2]; break; } События. Событие (event) - это объект синхронизации, сигнализирующий об окончании какой-либо операции. События обычно используются, когда после выполнения какого-то действия один поток должен сообщить об этом другому. Создается объект функцией CreateEvent() (или ex-версия). В сигнальное положение может быть приведен функциями SetEvent() и PulseEvent(). Объекты-события могут разделяться разными процессами. Потоки могут получать доступ к уже созданному объекту несколькими путями: вызовом функции CreateEvent(), передав в качестве параметра имени уже имя созданного объекта, наследованием дескриптора, применением функции DuplicateHandle(), вызовом функции OpenEvent() с передачей в параметре имени такого же имени. События делятся на два типа: с ручным сбросом и с автоматическим сбросом. Сигнальное состояние первых объектов сохраняется до вызова функции ResetEvent(). Сигнальное состояние объектов второго типа сохраняется до тех пор, пока не будет освобожден единственный поток, после чего система устанавливает не сигнальное состояние объекта. Если нет потоков, ожидающих этого события, объект остается в сигнальном состоянии. Пример использования событий: Code: HANDLE hEvent; int main() { hEvent = CreateEvent (NULL, TRUE, FALSE, NULL); HANDLE hThread[3]; DWORD dwThreadID; hThread[0] = _beginthreadex(NULL, 0, Func0, NULL, 0, &dwThreadID); hThread[1] = _beginthreadex(NULL, 0, Func1, NULL, 0, &dwThreadID); hThread[2] = _beginthreadex(NULL, 0, Func2, NULL, 0, &dwThreadID); SetEvent(hEvent); ... } DWORD WINAPI Func0 (PVOID pvParam) { WaitForSingleObject (hEvent, INFINITE); // Делаем свои черные дела return 0; } DWORD WINAPI Func1 (PVOID pvParam) { WaitForSingleObject (hEvent, INFINITE); // Делаем свои черные дела return 0; } DWORD WINAPI Func2 (PVOID pvParam) { WaitForSingleObject (hEvent, INFINITE); // Делаем свои черные дела return 0; } Ожидаемые таймеры. Ожидаемые таймеры (waitable timers) - это объекты ядра, которые самостоятельно переходят в свободное положение через определенное время или через регулярные промежутки времени. Создается таймер функцией CreateWaitableTimer(). Настройка таймера происходит по средством вызова функции SetWaitableTimer(). Объект переходит в сигнальное состояние по истечении таймаута. Отмену можно произвести функцией CancelWaitableTimer(). По аналогии с событиями таймеры тоже бывают со сбросом вручную и авто сбросом. Пример использования ожидаемых таймеров: Code: int main() { HANDLE hTimer = NULL; LARGE_INTEGER liDueTime; liDueTime.QuadPart = -100000000LL; hTimer = CreateWaitableTimer(NULL, TRUE, NULL); printf("Waiting for 10 seconds...\n"); if (WaitForSingleObject(hTimer, INFINITE) != WAIT_OBJECT_0) printf("WaitForSingleObject failed (%d)\n", GetLastError()); else printf("Timer was signaled.\n"); return 0; } Семафоры. Семафоры (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 дает большое понимание что и как делается). Из этого следует что эти функции представляют из себя не набор команд ОС, а обычные машинные инструкции. К примеру InterlockedIncrement в своей основе использует инструкцию xadd с префиксом блокировки lock. Как показал дизасем выглядит это примерно так: Code: mov ecx, [esp+4] mov eax,1 lock xadd [ecx], eax inc eax ret 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 в расшаренном доступе. И через это общаться )
не могу согласиться, т.к. планирование выполняется на уровне потоков. Upd: Хотя... я не уверен. Впрочем неважно
если быть совсем-совсем точным, термин "задача" лучше употреблять к процессору. С этой точки зрения задачами вообще являются потоки. http://dims.karelia.ru/x86/multitask.shtml Ещё кстати можно было бы про фиберы упомянуть. А вообще получился довольно неплохой алфавитный указатель к мсдн-у
Спасибо ) Насчет msdn'a - оттуда информации я почти не брал, один пример только по-моему. Насчет многопоточности и -задачности - так как задачи выполняют потоки, я думаю, сложно четко разделить эти понятия. Однако, так как в статье идет речь не только и не столько о потоках, а также еще много о чем, хоть и основная часть про синхронизацию, я решил, что это все таки многозадачность = ) Вот такое сложное предложение.. 6-ое место в google по запросу "Многозадачность в Windows"
извенюсь за флуд, но анекдот в тему. - Папа, папа! А это правда что ты написал новый многозадачный Windows? - Правда сынок. - А как это? - Сейчас, подожди, дискету отформатирую и покажу.