Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лекция 8 .Потоки.doc
Скачиваний:
0
Добавлен:
01.05.2025
Размер:
269.82 Кб
Скачать

Лекция № 8. Потоки

Каждый обработчик событий в Delphi является процедурой — с началом и концом. Важно не написать такой обработчик, который выполнялся бы слишком долго, потому что в таком случае приложение могло бы показаться зависшим (этот момент всегда можно обойти).

Для примера создадим новое приложение, поместив компонент TButton на главную форму, и напишем следующий код обработчика события OnClick:

procedure TForm1.Button1Click(Sender: TObject);

v ar

X: double;

begin

{$n+}

repeat

X:=x+0.1

until x> 100000000;

Application.MessageBox ('Сделано', 'Работа завершена', MB_OK);

end;

После запуска программы и нажатия на кнопку, приложение некоторое время будет казаться зависшим. Его нельзя сдвинуть или изменить размер окна, а если поместить перед ним другое окно и затем его убрать, то форма не будет выглядеть нормально, т. е не перерисует себя. Но через некоторое время появляется панель с сообщением 'Сделано', и приложение вновь готово к работе.

Как объяснить такое поведение, ведь Windows — многозадачная операционная система с преимущественной диспетчеризацией. И это действительно так. Хотя приложение казалось зависшим, все остальные в это время работали как обычно.

В конечном итоге все сводится к тому, что если не использовать специальные программные методы, приложение в каждый момент времени может выполнять только одну задачу. Таким образом, пока оно было занято счетом до 100 000 000, та часть кода, которая вызывает другие обработчики событий и производит обычные действия типа изменения размера окна, функционировать не могла.

Для большинства приложений можно использовать старый метод — выполнять в каждом обработчике события только небольшой фрагмент работы. Как показывает приведенный пример, счет до 100 000 000 занимает всего несколько секунд, так что большинство процедур окажутся вполне приемлемыми. Если при запуске программы она кажется зависшей, необходимо проверить, не делает ли она в каком-то месте слишком много работы.

Для полной реализации возможностей, заложенных в программной среде разработки приложений, необходимо иметь хотя бы элементарное понимание важных концепций операционных систем.

Одним из самых больших преимуществ операционных систем семейства Windows является иная, чем в DOS модель памяти. В DOS программист был связан очень сложной логической моделью памяти, состоящей из сегментов размером в 64К. Более того, это ограничивало размер одной единицы данных теми же 64К.

Верхняя граница памяти DOS

HeapEnd

FreePtr

… ...

Список записей, регистрирующий наличие свободного пространства в НЕАР-области.

Неиспользуемая

(свободная часть кучи)

HeapPtr

Область памяти Неар, которая распределяется, начиная с HeapOrg, в сторону увеличения адресов. Управление ведется через список свободных областей.

(Куча растет вверх ↑)

Ovr HeapEnd

HeapOrg

Область памяти для загрузки оверлеев

(оверлейный буфер)

Ovr HeapOrg

Стек (растет вниз) ↓

SSeg : SPtr

Незанятая часть стека

Sseg : 0000

Глобальные переменные

Dseg : 0000

Типизированные

константы

Кодовый сегмент модуля

System

Кодовый сегмент первого

модуля

Образ

ЕХЕ-файла

в оперативной памяти

Кодовые сегменты

других модулей

Кодовый сегмент

последнего модуля

PrefixSeg

Кодовый сегмент

основной программы

Префикс сегмента

программы (PSP)

… …

Нижняя граница памяти DOS

Это была довольно простая для разработки приложений среда, поскольку программа могла иметь известную стартовую точку. Она полностью контролировала то, что мог делать пользователь, и знала, что она предпримет в ответ на его действия.

Но каждое приложение DOS выглядело и вело себя по-своему. Было трудно выполнять одновременно два или несколько приложений. Приложения не могли иметь никакой коммуникации друг с другом.

Модель памяти в OS Windows намного проще. Здесь используется плоская модель памяти, в которой каждая программа или процесс имеет виртуальное адресное пространство. Операционная система берет на себя задачу отображения виртуальной памяти процесса на физическую память, а также делает все необходимое для перегрузки памяти на диск, чтобы предоставить программам больше виртуальной памяти, чем физически установлено на машине.

Каждая из выпущенных Microsoft основных операционных систем по-своему управляет тем, как прикладные программы взаимодействуют с системой и с другими программами.

Windows — многозадачная кооперативная операционная система. Это означает, что за отклик на события и принятие соответствующих мер отвечает приложение (и, следовательно, его разработчик).

Одной из главных проблем при создании и отладке Windows было то, что если одно из приложений было неотлаженным, это могло отрицательно сказаться на всех остальных приложениях, работавших параллельно с ним.

Разберем простой пример программы для разложения числа на простые множители (это требует большого объема вычислений).

Если приложение Windows организовать так, чтобы пользователь мог ввести число в окне редактирования и затем нажать на кнопку, чтобы получить искомое разложение, то разработчик мог бы просто поместить весь необходимый для разложения код в обработчик события, эквивалентного OnClick. Математический код активировался бы, и если число достаточно велико, то вся система казалась бы по видимости зависшей. В этом была большая проблема Windows. Любое приложение, ведущее себя неаккуратно, могло разрушить или связать всю операционную систему и все работающие в этот момент программы.

