Класс потока в Delphi
Раптор [Ментальная студия]
http://mental.mentsu.com
Четыре
Критический раздел (CriticalSection) — технология защиты общего доступа к данным. Фактически это эквивалент глобальной логической переменной. Но его работа отличается. Он имеет только две операции: Enter и Leave. Два его состояния также можно рассматривать как True и False, что указывает соответственно на то, находится ли он в критическом разделе. Эти две операции также являются примитивами, поэтому их можно использовать для защиты общих данных от нарушений доступа в многопоточных приложениях.
Метод использования критических секций для защиты общих данных очень прост: перед каждым доступом к общим данным вызывайте Enter, чтобы установить флаг критической секции, затем оперируйте данными и, наконец, вызывайте Leave, чтобы покинуть критическую секцию. Принцип его защиты следующий: после входа потока в критическую секцию, если другой поток в это время также захочет получить доступ к данным, он при вызове Enter обнаружит, что поток уже вошел в критическую секцию, и тогда этот поток будет Повесьте трубку и подождите, пока поток, находящийся в данный момент в критическом разделе, вызовет Leave, чтобы покинуть критический раздел. Когда другой поток завершит операцию и вызовет Leave, чтобы выйти, этот поток будет разбужен, установит флаг критического раздела и начнет обрабатывать данные. тем самым предотвращая конфликт доступа.
Взяв в качестве примера предыдущий InterlockedIncrement, мы используем CriticalSection (Windows API) для его реализации:
Вар
InterlockedCrit: TRTLCriticalSection;
PROcedure InterlockedIncrement(var aValue: Integer);
Начинать
EnterCriticalSection(InterlockedCrit);
Inc(значение);
LeaveCriticalSection(InterlockedCrit);
Конец;
Теперь посмотрим на предыдущий пример:
1. Поток A входит в критическую секцию (при условии, что данные равны 3).
2. Поток B входит в критическую секцию. Поскольку поток A уже находится в критическом разделе, поток B приостанавливается.
3. Поток A добавляет единицу к данным (теперь 4)
4. Поток A покидает критическую секцию и пробуждает поток B (текущие данные в памяти — 4)
5. Поток B просыпается и добавляет единицу к данным (теперь это 5)
6. Поток B покидает критическую секцию, и текущие данные верны.
Именно так критические разделы защищают доступ к общим данным.
Что касается использования критических секций, следует отметить обработку исключений во время доступа к данным. Потому что, если во время операции с данными произойдет исключение, операция Leave не будет выполнена. В результате поток, который должен быть разбужен, не будет пробуждён, что может привести к тому, что программа перестанет отвечать на запросы. Вообще говоря, правильный подход — использовать критические секции следующим образом:
EnterCriticalSection
Пытаться
//Работа с данными критической секции
Окончательно
Выход из критического раздела
Конец;
Последнее, что следует отметить, это то, что Event и CriticalSection являются ресурсами операционной системы, которые необходимо создать перед использованием и освободить после использования. Например, глобальное событие Event: SyncEvent и глобальное CriticalSection: TheadLock, используемые классом TThread, создаются и выпускаются в InitThreadSynchronization и DoneThreadSynchronization, и они вызываются в модуле «Инициализация и завершение классов».
Поскольку API используются для работы с Event и CriticalSection в TThread, API используется в качестве примера выше. Фактически, Delphi предоставила их инкапсуляцию. В модуле SyncObjs это класс TEvent и TCriticalSection соответственно. Использование почти такое же, как и предыдущий метод использования API. Поскольку конструктор TEvent имеет слишком много параметров, для простоты Delphi также предоставляет класс Event, инициализированный параметрами по умолчанию: TSimpleEvent.
Кстати, позвольте мне представить еще один класс, используемый для синхронизации потоков: TMultiReadExclusiveWriteSynchronizer, который определен в модуле SysUtils. Насколько мне известно, это самое длинное имя класса, определенное в Delphi RTL. К счастью, у него есть короткий псевдоним: TMREWSync. Что касается его использования, я думаю, вы можете узнать это, просто взглянув на название, поэтому больше не скажу.
Имея предварительные знания о Event и CriticalSection, мы можем официально начать обсуждение Synchronize и WaitFor.
Мы знаем, что Synchronize обеспечивает синхронизацию потоков, помещая часть кода для выполнения в основной поток, поскольку в процессе существует только один основной поток. Давайте сначала посмотрим на реализацию Synchronize:
процедура TThread.Synchronize(Метод: TThreadMethod);
начинать
FSynchronize.FThread := Self;
FSynchronize.FSynchronizeException: = ноль;
FSynchronize.FMethod := Метод;
Синхронизировать (@FSynchronize);
конец;
где FSynchronize — тип записи:
PSynchronizeRecord = ^TSynchronizeRecord;
TSynchronizeRecord = запись
FThread: TObject;
ФМетод: ТСреадМетод;
FSynchronizeException: TObject;
конец;
Используется для обмена данными между потоками и основным потоком, включая входящие объекты класса потока, методы синхронизации и возникающие исключения.
Его перегруженная версия вызывается в Synchronize, и эта перегруженная версия совершенно особенная, это «метод класса». Так называемый метод класса — это специальный метод-член класса. Его вызов не требует создания экземпляра класса, а вызывается через имя класса, как конструктор. Причина, по которой он реализован с использованием метода класса, заключается в том, что его можно вызвать, даже если объект потока не создан. Однако на практике используется другая его перегруженная версия (тоже метод класса) и другой метод класса StaticSynchronize. Вот код для этой синхронизации:
процедура класса TThread.Synchronize(ASyncRec: PSynchronizeRecord);
вар
СинкПрок: ТсинкПрок;
начинать
если GetCurrentThreadID = MainThreadID, то
ASyncRec.FMethod
еще
начинать
SyncProc.Signal := CreateEvent(ноль, Истина, Ложь, ноль);
пытаться
EnterCriticalSection (ThreadLock);
пытаться
если SyncList = ноль, то
SyncList := TList.Create;
SyncProc.SyncRec := ASyncRec;
SyncList.Add(@SyncProc);
СигналСинкСобытие;
если назначено (WakeMainThread), то
WakeMainThread(SyncProc.SyncRec.FThread);
LeaveCriticalSection (ThreadLock);
пытаться
WaitForSingleObject(SyncProc.Signal, INFINITE);
окончательно
EnterCriticalSection (ThreadLock);
конец;
окончательно
LeaveCriticalSection (ThreadLock);
конец;
окончательно
CloseHandle(SyncProc.Signal);
конец;
если назначено (ASyncRec.FSynchronizeException), то вызовите исключение ASyncRec.FSynchronizeException;
конец;
конец;
Этот код немного длиннее, но он не слишком сложен.
Первый — определить, является ли текущий поток основным. Если да, просто выполните метод синхронизации и вернитесь.
Если это не основной поток, он готов начать процесс синхронизации.
Данные обмена потоками (параметры) и дескриптор событий записываются через локальную переменную SyncProc. Структура записи следующая:
TSyncProc=запись
Синкрек: Псинхронизерекорд;
Сигнал: РУЧКА;
конец;
Затем создайте событие, затем войдите в критическую секцию (через глобальную переменную ThreadLock, поскольку только один поток может войти в состояние синхронизации одновременно, поэтому вы можете использовать глобальную переменную для записи), а затем сохраните записанные данные в Список SyncList (если этот список не существует, создайте его). Видно, что критический раздел ThreadLock предназначен для защиты доступа к SyncList. Это снова будет видно, когда CheckSynchronize будет представлен позже.
Следующий шаг — вызов SignalSyncEvent. Его код был представлен ранее при представлении конструктора TThread. Его функция — просто выполнить операцию Set для SyncEvent. Назначение этого SyncEvent будет подробно описано позже, когда будет представлен WaitFor.
Дальше самая важная часть: вызов события WakeMainThread для операций синхронизации. WakeMainThread — это глобальное событие типа TNotifyEvent. Причина, по которой здесь для обработки используются события, заключается в том, что метод Synchronize по сути помещает процесс, который необходимо синхронизировать, в основной поток для выполнения через сообщения. Его нельзя использовать в некоторых приложениях без циклов обработки сообщений (например, в консоли или DLL). , поэтому используйте это событие для обработки.
Объект приложения реагирует на это событие. Следующие два метода используются для установки и очистки ответа на событие WakeMainThread (из модуля Forms):
процедура TApplication.HookSynchronizeWakeup;
начинать
Classes.WakeMainThread := WakeMainThread;
конец;
процедура TApplication.UnhookSynchronizeWakeup;
начинать
Classes.WakeMainThread:= ноль;
конец;
Два вышеуказанных метода вызываются в конструкторе и деструкторе класса TApplication соответственно.
Это код, который отвечает на событие WakeMainThread в объекте приложения. Здесь отправляется сообщение. Для этого используется пустое сообщение:
процедура TApplication.WakeMainThread(Отправитель: TObject);
начинать
PostMessage (Дескриптор, WM_NULL, 0, 0);
конец;
Ответ на это сообщение также находится в объекте Application, см. следующий код (удалите ненужные части):
процедура TApplication.WndProc(var Сообщение: TMessage);
…
начинать
пытаться
…
с сообщением сделать
случай Сообщение о
…
WM_NULL:
Проверить синхронизацию;
…
кроме
HandleException(Self);
конец;
конец;
Среди них CheckSynchronize также определен в модуле «Классы». Поскольку он относительно сложен, мы пока не будем объяснять его подробно. Просто знайте, что это та часть, которая конкретно обрабатывает функцию Synchronize. код.
После выполнения события WakeMainThread выйдите из критического раздела, а затем вызовите WaitForSingleObject, чтобы начать ожидание события, созданного перед входом в критический раздел. Функция этого события — дождаться завершения выполнения этого метода синхронизации. Это будет объяснено позже при анализе CheckSynchronize.
Обратите внимание, что после WaitForSingleObject вы повторно заходите в критическую секцию, но выходите, ничего не делая. Это кажется бессмысленным, но это необходимо!
Потому что Enter и Leave в критической секции должны строго соответствовать один к одному. Так можно ли это изменить на это:
если назначено (WakeMainThread), то
WakeMainThread(SyncProc.SyncRec.FThread);
WaitForSingleObject(SyncProc.Signal, INFINITE);
окончательно
LeaveCriticalSection (ThreadLock);
конец;
Самая большая разница между приведенным выше кодом и исходным кодом заключается в том, что WaitForSingleObject также включен в ограничения критического раздела. Вроде бы никакого влияния не оказывает и сильно упрощает код, но возможно ли это на самом деле?
На самом деле нет!
Потому что мы знаем, что после входа в критическую секцию, если другие потоки захотят войти снова, они будут приостановлены. Метод WaitFor приостанавливает текущий поток и не пробуждается до тех пор, пока не дождется SetEvent других потоков. Если код изменить на приведенный выше, если потоку SetEvent также необходимо войти в критическую секцию, произойдет взаимоблокировка (теорию взаимоблокировок см. в информации о принципах работы операционной системы).
Взаимная блокировка — один из наиболее важных аспектов синхронизации потоков!
Наконец, событие, созданное вначале, высвобождается. Если синхронизированный метод возвращает исключение, исключение будет создано здесь снова.
(продолжение следует)