В наследнике TThread в методе Execute объявлена локальная переменная некоторого типа. Предполагается, что одновременно будут работать несколько нитей, созданных на основе этого класса. Соответственно, они могут одновременно обращаться к этой переменной. Нужна ли в этом случае синхронизация при обращении к данной переменной?
Варианты ответов:
Синхронизация не нужна
Синхронизация нужна только для переменных типов с автоматическим управлением памятью (AnsiString, динамические массивы и т.п.)
Синхронизация нужна для переменных всех сложных типов (записи, массивы и т.п.)
Синхронизация нужна для переменных любых типов
Комментарий: Локальные переменные размещаются в стеке, а стек у каждой нити свой. Таким образом, у каждой нити будет своя копия локальных переменных, и одна нить не сможет повлиять на значения локальных переменных в другой нити.
Для операций с динамической памятью, в том числе и для выполняемых автоматически, важно, чтобы эти операции синхронизировались между собой. Однако ручная синхронизация не требуется. Менеджер памяти Delphi умеет работать в двух режимах: однонитевом и многонитевом (это определяется переменной IsMultiThreaded модуля System). По умолчанию он работает в однонитевом режиме, но класс TThread (точнее, используемая им функция BeginThread) автоматически переводит менеджер памяти в многонитевой режим. Таким образом, никакая синхронизация здесь не нужна.
Вопрос №2
Неглавная нить, созданная с помощью наследника TThread, должна обратиться к объекту VCL, созданному в главной нити. Нужна ли при этом синхронизация, и если да, то как она должна реализовываться?
Варианты ответов:
Синхронизация не нужна
Синхронизация нужна, должна реализовываться критической секцией или мьютексом
Синхронизация нужна, должна реализовываться методом TThread.Synchronize
Синхронизация нужна, может реализовываться как критической секцией или мьютексом, так и методом TThread.Synchronize
Комментарий: Синхронизация при использовании несколькими нитями любого ресурса, в т.ч. объекта VCL, необходима, потому что если одна нить начнём модифицировать объект в тот момент, когда с ним работает другая, результат будет непредсказуем. Синхронизация заключается в том, что нить, пытающаяся получить доступ к ресурсу, приостанавливает свою работу до тех пор, пока другая, уже работающая с ним, не закончит эту работу. В большинстве случаев критические секции или мьютексы – самые эффективные средства синхронизации. Однако работа с ними имеет смысл лишь тогда, когда код обеих нитей использует их. Что же касается объектов VCL, с ними связано много кода, который выполняется в главной нити и не использует никаких мьютексов и критических секций. Следовательно, для синхронизации с кодом VCL они неприменимы, а единственный способ синхронизации в данном случае – вызов TThread.Synchronize.
Вопрос №3
Как работает метод TThread.Synchronize?
Варианты ответов:
Использует критические секции для синхронизации доступа к объектам VCL
Приостанавливает главную нить, чтобы неглавная могла работать с компонентами VCL, не опасаясь одновременного доступа
Приостанавливает нить, вызвавшую Synchronize, до тех пор пока главная нить не выполнит метод, переданный в Synchronize
Решает проблемы с синхронизацией неизвестным науке способом
Комментарий: В коде VCL организована специальная очередь для процедур, которые должны быть выполнены в главной нити. При вызове Synchronize переданный ей метод помещается в эту очередь, и нить, вызвавшая Synchronize, приостанавливается до тех пор, пока главная нить не дойдёт до этой процедуры и не выполнит её. Главная нить проверят очередь в перерывах между обработкой сообщений, извлекая и выполняя по одному ожидающему методу за один раз. С каждым элементом очереди связывается специальное событие (см. функцию CreateEvent), которое главная нить переводит в сигнальное состояние после выполнения процедуры из очереди. Переход события в сигнальное состояние и есть критерий окончания ожидания для нити, вызвавшей Synchronize.
Вопрос №4
Метод Execute некоторой нити выглядит следующим образом
procedure TSomeThread.Execute;
begin
while True do
begin
Sleep(1000);
DoSomeThing(); // Вызов некоторого метода класса TSomeThread
end;
end;
Переменная SomeThread – ссылка на эту нить. Как из кода главной нити корректно остановить нить SomeThread?
Варианты ответов:
TerminateThread(SomeThread.Handle, 0);
SomeThread.WaitFor;
SomeThread.Terminate;
Любым из перечисленных выше способов
Данная нить не может быть остановлена корректно
Комментарий: Вызов TerminateThread остановит нить, но при этом не даст ей возможность освободить занятые ресурсы. В частности, не будет освобождён стек нити, т.е. произойдёт утечка памяти, могут также возникнуть другие проблемы. Таким образом, функция TerminateThread – средство на самый крайний случай, корректной такую остановку не назовёшь. Метод WaitFor просто ожидает, когда нить завершится, но никак не ускоряет сам процесс завершения, поэтому для принудительной остановки нити он неприменим. Метод Terminate является штатным средством корректной остановки нити, но он всего лишь устанавливает флаг Terminated. В коде Execute должна быть проверка этого флага и выход в том случае, если он установлен. В данном случае этого не наблюдается, поэтому вызов Terminate тоже не остановит нить. Таким образом, в данном случае корректная остановка нити невозможна.
Не следует думать, что проверка флага Terminated и вызов Terminate извне – единственный способ корректной остановки нити. В некоторых случаях могут оказаться удобными другие способы. Например, можно создать событие (функция CreateEvent), при необходимости установить его (SetEvent), а в коде Execute ожидать наступления этого события с помощью WaitForSingleObject или подобной ей функции. Но в любом случае, чтобы нить можно было корректно остановить, программист должен предусмотреть в методе Execute возможность получения сигнала извне и выхода по этому сигналу.
Вопрос №5
Класс TSomeThread, наследник TThread, содержит следующий код
procedure TSomeThread.TimerEvent(Sender: TObject);
begin
// Делаем что-то полезное...
end;
procedure TSomeThread.Execute;
var
Timer: TTimer;
begin
Timer := TTimer.Create(nil);
try
Timer.OnTimer := TimerEvent;
Timer.Interval := 1000;
while not Terminated do
Sleep(2000);
finally
Timer.Free;
end;
end;
С какой частотой будет вызываться метод TimerEvent?
Варианты ответов:
Вообще не будет вызываться
Раз в секунду
Раз в две секунды
Раз в три секунды
Комментарий: Таймеры в Windows работают через оконные сообщения. Каждый таймер связывается с каким-либо окном, и по истечении заданного интервала в очередь этого окна помещается сообщение WM_TIMER. Объект TTimer создаёт невидимое окно и связывает таймер с ним. Оконная процедура этого окна при обработке сообщения WM_TIMER вызывает событие OnTimer. Но так как в нити отсутствует цикл выборки сообщений из очереди (состоящий, в простейшем случае, из вызовов функций GetMessage и DispatchMessage), оконная процедура никогда не получит ни одного сообщения. Соответственно, событие OnTimer никогда не возникнет и метод TimerEvent никогда не будет вызван.
Сказанное выше не означает, что TTimer нельзя использовать в неглавной нити. Можно, но необходимо реализовать петлю сообщений. TTimer – не единственный компонент, который использует оконные сообщения и поэтому требует наличия такой петли. К подробным компонентам относятся, например, TClientSocket и TServerSocket.
Вопрос №6
Класс TSomeThread, унаследованный от TThread, содержит не имеющий параметров метод DoSomething, а метод Execute у этого класса реализован следующим образом
procedure TSomeThread.Execute;
begin
while not Terminated do
Synchronize(DoSomething);
end;
Содержит ли этот код ошибки, и если да, то какие?
Варианты ответов:
Код не содержит ошибок
Код не содержит формальных ошибок, но смысла не имеет
Прототип метода DoSomething не подходит для передачи в Synchronize
В методе Execute отсутствует вызов метода Execute предка
Комментарий: Метод TThread.Execute является абстрактным, поэтому вызывать в наследниках его нельзя. В Synchronize передаётся метод без параметров, так что метод DoSomething для него вполне подходит. Таким образом, никаких формальных ошибок код не содержит, его можно откомпилировать и запустить. Тем не менее, смысла этот код не имеет. Synchronize приостанавливает вызвавшую его нить и передаёт сигнал в главную нить вместе с адресом метода. Главная нить находит промежуток времени, когда она не загружена другой работой, и вызывает данный метод. До тех пор, пока она не выполнила метод DoSomething до конца, нить TSomeThread находится в состоянии ожидания. Таким образом, при данной реализации нить TSomeThread не делает никаких действий, кроме подачи сигналов главной нити и ожидания выполнения ею метода DoSomthing, т.е. никакого распараллеливания не происходит, и всю работу выполняет главная нить. Поэтому приведённый здесь код формально правильный, но смысла не имеет.
Синхронизировать с помощью Synchronize надо не весь код нити, а только его отдельные участки, которые реально обращаются к VCL. Если код вашей нити таков, что целиком требует синхронизации, значит, вы не можете распараллелить его в принципе, и попытка вынести его в отдельную нить только нагружает процессор бессмысленной работой по обмену сигналами и переключениями между нитями.
Вопрос №7
Метод Execute нити TSomeThread реализован следующим образом
procedure TSomeThread.Execute;
begin
while not Terminated do
// Делаем что-то полезное...
Synchronize(SayGoodBye);
end;
procedure TSomeThread.SayGoodBye;
begin
Form1.Label1.Caption := 'Нить завершила работу';
end;
В главной форме есть переменная SomeThread, содержащая указатель на экземпляр класса TSomeThread, и кнопка Button1, обработчик которой выглядит следующим образом
procedure TForm1.Button1Click(Sender: TObject);
begin
if SomeThread <> nil then
begin
SomeThread.Terminate;
WaitForSingleObject(SomeThread.Handle, INFINITE);
SomeThread := nil;
end;
end;
Что произойдёт при нажатии на кнопку Button1?
Варианты ответов:
Нить SomeThread будет остановлена
Произойдёт исключение Access violation
Программа зависнет
Не будет никаких видимых эффектов, но нить SomeThread не будет остановлена
Комментарий: Программа зависнет, и произойдёт это вот почему. Главная нить, установив флаг Terminated нити, перейдёт в режим ожидания, и это ожидание продлится до того момента, когда завершится нить SomeThread. Но нить SomeThread, прежде чем завершиться, вызывает метод Synchronize, который ожидает, когда главная нить выполнит переданный ему метод (в данном случае – метод SayGoodBye). Но главная нить не может его выполнить, потому что сама находится в режиме ожидания. Таким образом, получаем взаимную блокировку двух нитей, в результате которой ни одна из них не может продолжать работу.
Аналогичная проблема произойдёт и в том случае, если нить SomeThread перед завершением отправит какому-то из окон главной нити сообщение с помощью SendMessage: нить SomeThread будет ждать, пока главная нить обработает это сообщение, но главная нить не сможет этого сделать, так как тоже будет находиться в режиме ожидания.
Вопрос №8
Условия те же, что и в предыдущем вопросе, но обработчик нажатия кнопки Button1 теперь выглядит так
procedure TForm1.Button1Click(Sender: TObject);
begin
if SomeThread <> nil then
begin
SomeThread.Terminate;
SomeThread.WaitFor;
SomeThread := nil;
end;
end;
Что произойдёт при нажатии на кнопку Button1 в этом случае?
Варианты ответов:
Нить SomeThread будет остановлена
Произойдёт исключение Access violation
Программа зависнет
Не будет никаких видимых эффектов, но нить SomeThread не будет остановлена
Комментарий: Метод WaitFor устроен достаточно интеллектуально. Он вызывает функцию WaitForSingleObject только в том случае, если он вызван не из главной нити. В главной же нити он использует более сложную логику, основанную на функциях MsgWaitForMultipleObjects и PeekMessage. Первая из них позволяет входить в режим ожидания, который может быть прерван поступлением сообщения в очередь нити, вторая позволяет нити обрабатывать сообщения, отправленные нити с помощью SendMessage, не затрагивая при этом сообщения, посланные с помощью PostMessage. Таким образом, при вызове из главной нити WaitFor ни Synchronize, ни SendMessage не приводят к взаимной блокировке, и в данном случае программа поведёт себя правильно, т.е. нить SomeThread будет остановлена, и никаких ошибок при этом не возникнет.
Вопрос №9
Как правильно создавать нить без использования класса TThread?
Варианты ответов:
Функцией CreateThread, реализуемой системной библиотекой kernel32.dll и импортируемой модулем Windows
Функцией BeginThread, реализованной в модуле System
Любой из двух названных функций
Комментарий: Функция CreateThread не делает нескольких важных вещей, которые при её использовании приходится делать вручную. Она не инициализирует сопроцессор, не создаёт фрейм исключений и не переводит менеджер памяти в многонитевой режим. Из-за этого исключение в нити, созданной с помощью CreateThread, может вызвать зависание всей программы, а выделение и освобождение памяти в нити – самые разные плохо предсказуемые ошибки. Функция BeginThread всё это делает, поэтому правильно использовать именно её.
Если вы заметили орфографическую ошибку на этой странице, просто выделите ошибку мышью и нажмите Ctrl+Enter. Функция может не работать в некоторых версиях броузеров.