Класс потока в Delphi
Раптор [Ментальная студия]
http://mental.mentsu.com
Часть 2
Первый — это конструктор:
конструктор TThread.Create(CreateSuspended: Boolean);
начинать
унаследовано Создать;
АддПоток;
FSuspended := CreateSuspended;
FCreateSuspended := CreateSuspended;
FHandle:= BeginThread(nil, 0, @ThreadPROc, Pointer(Self), CREATE_SUSPENDED, FThreadID);
если FHandle = 0, то
поднять EThread.CreateResFmt(@SThreadCreateError, [SysErrorMessage(GetLastError)]);
конец;
Хотя в этом конструкторе не так много кода, его можно рассматривать как наиболее важный член, поскольку здесь создается поток.
После вызова TObject.Create через Inherited первое предложение — это вызов процесса: AddThread, а его исходный код выглядит следующим образом:
процедура AddThread;
начинать
InterlockedIncrement(ThreadCount);
конец;
Также существует соответствующий RemoveThread:
процедура RemoveThread;
начинать
InterlockedDecrement(ThreadCount);
конец;
Их функция очень проста: подсчет количества потоков в процессе путем увеличения или уменьшения глобальной переменной. Просто для увеличения или уменьшения переменных здесь не используется широко используемый процесс Inc/Dec, а используется пара процессов InterlockedIncrement/InterlockedDecrement. Они реализуют одну и ту же функцию, добавляя или вычитая единицу из переменной. Но у них есть одно самое большое отличие: InterlockedIncrement/InterlockedDecrement является потокобезопасным. То есть они могут гарантировать правильные результаты выполнения в условиях многопоточности, а Inc/Dec — нет. Или, с точки зрения теории операционных систем, это пара «примитивных» операций.
Возьмите плюс один в качестве примера, чтобы проиллюстрировать разницу в деталях реализации между ними:
Вообще говоря, операция добавления единицы к данным памяти состоит из трех этапов после разложения:
1. Чтение данных из памяти
2. Данные плюс один
3. Сохраните в памяти
Теперь предположим ситуацию, которая может возникнуть при использовании Inc для выполнения операции приращения в двухпоточном приложении:
1. Поток A считывает данные из памяти (предполагается, что их число равно 3).
2. Поток B читает данные из памяти (тоже 3)
3. Поток A добавляет единицу к данным (теперь это 4)
4. Поток B добавляет единицу к данным (теперь тоже 4)
5. Поток A сохраняет данные в памяти (теперь число данных в памяти равно 4).
6. Поток B также сохраняет данные в памяти (данных в памяти по-прежнему 4, но оба потока добавили к ним единицу, которая должна быть 5, поэтому здесь неверный результат)
С процессом InterlockIncrement такой проблемы нет, поскольку так называемый «примитив» — это непрерываемая операция, то есть операционная система может гарантировать, что переключение потоков не произойдет до того, как будет выполнен «примитив». Таким образом, в приведенном выше примере только после того, как поток A завершит выполнение и сохранит данные в памяти, поток B может начать получать число и добавлять его. Это гарантирует, что даже в многопоточной ситуации результат будет таким же. правильный.
Предыдущий пример также иллюстрирует ситуацию «конфликта доступа к потоку», поэтому потоки необходимо «синхронизировать» (Synchronize). Это будет подробно обсуждаться позже, когда будет упомянута синхронизация.
Говоря о синхронизации, следует сделать отступление: Ли Минг, профессор Университета Ватерлоо в Канаде, однажды возражал против перевода слова Synchronize как «синхронизация» в «синхронизация потоков». Лично я считаю, что то, что он сказал, на самом деле очень. разумный. «Синхронизация» на китайском языке означает «происходить одновременно», а цель «синхронизации потоков» — избежать этого «происхождения в одно и то же время». В английском языке Synchronize имеет два значения: одно — синхронизация в традиционном смысле (Происходить одновременно), а другое — «Действовать в унисон» (Действовать в унисон). Слово «Синхронизировать» в «синхронизации потоков» должно относиться к последнему значению, то есть «гарантировать, что несколько потоков сохраняют координацию и избегают ошибок при доступе к одним и тем же данным». Однако в ИТ-индустрии до сих пор существует много неточно переведенных слов. Поскольку это стало общепринятым, в этой статье я просто объясню это здесь, потому что разработка программного обеспечения — это кропотливая работа, и что следует уточнить. никогда не должно быть Не может быть расплывчатым.
Вернёмся к конструктору TThread. Самое важное дальше — это предложение:
FHandle:= BeginThread(nil, 0, @ThreadProc, Pointer(Self), CREATE_SUSPENDED, FThreadID);
Здесь используется упомянутая ранее RTL-функция Delphi BeginThread. Она имеет множество параметров, ключевыми из которых являются третий и четвертый параметры. Третий параметр — это упомянутая ранее функция потока, то есть часть кода, выполняемая в потоке. Четвертый параметр — это параметр, передаваемый функции потока, здесь это созданный объект потока (т. е. Self). Среди других параметров пятый используется для того, чтобы приостановить поток после создания и не выполнять его немедленно (работа по запуску потока определяется в соответствии с флагом CreateSuspended в AfterConstruction), а шестой — для возврата идентификатора потока.
Теперь давайте посмотрим на ядро TThread: функцию потока ThreadProc. Что интересно, ядро этого класса потока не является членом потока, а является глобальной функцией (поскольку соглашение о параметрах процесса BeginThread может использовать только глобальные функции). Вот его код:
функция ThreadProc (Thread: TThread): Integer;
вар
FreeThread: логическое значение;
начинать
пытаться
если не Thread.Terminate, то
пытаться
Поток.Выполнить;
кроме
Thread.FFatalException: = AcquireExceptionObject;
конец;
окончательно
FreeThread := Thread.FFreeOnTerminate;
Результат: = Thread.FReturnValue;
Thread.DoTerminate;
Thread.FFinished := True;
СигналСинкСобытие;
если FreeThread, то Thread.Free;
EndThread (Результат);
конец;
конец;
Хотя кода здесь немного, это самая важная часть всего TThread, поскольку именно этот код фактически выполняется в потоке. Ниже приводится построчное описание кода:
Сначала определите флаг Termination класса потока. Если он не помечен как завершенный, вызовите метод Execute класса потока для выполнения кода потока. Поскольку TThread — это абстрактный класс, а метод Execute — это, по сути, абстрактный метод. выполняет код Execute в производном классе.
Таким образом, Execute — это функция потока в классе потоков. Весь код в Execute необходимо рассматривать как код потока, например, для предотвращения конфликтов доступа.
Если в Execute возникает исключение, объект исключения получается через AcquireExceptionObject и сохраняется в члене FFatalException класса потока.
Наконец, перед тем, как нить закончится, нужно сделать несколько последних штрихов. Локальная переменная FreeThread записывает настройку атрибута FreeOnTermination класса потока, а затем устанавливает возвращаемое значение потока в значение атрибута возвращаемого значения класса потока. Затем выполните метод DoTerminate класса потока.
Код метода DoTerminate выглядит следующим образом:
процедура TThread.DoTerminate;
начинать
если назначено(FOnTerminate), то синхронизировать(CallOnTerminate);
конец;
Это очень просто, достаточно вызвать метод CallOnTerminate через Synchronize, а код метода CallOnTerminate следующий: просто вызвать событие OnTerminate:
процедура TThread.CallOnTerminate;
начинать
если назначено(FOnTerminate), то FOnTerminate(Self);
конец;
Поскольку событие OnTerminate выполняется в Synchronize, по существу это не код потока, а код основного потока (подробности см. в анализе Synchronize ниже).
После выполнения OnTerminate установите для флага FFinished класса потока значение True.
Далее выполняется процесс SignalSyncEvent, его код следующий:
процедура SignalSyncEvent;
начинать
SetEvent(SyncEvent);
конец;
Это также очень просто: просто установите глобальное событие: SyncEvent. В этой статье позже будет подробно описано использование Event, а назначение SyncEvent будет объяснено в процессе WaitFor.
Затем решается, освободить ли класс потока на основе настройки FreeOnTerminate, сохраненной в FreeThread. Когда класс потока освобождается, выполняются некоторые операции. Подробности см. в следующей реализации деструктора.
Наконец, EndThread вызывается для завершения потока и возвращается возвращаемое значение потока.
На этом ветка полностью завершена.
(продолжение следует)