Студопедия

Главная страница Случайная страница

Разделы сайта

АвтомобилиАстрономияБиологияГеографияДом и садДругие языкиДругоеИнформатикаИсторияКультураЛитератураЛогикаМатематикаМедицинаМеталлургияМеханикаОбразованиеОхрана трудаПедагогикаПолитикаПравоПсихологияРелигияРиторикаСоциологияСпортСтроительствоТехнологияТуризмФизикаФилософияФинансыХимияЧерчениеЭкологияЭкономикаЭлектроника






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






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

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

procedure TForm1.Button1Click(Sender: TObject);

var

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. Для вызова требуется шесть аргументов; однако необходимы из них только два: адрес функции, с которой начинается новая нить, и переменная для хранения идентификатора нити.

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

Во-первых: переместим всю логику в новую функцию, которая послужит началом новой нити. Затем модифицируем все ссылки на объекты, чтобы они включали Form1. Например, компонент списка ListBox1окажется вне области действия, если идентифицировать его просто как ListBox1; поэтому вам нужно ссылаться на него как на Form1.ListBox1. Наконец, добавим вызов API CreateThread к обработчику события OnClick кнопки Buttonl. Вызов имеет вид

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

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

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

Поиск простых чисел — вариант с несколькими нитями

function DoFactor (NotUsed: Pointer): integer;

var

Low, High, Count, Count2: integer;

begin

// очистим ListBox1

Form1.ListBox1.Items.Clear;

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

Low: = strToInt(Form1.edit1.Text);

High: = strToInt(Form1.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

Form1.ListBox1.Items.Add(inttostr(Count));

end {for}

end;

 

procedure TForm1.Button1Click(Sender: TObject);

var

THID: DWORD;

begin

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

end; {procedure}

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

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

□ Если какие-то функции должны выполняться параллельно основному процессу, выделение потока обязательно.

□ Если какие-то расчеты идут достаточно долго, то многие считают, что их тоже нужно помещать в поток. Во время расчетов, программа блокируется и невозможно нажать кнопку Отмена или произвести какие-либо действия. Но поток тут абсолютно необязателен, потому что можно обойтись и без него. Достаточно внутри расчетов поставить ВЫЗОВ Application.ProcessMessages, И В ЭТОМ месте выполнение расчетов будет прерываться на некоторое время, а программа будет обслуживать другие сообщения, пришедшие от пользователя. Таким образом получится простой эффект многозадачности без использования потока.

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

Истинную многозадачность можно получить только на многопроцессорных системах, где каждый процессор выполняет свою задачу. Для создания многозадачности на компьютерах с одним процессором, используют псевдомногозадачность. В этом виде один процессор выполняет сразу несколько задач благодаря быстрым переключениям между ними. Например, процессор может выполнять сразу десять задач, при этом каждой из них выделять по 10 миллисекунд своего рабочего времени. В этом случае процессор будет через определенные промежутки времени переключаться между задачами, и у пользователя будет создаваться иллюзия того, что они выполняются одновременно. Но это общий вид псевдомногозадачности, реально она реализована более сложным образом.

 

В 32-разрядных версиях Windows используется вытесняющая многозадачность (до этого была согласованная). В такой среде ОС разделяет процессорное время между разными приложениями и потоками на основе вытеснения.

Разделение происходит в основном благодаря приоритету потока. У каждого потока есть приоритет, по которому определяется его важность. Чем выше приоритет, тем больше процессорного времени ему выделяется.

Потоки с одинаковым приоритетом будут получать одинаковое количество процессорного времени.

У дополнительных потоков приоритет выставляется такой же, как и у главного потока программы, но его можно увеличить, или уменьшить.

 

Простейший поток

Для создания дополнительного потока в программах Delphi предназначен специальный модуль потока в репозитории (File\New\Other…) он обозначен пиктограммой Thread Object

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

Необходимость наследования связана с тем, что класс TThread содержит абстрактный метод Execute, который, собственно, и должен исполняться в рамках нового потока и который, следовательно, обязан перекрываться в потомках.

После указания имени дочернего класса Delphi раскрывает дополнительный модуль с обширным комментарием и заготовкой для дочернего класса.

{Важно: Методы и свойства объектов из библиотеки визуальных компонентов могут использоваться только в рамках вызова метода Synchronize, например:

Synchronize(UpdateCaption);

где метод UpdateCaption должен быть подобен такому

procedure MyThread.UpdateCaption;

Begin

Form1.Caption: = 'Новый текст метки';

end; }

{MyThread }

procedure MyThread.Execute;

Begin

{ Пожалуйста, поместите код потока в этом месте }

end;

End.

Программирование потока ничем не отличается от программирования обычной программы за одним важным исключением:

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

Точнее, он может это делать только при обращении к специальному методу Synchronize, с помощью которого осуществляется синхронизация исполнения главного потока программы с дополнительным потоком.

Изучение класса TThread начнем с метода Execute:

procedure Execute; virtual; abstract;

Это и есть код, исполняемый в создаваемом потоке TThread.






© 2023 :: MyLektsii.ru :: Мои Лекции
Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав.
Копирование текстов разрешено только с указанием индексируемой ссылки на источник.