Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Книга о KOL.doc
Скачиваний:
29
Добавлен:
30.04.2019
Размер:
1.77 Mб
Скачать

5 .9. Нить, или поток команд (tThread)

Так же, как и VCL, для организации самостоятельного потока команд (или нити команд, thread переводится как "нить"), в библиотеке KOL имеется объект, который назван так же - TThread. Но, в отличие от VCL, в KOL не требуется для организации собственного потока создавать объект-наследник объектного типа TThread. Достаточно назначить ему обработчик события OnExecute, и в нем реализовать код, который будет выполняться, когда поток будет запущен. Для запуска потока следует вызвать метод Resume (не знакомые с этим обстоятельством новички пытаются вызывать метод Execute, но это не приводит к желаемым результатам, т.к. в этом случае запуск происходит в том же потоке, откуда вызван Execute).

Имеется несколько различных конструкторов для организации потока команд:

NewThread - создает объект в состоянии Suspended ("остановлен"). После такого создания объекта возможно назначить ему обработчик события OnExecute, и запустить поток (Resume);

NewThreadEx( onexec ) - создает объект, присваивает его событию OnExecute указанный в параметре обработчик, и запускает его на выполнение (если только параметр onexec не равен nil). Т.е. в результате такого конструирования потока, тот начинает работу немедленно. Если приложение работает на машине с одним процессором, скорее всего, какая-то часть кода обработчика (т.е. кода потока) будет выполнена уже до того, как управление возвратится в поток, создавший объект, в точку, следующую за вызовом конструктора NewThreadEx;

NewThreadAutoFree( onexec ) - создает и запускает объект потока аналогично предыдущему конструктору, но дополнительно обеспечивает автоматическое разрушение объекта по завершении потока.

Важная деталь: объект потока в KOL не позволяет запустить себя более чем однократно. В любом случае, если поток был завершен, т.е. произошел возврат из обработчика OnExecute, объект уже не нужен. Отличие третьего способа конструирования заключается в том, что объект сам себе устраивает вызов метода Free по завершении обработчика.

Методы, свойства и события объекта TThread:

Execute - этот метод служит только для внутренних целей (хотя и объявлен в разделе public). В принципе, поскольку этот метод объявлен виртуальным, существует возможность создавать наследник объекта потока TThread, подобно тому, как это делается в VCL. Но я предпочитаю использовать более экономный метод присваивания обработчика событию OnExecute. Этот метод еще и более удобный, так как позволяет использовать "зеркальный" компонент MCK, и создавать код для потока "визуально". (Вы теперь догадываетесь, почему в VCL нет компонента времени проектирования для представления потоков команд, и код приходится прописывать вручную?);

Resume - переводит приостановленный (Suspended) поток в режим "выполнение команд". Если поток уже работает, данный метод ничего не делает. Для запуска потока следует использовать именно этот метод, а не Execute, иначе операторы потока станут выполняться прямо в том же потоке, из которого и был вызван Execute;

Suspend - приостанавливает выполнение потока. В том числе, данный метод может быть выполнен и в самом приостанавливаемом потоке. Возврат из вызова метода Suspend в этом случае произойдет только тогда, когда из другого потока для него будет выполнен метод Resume;

Suspended - проверяет, что поток "приостановлен";

Terminate - останавливает поток, используя API-функцию TerminateThread. В отличие от VCL, это не нормальный способ остановки, а скорее, аварийный. Для того, чтобы завершить выполнение потока более безопасно, следует каким-то образом уведомить свой поток, что ему бы пора завершиться (например, установить флажок в какой-нибудь глобальной переменной или в поле объекта, известного потоку - да хотя бы задействовать его свойство Tag, общее для всех наследников TObj, в том числе и для объекта TThread). После чего следует в течение какого-то времени подождать, чтобы ваш поток принял эту информацию к сведению, и сам бы завершился. Разумеется, код для обеспечения такого взаимодействия целиком ложится на плечи программиста, то есть на вас;

Terminated - проверяет, что поток завершен (не обязательно аварийно, методом Terminate, может быть, и обычным образом - возвратом из обработчика OnExecute);

WaitFor - ожидание завершения данного потока, возврат из этого метода происходит только тогда, когда поток действительно завершился;

Handle - системный дескриптор объекта потока hThread. Может пригодиться для каких-либо вызовов функций API, например, он может быть передан процедурам WaitForMultipleObjects и WaitForMultipleObjectsEx;

ThreadID - еще один дескриптор потока команд как системного объекта уровня ядра (kernel);

PriorityClass - класс приоритета потока. Рекомендую особенно классами приоритетов не злоупотреблять, т.к. используемый в Windows способ обслуживания задач и потоков несовершенен. Малейшее повышение приоритета одного из потоков часто приводит к тому, что выполняется только этот поток, а остальные вынуждены простаивать, пока ему есть что делать (т.е. пока он сам себя не остановит, или не запросит длительную операцию асинхронного ввода-вывода). Различаются классы:

- IDLE_PRIORITY_CLASS - низший, т.е. поток работает только тогда, когда системе вообще больше нечего делать;

- NORMAL_PRIORITY_CLASS - обычный класс приоритета;

- HIGH_PRIORITY_CLASS - повышенный, т.е. все потоки с обычным приоритетом будут выполняться крайне медленно, если работает хотя бы один поток с повышенным приоритетом;

- REALTIME_PRIORITY_CLASS - реального времени: вообще все останавливается, работает только этот поток, даже мышь система в этом режиме обслуживать не сможет, если ваш поток занят (хм, а кнопка "reset" на вашем компьютере есть?);

