Пример создания многозадачного приложения с помощью системы программирования Borland Delphi
Рассмотрим пример использования механизмов, специально созданных для организации взаимного исключения, которые имеются в системе программирования Borland Delphi 3.0. Эта система программирования, будучи ориентированной на создание приложений в многозадачной операционной системе Microsoft Windows 9x, содержит в себе стандартные классы, позволяющие без особых усилий использовать многопоточные возможности этих ОС. Воспользуемся стандартным классом TThread. Объект, создаваемый на основе этого класса, можно охарактеризовать следующими теперь уже очевидными для нас свойствами:
¨ каждый тред имеет свою, при необходимости уникальную, исполняемую часть;
¨ каждый тред для своего исполнения требует отдельного процессорного времени, то есть диспетчер задач принимает во внимание только приоритет треда;
¨ диспетчеризация выполнения тредов осуществляется операционной системой и не требует вмешательства программиста;
¨ несколько тредов, принадлежащих одному процессу, могут использовать один и тот же ресурс (например, глобальную переменную). Как нам известно, треды могут обращаться к полям другого треда из того же вычислительного процесса. При этом программисту необходимо самостоятельно ограничивать доступ к этому ресурсу во избежание известных проблем.
Итак, пусть необходимо создать многопоточное приложение, схема взаимодействия отдельных потоков в котором (в рамках единого вычислительного процесса) приведена на рис. 6.6.
Рис. 6.6. Схема №1 взаимодействия параллельно выполняющихся задач
Так, согласно этому рисунку, процесс А после своего завершения запускает задачи D, С и Е. Считаем, что задачи В, D и С завершаются примерно в одинаковое время. По крайней мере, нам не известно, какой из потоков должен быть первым, а какой – последним. Однако по условиям задачи пусть поток F будет запускаться тем из перечисленных тредов, который завершается первым, но только после того, как завершатся два оставшихся треда, приходящие в «точку синхронизации». Наконец, пусть задача G запускается последним закончившим работу потоком Е или F.
Все указанные задачи создадим как потомки объекта TThread. Тексты всех программных модулей приведены в приложении А. Поскольку, согласно условию, действия, выполняемые задачами, для нас не имеют значения, представим исполняемую часть простейшим циклом с соответствующей задержкой. Для наглядности внутри цикла можно организовать вывод текущего состояния выполнения задачи в процентах на «строке состояния», для чего используем компонент TGauge. Благодаря тому, что все семь тредов похожи (используют одни и те же методы) и отличаются только в части принятия решения о синхронизации, опишем организацию базового объекта-треда.
Базовый объект (TTreadProgress) является потомком объекта TTread. При этом он имеет следующие поля:
¨ имя треда;
¨ строка состояния треда;
¨ «длина» треда (время его работы в отсутствие конкурентов);
¨ текущее состояние треда;
¨ признак завершения треда;
¨ имя запустившего треда;
¨ строка для вывода сообщений в компонент TMemo.
В базовом объекте объявлены следующие процедуры:
¨ исполняемая часть;
¨ завершающая часть;
¨ процедура прорисовки строки состояния;
¨ процедура вывода сообщения;
¨ конструктор объекта.
Все треды (от А до G) являются потомками этого объекта и перекрывают единственный метод – процедуру завершения процесса. В исполняемой части задачи после завершения цикла задержки, имитирующего выполнение полезной работы, устанавливается признак завершения и вызывается процедура завершения задачи, которая и выполняет соответствующие действия.
Общую схему работы программы, реализующей задание, можно описать следующим образом. Все задачи инициализируются соответствующей процедурой одновременно, но в режиме ожидания запуска. В качестве параметров инициализации в создаваемый поток передаются его имя, длительность и имя запускающего объекта (если оно известно заранее). Сразу после инициализации запускаются задачи А и В. Обе задачи сигнализируют об этом соответствующим сообщением. После своего завершения поток А запускает задачи (потоки) С, D и Е. Далее всё идет в соответствии с заданной блок-схемой. Задача, запускающая другую задачу, передаёт ей свое имя, обращаясь непосредственно к полю этого объекта. Информацию о том, завершился тот или иной поток, можно получить, обратившись к соответствующему полю – признаку завершения задачи.
Естественно, что при подобной организации доступа к полям тредов вероятно возникновение разного рода критических ситуаций. Напомним, основная причина их возникновения заключена в том, что несколько задач (в нашем случае – потоков) реально имеют возможность обращения к общим ресурсам практически одновременно, то есть с таким интервалом времени, за который этот ресурс не успеет изменить своё состояние. В результате задачи могут получать некорректные значения, о чем мы уже немало говорили.
Каждый процесс имеет связь с так называемыми VCL-объектами – видимыми компонентами. В данном случае такими являются строка состояния TGauge и поле сообщений TMemo. Для того чтобы в процессе работы нескольких параллельно выполняющихся задач не возникало критических ситуаций с выводом информации на эти видимые на экране объекты, к ним необходимо обеспечить синхронизированный доступ. Это довольно легко достигается с помощью стандартного для объекта TThread метода Synchronize. Метод имеет в качестве параметра имя процедуры, в которой производится вывод на VCL-объекты. При этом сама эта процедура нигде в программе не должна вызываться напрямую без использования метода Synchronize. В нашей программе такими процедурами являются прорисовка строки состояния (Do Visual Progress) и вывод текстового сообщения (WriteToMemo). Подобное использование метода Synchronize обеспечивает корректную работу нескольких параллельных процессов с VCL-объектами.
Однако метод Synchronize не помогает в случае совместного доступа к другим общим ресурсам. Поэтому необходимо применять другие средства для организации взаимного исключения. Главная цель этих средств заключается в обеспечении монопольного доступа для каждой задачи к общим ресурсам, то есть пока один поток не закончил обращение к подобному ресурсу, другой не имеет право этот ресурс использовать.
В системе программирования Delphi для этой цели имеется довольно-таки простой в использовании и достаточно эффективный метод критической секции с помощью объекта TCriticalSection. Этот метод заключается в следующем:
¨ участок кода каждого потока, в котором производится обращение к общему ресурсу, заключается в «скобки» критической секции – используются методы Enter и Leave;
¨ если какой-либо тред уже находится внутри критической секции, то другой поток, который дошел до «открывающей скобки» Enter, не имеет права входить в критическую секцию до тех пор, пока первый поток находится в ней. Когда первый тред выйдет из критической секции, второй сможет войти в неё и, в свою очередь, обратиться к критическому ресурсу.
Очевидно, что такой метод надежно обеспечивает задачам монопольный доступ к общим (критическим) ресурсам.
Метод критической секции имеет ряд преимуществ перед его аналогами. Так, например, использование семафоров (Semaphore) сложнее в реализации. Другой метод взаимных исключений – mutex – в целом похож на метод критической секции, но он требует больше системных ресурсов и имеет своё время тайм-аута, по истечении которого ожидающий процесс может всё-таки войти в защищённый блок, в то время как в критической секции подобного механизма нет.
Текст всей программы с необходимыми комментариями приведен в приложении А.
Дата добавления: 2022-02-05; просмотров: 280;