Эту проблему удалось решить с помощью мультипрограммирования с преимущественными правами, когда каждой программе дается своя порция времени для выполнения своей задачи, но операционная система во всех случаях оставляет за собой контроль. Понятно, почему такая система лучше застрахована от аварий. Каждое приложение продолжает исполняться вне зависимости от того, выполняются ли в системе другие приложения. Но это не решает проблемы интенсивных вычислений в приложении, которое в этом случае кажется зависшим. Потому что по умолчанию в каждом приложении выполняется в данный момент единственный фрагмент кода.

Таким образом, если выполняется функция, связанная с интенсивными вычислениями, другие части программы (в том числе та, что отвечает за изменение размеров окна) не могут получить управление. Но, начиная с Win32, имеется способ исполнять одновременно несколько фрагментов кода. В этом процессе используется нечто, называемое нитями (потоками) кода.

Введение в нити

В операционных системах семейства Windows каждая запущенная программа называется процессом. Процесс имеет дело в основном с вопросами разного рода собственности. Например, процесс владеет своим пространством в памяти. Что же касается исполняемого кода, то операционная система использует другой объект, называемый потоком (нитью).

Когда запускается новый процесс, ему автоматически придается одна нить, которая начинает исполнять код соответствующей процедуры.

Нити используются операционной системой для диспетчеризации времени процессора. Что означает это распределение времени? Диспетчеризация — это метод выделения времени каждой из нитей (не процессов, поскольку процесс может иметь несколько нитей). Операционная система рассматривает все готовые к запуску нити и выбирает для исполнения одну из них. Диспетчер также определяет порцию времени, даваемую для исполнения каждой из нитей.

В любой момент времени в процессе может исполняться более одной нити. Каковы свойства каждой из них? Каждая исполняемая нить имеет свой собственный стек для локальных переменных и копии своего состояния. Все нити процесса имеют доступ к любым глобальным объектам в памяти; таким образом, например, текстовый процессор может иметь одну нить, отведенную для проверки орфографии, в то время как основная нить будет решать все остальные задачи.

Рассмотрим, как это все работает. Напишем программу, находящую все простые числа из заданного диапазона. Эта программа достаточно интенсивная в смысле вычислений, и применим нити, чтобы программа не выглядела зависшей.

Первая реализация приложения не использует нитей, а просто выполняется, когда пользователь нажимает на кнопку.

Вариант программы поиска простых чисел с одной нитью

procedure TForm1.Button1Click(Sender: TObject);

var

Low, High, Count, Count2 :integer;

begin

// очистим ListBox1

ListBox1.Items.Clear;

// зададим диапазон

Low := strtoint (Edit1.Text);

High := strtoint (Edit2.Text);

// найдем простые числа

for Count := Low to High do

begin

Count2 := 2;

while (Count2 < Count) and not(Count mod Count2 = 0) do

inc(Count2);

// выведем в ListBox

if (Count=Count2) then

ListBox1.Items.Add (Inttostr (Count));

end // for

end ;

Эта программа работает неплохо при поиске простых чисел, не превосходящих 50000.

Если применить ее для поиска больших простых чисел, например, в диапазоне от 100000 до 110000, то получим «синдромом ложного зависания приложения».

Возникает вопрос – как избавиться от подобных ситуаций?

Можно незначительно модернизировать приложение, отведя отдельный поток (нить) для выполнения арифметической работы. Тогда основной поток останется доступным для всех служебных функций, и приложение не будет казаться зависшим.

Диспетчеру требуется знать — с какой точки следует начать исполнение нового потока. При желании можно передать и другую информацию, но начальная точка — это главное, что следует указать. Напишем функцию, которая станет началом новой нити.

Надо иметь в виду, что предмет мультипрограммирования сложен и включает в себя много моментов. Рассматриваемый пример направлен на то, чтобы дать представление о мощности методов, использующих нити. При разработке приложений со многими нитями, появляются новые вопросы, которым нужно уделить внимание.

Первый шаг состоит в написании функции, с которой начнется исполнение нити. Это функция специального вида, которая должна возвращать целое и принимать в качестве параметра один указатель. Заголовок функции, которая будет производить деление, должен выглядеть так:

function DoFactor (NotUsed: Pointer): integer;

В данном случае параметр не нужен, поэтому он и назван для ясности NotUsed. Оставшаяся часть функции просто выполняет то, для чего предназначена нить.

Чтобы создать саму нить, следует вызвать процедуру API Win32 с именем CreateThread. Для вызова требуется шесть аргументов; однако необходимы из них только два: адрес функции, с которой начинается новая нить, и переменная для хранения идентификатора нити.

Чтобы реализовать программу поиска простых чисел с помощью нитей, нужно внести в нее лишь небольшие изменения:

  1. Переместим всю логику в новую функцию, которая послужит началом новой нити.

  2. Модифицируем все ссылки на объекты, чтобы они включали Form1. Например, компонент списка ListBox1окажется вне области действия, если идентифицировать его просто как ListBox1; поэтому необходимо будет ссылаться на него как на Form1.ListBox1.

  3. Добавим вызов API CreateThread к обработчику события OnClick кнопки Buttonl. Вызов имеет вид

CreateThread (nil, 0, @DoFactor, nil, 0, THID);

Он означает, что необходимо запустить новую нить. Информация, используемая для защиты, отсутствует, размер стека будет определяться динамически.

Новая нить должна начинаться в функции DoFactor, не требующей аргумента. Никаких флагов не устанавливается; идентификатор нити помещается в переменную THID. @DoFactor – означает передачу адреса функции, а не ее вызов с последующим возвратом значения.

Этот простой пример дает представление о том, как нити помогают усовершенствованию приложений.