ThreadPriority - приоритет потока внутри своего класса приоритетов. В отличие от предыдущего свойства, это более "мягкий" (и менее фатальный) способ управления приоритетами потоков. Однако, здесь картина прямо противоположная: довольно часто весьма мало что удается изменить, управляя приоритетом с помощью этого свойства, если не использовать крайние значения.

Различаются следующие приоритеты:

- THREAD_PRIORITY_IDLE - низший;

- THREAD_PRIORITY_LOWEST - минимальный, позволяющий потоку исполняться, даже когда система не простаивает;

- THREAD_PRIORITY_BELOW_NORMAL - пониженный по сравнению с нормальным;

- THREAD_PRIORITY_NORMAL - нормальный;

- THREAD_PRIORITY_ABOVE_NORMAL - выше нормального,

- THREAD_PRIORITY_HIGHEST - повышенный;

- THREAD_PRIORITY_TIME_CRITICAL - критический по времени (такой приоритет хорош для потока, в котором обрабатываются события от таймера, созданного в этом же потоке);

PriorityBoost - разрешает или запрещает сразу для всех потоков в системе (т.е. вообще во всей системе, если верить справке по Win32 API) временное повышение приоритета для потоков, выходящих из состояния ожидания какого-либо системного события. Обычно, такое поведение для системы является поведением по умолчанию, т.е. временное повышение приоритета разрешено;

Data - произвольный указатель (или 32-разрядное число), ассоциируемый с объектом потока;

OnExecute - "событие", которое используется для назначения кода, исполняемого в потоке, без наследования других объектных типов от TThread. Завершение этого обработчика означает завершение работы потока, его свойство Terminated принимает значение true. Повторно поток запустить уже нельзя. Если один и тот же поток предполагается использовать для многократного выполнения однотипных задач, то необходимо в обработчике организовать "вечный" цикл ожидания заказов на выполнение этих задач. Разумеется, необходимо предусмотреть и выход из этого "вечного" цикла при наступлении какого-то события, иначе поток придется завершать аварийно, когда приложению пора будет закрываться;

OnSuspend - событие, которое срабатывает непосредственно перед приостановкой данного потока. Данное событие срабатывает в контексте потока, вызвавшего метод Suspend. В том числе, если поток приостанавливает себя сам, то обработчик вызывается в его контексте. В момент срабатывания этого события поток еще не приостановлен.;

OnResume - событие, которое срабатывает после возобновления потока. Обработчик этого события так же вызывается в контексте того потока команд, который вызвал метод Resume. Это значит, что, во-первых, этот обработчик в принципе никогда не будет вызван в контексте самого возобновленного потока (т.к. поток не может возобновить себя самостоятельно). А, во-вторых, к моменту срабатывания обработчика этого события, возобновленный поток к моменту (или в процессе) вызова обработчика данного события, вполне возможно, уже опять остановлен (а то и завершен, и даже разрушен как объект!);

Synchronize( method ) - вызывает указанный метод "синхронно", в главном потоке. Метод Synchronize следует использовать в самом выполняемом потоке для того, чтобы обеспечить более безопасное исполнение участков кода, которые желательно выполнять в контексте главного потока. Под "главным" потоком подразумевается поток команд, в котором создан оконный объект Applet (и в том же потоке должны бы создаваться и все формы, во избежание недоразумений). Данный метод обеспечивает синхронизацию путем отправки (SendMessage) сообщения главному окну (апплету), и в результате система "замораживает" поток-отправитель вплоть до возврата из переданного на выполнение в главном потоке метода. Такое замораживание не делает поток "приостановленным" (Suspended) в обычном смысле, т.к. его нельзя в этот момент "возобновить" (Resume не действует);

SynchronizeEx( method, param ) - аналогично предыдущему методу, с той только разницей, что метод должен иметь (единственный, не считая Self) параметр, в общем случае - указатель. Что это за указатель, это дело программиста. Я добавил такой метод, когда ощутил необходимость не просто синхронизировать некоторое непараметризуемое действие, и передавать ему некоторый набор входных данных для работы. Один параметр - это, конечно, мало, но через указатель, в принципе, можно передать что угодно, достаточно объявить свою структуру данных и создать тип указателя на нее.

Как и для многих других объектов, управляемых свойствами и событиями, в MCK для объекта TThread существует зеркальный компонент TKOLThread. Достаточно положить его на форму, назначить обработчик события OnExecute, и настроить другие его свойства на этапе разработки, чтобы начать строить многопоточное приложение.

Следует, однако, понимать, что такое потоки команд, и как их правильно использовать, чтобы не напороться на крупные неприятности. Прежде всего, надо себе четко представлять, что выполнение кода, выполняемого в потоке, может быть в любой момент прервано системой, на границе любой машинной инструкции (даже не оператора Паскаля), и управление может быть передано любому другому потоку в системе или в вашем же приложении. И главный поток в этом нисколько не отличается от прочих потоков.

Это означает, что данные (переменные, дескрипторы системных объектов), совместно используемые в нескольких потоках исполнения, должны быть защищены. Самое неприятное, что может случиться - это разрушение объектов (или освобождение памяти структур) в одном потоке, в то время как с ними идет работа в другом потоке.

Мои рекомендации:

  • Защищайте ответственные за создание и уничтожение общих данных участки кода, используя мьютексы, семафоры, и критические секции;

  • Защищайте совместно используемые объекты от преждевременного уничтожения (используя свойство RefCount и методы RefInc и RefDec);

  • Синхронизируйте выполнение действий, изменяющих состояние оконных объектов, с главным потоком (методы Synchronize и SynchronizeEx);

  • И, наконец: избегайте использования слишком большого числа потоков в приложении. Если есть возможность решить ту же задачу без создания потоков команд, решайте ее именно так.