Общие сведения о потоках. Создание потоков
Потоки создаются в процессах, и определяют порядок выполнения программного кода в адресном пространстве процесса.
Каждый поток включает в себя следующие понятия:
1. Объект ядра типа «поток», с помощью которого ОС управляет потоком, и в котором хранятся статистические данные потока;
2. Стек потока, в котором хранятся параметры всех функций, процедур, а также локальные переменные, необходимые для выполнения программной части потока.
Потоки, в отличие от процессов, используют значительно меньше системных ресурсов. Это связано, прежде всего, с тем, что образование потоков, в отличие от образования процесса, не связано с выделением области ВАП, на что требуются дополнительные системные ресурсы. В частности, много памяти при создании процессов расходуется на хранение статистических данных о процессах, файловых ресурсах для загрузки .exe и .dll-файлов. Для потока необходимы лишь ресурсы для создания объекта ядра и стека. Объем статистических сведений для потока гораздо меньше.
При инициализации процесса, в системе всегда создается первичный поток. В ходе выполнения программы главной функции этого потока могут создаваться новые потоки, которые начинают выполняться в режиме мультипрограммирования, а точнее – в режиме вытесняющей многозадачности. Любой новый поток создается путем вызова в первичном потоке процесса, либо во вновь созданном потоке системной функции создания потока функции CreateThread (p1..p6). При вызове этой функции система создает объект ядра типа «поток», который представляет собой системную структуру данных, хранящую статистическую информацию о потоке и используемую для управления им. Кроме того, для нового потока в ВАП процесса выделяется память для его индивидуального стека. Новый поток выполняется в рамках того же процесса, что и родительский поток. Это означает, что вновь созданный поток имеет доступ ко всем описателям объектов ядра и стекам других потоков в данном процессе. За счет этого, потоки одного процесса могут взаимодействовать друг с другом.
Функция CreateThread является системной функцией, входящей в WinAPI. В ряде систем программирования, для реализации действий этой функции используются эквивалентные ей средства. Например, в системе Delphi используется TThread.
Смысл параметров функции создания потока:
1. Указатель на системную структуру данных, в которой хранятся атрибуты защиты объектов ядра потока;
2. Задает размеры стека потока. После создания процесса, этот параметр принимает начальное значение, которое хранится в исполняемом файле программы первичного потока. Можно управлять этим значением с помощью ключа компоновщика /STACK: arg1, arg2. Первый параметр определяет объем памяти, который система должна зарезервировать под стек, а по умолчанию – один мегабайт ВАП. Второй параметр задает объем физической памяти, которая изначально передается под стек. По умолчанию – одна страница памяти (x32 – 4kb, x64 – 8kb). В ходе выполнения программы потока может возникнуть потребность в увеличении стека. Если возникает переполнение стека, то генерируется прерывание, обрабатывая которое, система передает для зарезервированного стека столько страниц физической памяти, сколько указано в параметре arg2. В результате, размер стека может динамически изменяться;
3. Определяет адрес функции, с которого начинается выполнение программы создаваемого потока;
4. Служит для передачи в функцию потока какого-либо инициализирующего значения – константы или указателя на что-либо еще;
5. Представляет собой флаг, управляющий запуском потока на выполнение. Состояние «0» этого флага соответствует случаю немедленного начала исполнения потока. Состояние «1» означает, что поток создается, инициализируется, но начало его исполнения задерживается до определенного момента;
6. Это адрес переменной, в которой функция создания потока возвращает идентификатор, назначаемый системой новому потоку.
Выполнение потоков.
Последовательность действий системы в момент создания и инициализации потока, предшествующий началу выполнения, можно пояснить следующей иллюстрацией:
В результате выполнения функции CreateThread создается объект ядра «поток». Одновременно, инициализируются основные параметры этого объекта ядра, а именно – счетчику числа пользователей присваивается 2, счетчику числа простоев – 1, коду завершения – 0x103, а состояние объекта устанавливается в «занято». Стеку потока из ВАП процесса выделяется память, и в старшие адреса записываются значения параметров P3, P4 функции создания. У любого вновь создаваемого потока имеется собственный набор используемых регистров процессора, называемый контекстом потока. Контекст отражает состояние регистров процессора на момент последнего выполнения потока. Контекст сохраняется в системной структуре данных CONTEXT, которая содержится в объекте ядра «поток». В контекст входят значения регистров указателя команд EIP и состояние указателя стека ESP. В ходе выполнения команд программы потока, в них формируются адреса памяти из области ВАП всего процесса. В момент инициализации объекта ядра «поток» указателю стека ESP присваивается адрес, по которому в стек заносится значение параметра P3, а указателю команд EIP присваивается адрес системной функции BaseThreadStart ().
Состояния этих регистров заносится в CONTEXT. После инициализации потока, система проверяет значение флага создания потока из параметра P5. Если это значение равно нулю, выполнение потока может быть начато немедленно. В этом случае, счетчик числа простоев обнуляется, в регистры процессора загружаются значения, которые хранились в структуре CONTEXT потока. После этого, поток переходит в состояние готовности к выполнению, то есть ему может быть предоставлено процессорное время, а точнее – очередной квант процессорного времени. Когда это происходит, то начинается выполнение программы потока. Так как EIP указывает на BaseThreadStart (), то выполнение потока начинается не с первой команды его программы, а именно с этой функции. Эта функция получает доступ к двум параметрам P3, P4, записанным в стеке потока, и в ходе своего выполнения реализует следующие действия:
1. Основная функция потока включается в область структурной обработки исключений, в результате чего любое исключение, если оно возникнет в ходе выполнения программы потока, будет обрабатываться системой по умолчанию;
2. Вызывается прикладная функция потока, и ей, в качестве параметра, передается значение P4;
3. Начинается выполнение программного кода основной функции потока.
По окончании выполнения этой функции, она снова возвращает управление в системную функцию BaseThreadStart (). Эта функция, в свою очередь, вызывает функцию завершения потока ExitThread(). В нее передается значение кода возврата, возвращаемое прикладной функцией потока. Одновременно, значение счетчика числа пользователей объекта ядра «поток» уменьшается на единицу, и выполнение потока прекращается.
Инициализация самого первого (первичного) потока процесса имеет некоторые особенности. Его указатель команд настраивается по адресу другой системной функции – BaseProcessStart (). Она, отчасти, аналогична функции запуска потока, но отличие состоит в том, что данная функция не использует параметр P3 из общего числа параметров. Вместо этого, функция BaseProcessStart () обращается к стартовой функции библиотеки системы программирования, которая выполняет необходимую инициализацию, а затем уже обращается к основной функции первичного потока.
Завершение потоков.
Любой поток может быть завершен одним из следующих 4 способов:
1) естественный. Функция потока выполняется до конца, и возвращает управление в ОС;
2) поток самоуничтожается путем вызова в нем ExitThread ();
3) любой другой поток данного или иного процесса вызывает функцию уничтожения потока TerminateThread ();
4) завершение процесса, содержащего данный поток.
Наиболее корректным для системы является первый способ. В этом случае, выполняются следующие системные действия:
1) все объекты, созданные в программе потока, уничтожаются соответствующими деструкторами;
2) система корректно освобождает память, которую занимал стек потока;
3) система устанавливает код завершения данного потока, который возвращается из прикладной функции потока (из программы потока);
4) счетчик числа пользователей объекта ядра «поток» уменьшается на единицу.
Во втором способе, вызывается в потоке и принудительно исполняется функция ExitThread (). При этом, освобождаются все системные ресурсы, выделенные потоку, в частности, стек. Но все объектные ресурсы, созданные в программе потока, не уничтожаются. В функцию ExitThread передается значение, которое рассматривается системой, как код завершения всего потока.
В третьем способе, в любом другом потоке процесса выполняется функция TerminateThread. Она имеет два параметра. В первом параметре задается дескриптор завершаемого потока, а второй параметр интерпретируется как код завершения для уничтожаемого потока. Особенность исполнения этой функции состоит в том, что её вызов оказывается внезапным для потока, и в следствие этого, в потоке не могут быть выполнены все действия для корректного освобождения системных и объектных ресурсов.
В первых двух способах завершения стек уничтожается. В третьем способе стек сохраняется до момента завершения процесса, которому принадлежит уничтожаемый поток. Это сделано для того, чтобы другие потоки могли использовать указатели, ссылающиеся на данные в стеке завершаемого потока. В любом случае, некорректное освобождение ресурсов может привести к ошибке.
Четвертый способ. Поток завершается в результате окончания всего процесса. В этом случае, выполняется функция ExitProcess () или TerminateProcess (). В результате, выполняется освобождение выделенных для процесса ресурсов, в том числе и освобождение стеков всех потоков. Однако, при этом деструкторы созданных объектов не вызываются, и промежуточные данные не переписываются на диск для сохранения, и следовательно, завершение потока выполняется некорректно.
В любом из четырех рассмотренных случаев, в системе дополнительно реализуются дополнительные операции:
1) освобождаются все дескрипторы интерфейсных объектов, которые принадлежали потоку, то есть окна, меню, значки, шрифты, курсоры, и т.д.;
2) код завершения потока меняется с исходного значения 0x103 на действительное, которое вырабатывается в системе;
3) объект ядра «поток» из состояния «занято» переводится в состояние «свободно», но при этом, объект полностью не освобождается до тех пор, пока на него не будут закрыты все внешние ссылки;
4) счетчик числа пользователей объекта ядра «поток» уменьшается на единицу;
5) если данный поток был самым последним активным потоком процесса, то завершается и сам процесс.
Планирование потоков. Приостановка и возобновление процессов и потоков.
В ОС Windows используется вариант мультипрограммного режима работы, называемый вытесняющей многозадачностью. Для её реализации, в системе используется алгоритм, позволяющий распределять процессорное время между потоками.
В версиях системы Windows 2000 и далее примерно каждые 18мс в системе просматриваются все существующие объекты ядра типа «поток», и отмечаются те из них, которые готовы получить процессорное время. Далее, в соответствии с принятым алгоритмом, выбирается один из них, и из его контекста загружаются регистры процессора. Данная операция называется переключением контекста потока. Затем, в течение интервала квантования, программа потока выполняется в адресном пространстве своего процесса. По окончании этого интервала, система сохраняет состояния регистров процессора в структуре CONTEXT данного потока, и приостанавливает его выполнение. Затем снова просматриваются все объекты ядра типа поток, выбирается один из них на основе алгоритма дисциплины распределения процессорного времени, в процессор загружается его контекст, и начинается выполнение программы этого потока, либо продолжение выполнения программы ранее прерванного потока.
Подобный цикл операций, включающий:
1) выбор потока для запуска;
2) загрузку его контекста;
3) исполнение программы потока в процессоре в течение интервала квантования;
4) сохранение контекста;
начинается с момента запуска системы вплоть до её выключения.
Из приведенного общего описания алгоритма распределения процессорного времени между потоками следует, что в режиме вытесняющей многозадачности в системе в каждый момент времени присутствуют потоки следующих типов:
1) выполняющиеся в процессоре;
2) планируемые к выполнению;
3) приостановленные системой;
4) ожидающие возникновения каких-либо событий.
В объекте ядра «поток» имеется системная переменная SUSPENDCOUNT, которая называется счетчиком числа простоев данного потока. Если его значение равно нулю, то соответствующий поток приостанавливается, и не получает процессорного времени. При вызове функций CreateProcess () и CreateThread () эта переменная инициализируется единицей, которая запрещает системе немедленно выделять новому потоку процессорное время, поскольку требуется его инициализация. После выполнения инициализации проверяется состояние одного из флагов функции создания потока CreateThread, а именно P5. Если он равен единице, то поток оставляется в приостановленном состоянии. Если этот флаг сброшен, то SUSPENDCOUNT обнуляется, и поток включается в число планируемых к выполнению в процессоре.
Новый поток может быть изначально создан как приостановленный, если упомянутый флаг имеет исходное значение единицы. В этом случае, в системе имеется возможность изменить какие-либо свойства потока, например, его приоритет. После подобной настройки потока, ему может быть разрешено выполнение в процессоре. В этом случае, реализуется процедура возобновления потока. С этой целью в системе используется функция ResumeThread (), в которую в качестве параметра передается дескриптор потока, возвращаемый функцией создания потока. Если поток ожидает наступления какого-либо внешнего события, например, ввода данных с клавиатуры, то он будет находиться в этом состоянии, называемом состоянием блокирования, до момента возникновения этого события. Затем поток снова включается в число планируемых к выполнению.
Поток может быть приостановлен не только в момент его создания, но и в ходе свого выполнения. Для этого используется системная функция SuspendThread (). Параметром функции является описатель приостанавливаемого потока. Эта функция может быть вызвана из любого другого потока, если известен его описатель. Поток может быть приостановлен не более чем устанавливаемое в системе количество раз. С помощью функции Sleep () поток может запросить у системы возможность не выделять ему процессорное время на определенный период времени, то есть запросить возможность приостановки самого себя. Параметр – период приостановки в миллисекундах. После её выполнения поток приостанавливается, и у него отбирается остаток выделенного ему кванта процессорного времени. По окончании периода приостановки, система переводит поток в число планируемых, после чего он продолжает свое выполнение.
В системе имеется также функция SwitchToThread (), с помощью которой система может подключить процессору новый поток, если он имеется. При вызове этой функции, система проверяет, присутствует ли поток, которому хронически не хватает процессорного времени («голодание потока»). Если такого потока нет, то данная функция просто завершается. Если подобный поток обнаруживается, то ему выделяется дополнительный квант времени. При этом, с помощью данной функции может быть выделено время потоку с более низким приоритетом, чем потоки, которые до этого выполнялись в процессоре.
В системах Windows, понятия, о которых здесь шла речь, а именно – приостановка и возобновление, неприменимы к процессам, поскольку процессы в распределении процессорного времени непосредственно не участвуют. Существует лишь одна возможность приостановить одновременно все потоки какого-либо процесса. Это можно сделать из другого процесса, который является отладочным.