До расмотрения деталей создания потока и выполнения его кода независимо от основного потока приложения необходимо разобраться в диаграмме, иллюстрирующей динамику выполнения потока. Это значительно нам поможет, когда мы начнем разрабатывать многопоточные программы. Рассмотрим пример 1.
Приложение имеет один исполняемый поток: основной поток VCL. Его работу можно проиллюстрировать диаграммой, показывающей состояние потока в течение времени выполнения. Ось времени направлена вниз. Описание этой диаграммы будет относиться и ко всем последующим диаграммам выполнения потоков.
Заметьте, что эта диаграмма не показывает деталей выполнения алгоритмов. Вместо этого она отображает порядок событий во времени и состояние потока приложения между этими событиями. Имеет значение не фактическое расстояние между разными точками на диаграмме, а их вертикальное упорядочение. Некоторые части этой диаграммы следует рассмотреть особенно подробно.
Поток приложения не выполняется непрерывно Могут быть длинные периоды времени, когда он не получает никаких внешних стимулов, и совсем не выполняет вычислений или действий. Память и ресурсы приложением заняты, и окно находится на экране, но CPU не исполняет кода.
Приложение запущено, и выполняется основной поток. Как только создано главное окно, работы больше нет, и поток попадает в часть кода VCL, которая называется цикл обработки сообщений, опрашивающий операционную систему о наличии сообщений. Если нет сообщений, требующих обработки, операционная система приостанавливает (suspend) поток.
Хотя Win32 API обеспечивает исчерпывающую поддержку многопоточности, для создания и уничтожения потоков, в VCL имеется полезный класс, TThread, который предоставляет более высокоуровневый подход, значительно упрощает работу и помогает программисту избегать некоторых неприятных ловушек, в которые можно попасть при недостатке опыта. Я рекомендую использовать именно его. Система помощи Дельфи дает неплохое введение в создание класса потока, так что я не буду подробно рассказывать о последовательности действий для создания потока, за исключением того, что предложу выбрать пункт меню File| New... и затем Thread Object .
Этот пример содержит программу, которая вычисляет, является ли данное число простым. Она состоит из двух модулей, один с обычной формой, и один с объектом потока. Она более или менее работоспособна, но обладает несколькими неприятными особенностями, которые иллюстрируют основные проблемы, с которыми встречаются программисты, разрабатываюшие многопоточные приложения. Мы обсудим пути их преодоления позже. Модуль формы и модуль объекта потока.
Всякий раз, когда нажата кнопка "Spawn", программа создает новый объект потока, инициализирует несколько его полей, затем запускает выполнение потока. В зависимости от величины входного числа, поток работает, вычисляя, простое ли это число, и как только вычисления завершаются, поток отображает сообщение, показывая, простое оно или нет. Эти потоки конкурируют между собой, и, независимо от того, одно- или многопроцессорная у вас машина, с точки зрения пользователя они выполняются одновременно. Кроме того, эта программа не ограничивает числа созданных потоков. В результате вы можете продемонстрировать истинный параллелизм так:
На этом этапе появляется проблема синхронизации. Когда основной поток возобновляет выполнение (вызывает resume) "рабочего" потока, основной поток программы не может ничего знать о состоянии рабочего потока и наоборот. Вполне возможно, что рабочий поток может завершить свое выполнение прежде, чем в основном потоке VCL выполнится хоть один оператор. Фактически для маленьких чисел, расчет для которых займет менее чем 1/20 секунды, это весьма вероятно. Аналогично, рабочий поток не может ничего предполагать о состоянии основного потока. Остается лишь полагаться на планировщик Win32. Рассмотрим три основные проблемы: запуск, взаимодействие и завершение.
Delphi облегчает запуск потока. Перед началом исполнения порожденного потока часто нужно установить некоторое его начальное состояние. Создавая поток приостановленным (параметр конструктора потока), можно быть уверенным, что код потока не будет выполняться пока его не активируют. Это означает, что основной поток VCL может безопасно прочитать и модифицировать данные объекта TThread, гарантируя, что они будут правильными, когда порожденный поток начнет выполняться.
В данной программе свойства потока "FreeOnTerminate" и "TestNumber" установлены до начала выполнения. Если бы это не было сделано, то поведение потока должно было быть неопределенным. Если вы не хотите создавать поток приостановленным, то просто отодвигаете проблемы запуска до следующего этапа: проблемы взаимодействия.
Эти проблемы появляются, если у вас есть два работающих потока, и вам нужно каким-то способом связываться между ними. Эта программа не затрагивает взаимодействие потоков. На данный момент достаточно отметить, что если вы не защищаете все операции над разделяемыми данными, ваша программа, вероятно, будет вести себя непредсказуемо. Если вы не обеспечиваете требуемую синхронизацию и управление параллельным доступом, недопустимо следующее:
Даже такая простая операция, как доступ к общей целой переменной из двух потоков, может закончиться полным беспорядком, а несинхронизированный доступ к общим ресурсам или вызовы VCL приведут ко многим часам непростой отладки, значительной неразберихи, и возможно к обращению в ближайшую психиатрическую лечебницу. Пока вы не изучили подходящие методы в следующих главах, не делайте этого.
Есть ли хорошие новости? Вы можете делать все три вышеуказанные действия, если используете правильные механизмы для управления параллельным выполнением, и это не так уж и трудно! Мы рассмотрим простой путь разрешения вопросов взаимодействия через VCL в следующей главе, а более изящные (но и более сложные) методы позже.
Поток, подобно любому другому объекту Delphi, использует распределение памяти и других ресурсов, так что не должен вызывать удивления факт, что важно обращаться с завершением потока очень аккуратно, а наша программа этого не делает. Есть два возможных подхода к проблеме освобождения ресурсов.
Первый - позволить потоку решить все самому. Это главным образом используется для потоков, которые:
а) Передают результаты выполнения потока в основной поток VCL перед остановкой.
б) Не содержат ко времени завершения никакой информации, необходимой другому потоку.
В этих случаях программист может установить флаг "FreeOnTerminate" для объекта потока, и он корректно освободит ресурсы при своем завершении.
Второй подход - основной поток VCL должен прочитать данные из объекта рабочего потока после его завершения, а затем уничтожить его. Это описано в Главе 4.
Я не затрагивал проблем передачи результатов в основной поток, поскольку рабочий поток сам сообщает пользователю ответ путем вызова ShowMessage. При этом не используется связь с основным потоком VCL, и вызов ShowMessage можно рассматривать как потокобезопасный, так что работа VCL не нарушается. В результате я могу использовать первый метод, для разрушения потока и разрешить потоку самоуничтожиться. Несмотря на это, программа иллюстрирует одну неприятную особенность, проявляющуюся при саморазрушении потока:
Как можно заметить, могут произойти две вещи. Во-первых, мы можем попытаться выйти из программы, когда поток еще активен и ведет вычисления. Во-вторых - мы можем попытаться выйти из программы, когда поток приостановлен. Первый случай довольно благоприятный: приложение закрывается, не считаясь с потоком. Код завершения Delphi и Windows cделает все, как нужно. Второй вариант несколько хуже, поскольку поток приостанавливается где-то в недрах подсистемы обмена сообщениями Win32. При этом Delphi производит работу по очистке в обоих случаях. Тем не менее, принудительный выход из потока без учета того, в каком он состоянии - плохой стиль программирования. Например, рабочий поток может в это время вести запись в файл. Если пользователь выходит из программы до завершения процесса записи, то файл может быть поврежден. Вот почему правильнее, когда порожденные потоки завершают работу согласованно с основным потоком VCL, даже если не требуется передача данных: при этом возможно чистое завершение процесса и потока. В Главе 4 обсуждаются решения этой проблемы.
© Martin Harvey
2000.