- •8.1. Общие сведения о потоках
- •8.2. Синхронизация потоков
- •При работе с критическими секциями всегда необходимо следить за порядком вхождения в критические секции, чтобы избегать ситуации взаимной блокировки.
- •8.2.1. Синхронизация потоков объектами ядра
- •8.2.2. Функции ожидания объектов ядра (wait - функции)
Лекция 10. Потоки в операційній системі Windows. Засоби синхронізації потоків. Засоби роботи з потоками. Об’єкт TThread систем Delphi та C++ Builder для роботи з потоками. Функції API Windows для роботи з потоками. Локальна пам’ять потоків.
8.1. Общие сведения о потоках
При создании процесса в системе появляется новый программный поток, принадлежащий этому процессу. В начале любой вновь созданный процесс обладает лишь одним потоком. Этот поток может создавать новые потоки, а эти новые потоки, в свою очередь, могут создавать другие новые потоки. Процесс продолжает свое существование до тех пор, пока в его владении находится, по крайней мере, один программный поток.
Потоки могут выполнять какие-либо действия в фоновом режиме относительно основной программы. Потоки удобно использовать также в случае, если блокирование или подвисание какой-либо процедуры не должно стать причиной нарушений функционирования основной программы. Например, в то время, как основная программа выполняет какие-либо операции с базой данных, отдельный программный поток может осуществлять обмен данными через канал связи, например, через модем. В случае замедления передачи данных через канал связи или в случае подвисания модема функционирование основной программы не будет нарушено.
Как уже упоминалось выше, реализация многопоточности основывается на предоставлении каждому потоку определенного времени для работы, т.е. каждый поток получает в свое распоряжение процессор на некоторое время. Такой подход приводит к тому, что многопоточное приложение может работать медленнее, чем однопоточное, но в тоже время использование потоков может быть чуть ли не единственным решением во многих задачах. В первую очередь потоки находят применение при разработке коммуникационных программ и программ для различных вычислений.
Любой поток определяет последовательность исполнения кода в процессе и состоит из двух компонентов:
объект ядра, через который операционная система управляет потоком, и в котором содержится информация о потоке.
стек потока, который содержит параметры всех функций и локальные переменные, необходимые потоку для выполнения кода.
Любой поток принадлежит какому-либо процессу, который ничего не выполняет, а является контейнером (адресным пространством) для потоков. Потоки всегда создаются в контексте какого-либо процесса, и вся их жизнь проходит в его границах (адресном пространстве). Два и более потока, созданные в контексте одного процесса разделяют одно адресное пространство. Потоки могут исполнять один и тот же код и манипулировать одними и теми же данными, а так же совместно использовать описатели объектов ядра, поскольку таблица описателей принадлежит процессу, а не потоку. Потоку требуется объект ядра и стек, объём статических сведений о потоке невелик и занимает немного памяти, в отличие от процесса, который требует значительных системных ресурсов (в адресное виртуальное пространство процесса загружаются DLL и EXE файлы). Рекомендуется избегать создания новых процессов и решать необходимые задачи с помощью потоков.
При инициализации процесса система всегда создаёт первичный поток, функцией которого является входная функция (WinMain, wWinMain, main, wmain). Все остальные потоки будут дочерними по отношению к первичному потоку, и будут иметь более низкий приоритет. Если завершить первичный поток, то будут автоматически закрыты и дескрипторы дочерних потоков, следовательно, процесс будет закрыт. Любой дочерний поток имеет функцию вида:
DWORD WINAPI ThreadFuncName( PVOID pvParam )
{
DWORD dwResult = 0;
// . . .
return (dwResult);
}
Функция потока может выполнять любые задачи, поставленные перед потоком, по завершении которых она вернёт управление вызывавшему ее потоку, поток остановится, память, отведённая под его стек будет освобождена, поток будет разрушен, счётчик пользователей объекта ядра уменьшается на 1. Функция дочернего потока получает единственный параметр, представляющий собой указатель на любой тип данных. Функция потока должна возвращать значение, которое будет использоваться как код завершения потока. Функциям потоков рекомендуется по мере возможности обходиться локальными переменными (они создаются в стеке потока) или использовать средства синхронизации потоков.
Иногда удобно использовать некоторую область памяти, которая относится к конкретному потоку, но не является локальной переменной. Предположим, разрабатывается некоторое многопоточное приложение, в котором каждый поток обращается к одной и той же функции. При этом необходимо ограничить количество обращений к этой функции из потоков, например не более 20 обращений из каждого потока. Для организации такого счетчика глобальная или локальная переменные не подойдут.
Для решения этой проблемы можно сделать так, чтобы в каждом потоке выделить специальную область памяти, принадлежащую только ему и каждый раз при обращении к функции передавать этой функции указатель на эту область в качестве аргумента. Этот способ подойдет для решения описанной проблемы, но если потребуется несколько подобных переменных, то могут возникнуть другие проблемы.
Рассмотрим решение, которое можно реализовать, используя возможности операционной системы. Предположим, что есть таблица с двумя колонками, таким образом, каждая запись имеет два поля: в первом поле содержится идентификатор ID-потока, а во втором — 32-битное число. Каждый раз при обращении потока к функции в этой функции определяется ID обратившегося потока при помощи функции GetCurrentThreadID и осуществляется его поиск в таблице. Если запись с таким идентификатором есть, то функция использует эту запись, в противном случае в таблицу вносится новая запись, содержащая ID текущего потока. Таким образом, получается таблица, принадлежащая текущему потоку, во втором поле этой записи можно хранить счетчик обращения данным потоком к функции.
По описанному выше принципу работает локальная память потоков (Thread Local Storage, TLS). Для каждого из процессов создается набор внутренних таблиц. Windows может создать для каждого процесса до 64 таких таблиц, таким образом можно использовать 64 различные переменные TLS. При обращении к TlsAlloc Windows выделяет одну таблицу TLS. После этого можно использовать функции TlsGetValue и TlsSetValue для того, чтобы установить или прочитать значение из таблицы. При этом операционная система обращается именно к той записи в таблице, которая соответствует текущему потоку. Если таблица больше не нужна, то можно обратиться к функции TlsFree для того, чтобы освободить ее. В каждой записи таблицы можно хранить любое 32-битное число, однако, чаще всего таблица TLS используется для хранения указателей на класс или структуру, содержащую все необходимые для потока переменные.
В последних версиях ОС Windows появилось такое понятие как нить. Преобразовать поток в нить можно при помощи функции ConvertThreadToFiber. Каждый поток может обладать несколькими нитями. Когда система переключается с одного потока на выполнение другого, начинается выполнение текущей нити для данного потока. Если в потоке только одна нить, то код, который ранее был частью потока, стал частью нити. Однако если в потоке несколько нитей, то будет возможность управлять переключением между ними при помощи функции SwitchToFiber. Создать новую нить в текущем потоке можно при помощи функции CreateFiber.