Построение оконных мультизадачных приложений, организация обмена данными между процессами
Федеральное агентство по образованию
Ангарская
Государственная Техническая
Кафедра
“Вычислительные машины, комплексы,
системы и сети”.
Курсовая
работа
По курсу «Системное программное обеспечение»
По теме «Построение
оконных мультизадачных приложений,
организация обмена данными между процессами»
Выполнил:
cтудент гр. ВМК-02-2
Проверил:
ассистент
г. Ангарск
2005
СОДЕРЖАНИЕ
1.ЗАДАНИЕ
В курсовом проекте
предлагается спроектировать и разработать
программу, обеспечивающую мультизадачность.
В программе необходимо предусмотреть
два визуальных процесса: один для ввода
с контролем и корректировки данных (выполнять
все виды корректировки – добавление,
удаление, замену) и вывода результатов
указанного в задании запроса и второй
- для выполнения запроса.
Структура
записи:
Лекарства с реквизитами
| Название лекарства | Номер аптеки | Цена упаковки | Количество упаковок |
Запрос: Выдать информацию о наличии некоторого лекарства в различных аптеках.
Обмен данными: через именованный канал.
Средства
синхронизации потоков: критические
секции.
2.ВВЕДЕНИЕ
Работая с Delphi, нужно иметь в виду: этот замечательный продукт не только упрощает разработку сложных приложений, он использует при этом все возможности операционной системы. Одна из возможностей, которую поддерживает Delphi, — это так называемые потоки (threads) или нити.
Потоки позволяют в рамках одной программы решать несколько задач одновременно. С недавних пор операционные системы для персональных компьютеров сделали это возможным.
Операционная система (ОС) предоставляет приложению некоторый интервал времени центрального процессора (ЦП) и в момент, когда приложение переходит к ожиданию сообщений или освобождает процессор, операционная система передает управление другой задаче. Теперь, когда компьютеры с более чем одним процессором резко упали в цене, а операционная система Windows NT может использовать наличие нескольких процессоров, пользователи действительно могут запускать одновременно более одной задачи.
Планируя время центрального процессора, Windows 95 или Windows NT распределяют его между потоками, а не между приложениями. Чтобы использовать все преимущества, обеспечиваемые несколькими процессорами в современных операционных системах, программист должен знать, как создавать потоки.
Определение потока довольно простое: потоки — это объекты, получающие время процессора. Время процессора выделяется квантами (quantum, timeslice). Квант времени — это интервал, имеющийся в распоряжении потока до тех пор, пока время не будет передано в распоряжение другого потока. Обратите внимание, что кванты выделяются не программам или процессам, а порожденным ими потокам. Как минимум, каждый процесс имеет хотя бы
один (главный) поток, но современные операционные системы, начиная с Windows 95 (для приверженцев Borland Kylix и Linux также), позволяют запустить в рамках процесса несколько потоков. Cамый простой пример их использования — приложения из состава Microsoft Office. К примеру, пакеты Excel и Word задействуют по несколько потоков. Word может одновременно корректировать грамматику и печатать, при этом осуществляя ввод данных с клавиатуры и мыши; программа Excel способна выполнять фоновые вычисления и печатать.
Если задачи приложения можно разделить на различные подмножества: обработка событий, ввод/вывод, связь и др., то потоки могут быть органично встроены в программное решение. Если разработчик может разделить большую задачу на несколько мелких, это только повысит переносимость кода и возможности его многократного использования.
Сделав приложение многопоточным, программист получает дополнительные возможности управления им. Например, через управление приоритетами потоков. Если один из них "притормаживает" приложение, занимая слишком много процессорного времени, его приоритет может быть понижен. Другое важное преимущество внедрения потоков — при возрастании "нагрузки" на приложение можно увеличить количество потоков и тем самым
снять проблему. Потоки упрощают жизнь тем программистам, которые разрабатывают приложения в архитектуре клиент/сервер.
Когда мы говорим "программа" (application), то обычно имеем в виду понятие, в терминологии операционной системы обозначаемое как "процесс". Процесс состоит из виртуальной памяти, исполняемого кода, потоков и данных. Процесс может содержать много потоков, но обязательно содержит, по крайней мере, один. Поток, как правило, имеет "в собственности" минимум ресурсов; он зависит от процесса, который и распоряжается виртуальной памятью, кодом, данными, файлами и другими ресурсами ОС. Почему мы используем потоки вместо процессов, хотя, при необходимости, приложение может состоять и из нескольких процессов? Дело в том, что переключение между процессами — значительно более трудоемкая операция, чем переключение между потоками. Другой довод в пользу использования потоков — то, что они специально задуманы для разделения ресурсов; разделить ресурсы между процессами (имеющими раздельное адресное пространство) не так-то просто.
3. ПРОБЛЕМЫ ПРИ СИНХРОНИЗАЦИИ ПОТОКОВ
К сожалению, простота создания потоков подчас "компенсируется" сложностью их применения. Две типичные проблемы, с которыми программист может столкнуться при работе с потоками, — это тупики (deadlocks) и гонки (race conditions).
Тупики
Вероятно, вы не раз наблюдали на трамвайной остановке следующую забавную картину
Тупики имеют место, когда поток ожидает ресурс, который в данный момент принадлежит
другому
потоку. Рассмотрим пример. Поток 1 захватывает
ресурс А, и для того чтобы продолжать
работу, ждет возможности захватить ресурс
Б. В тоже время Поток 2 захватывает ресурс
Б и ждет возможности захватить ресурс
А. Развитие этого сценария заблокирует
оба потока; ни один из них не будет исполняться.
Ресурсами могут выступать любые совместно
используемые объекты системы — файлы,
массивы в памяти, устройства ввода/вывода
и т. п. В информационных технологиях все
бывает сложнее. Откройте любой документ,
сопровождающий очередной пакет обновления
к любой версии Windows. Очень часто там можно
найти информацию об одной двух исправленных
ситуациях тупиков.
Гонки
Ситуация гонок возникает, когда два или более потока пытаются получить доступ к общему ресурсу и изменить его состояние. Рассмотрим следующий пример. Пусть Поток 1 получил доступ к ресурсу и изменил его в своих интересах; затем активизировался Поток 2 и модифицировал этот же ресурс до завершения Потока 1. Поток 1 полагает, что ресурс остался в том же состоянии, в каком был до переключения. В зависимости от того, когда
именно был изменен ресурс, результаты могут варьироваться — иногда код будет выполняться нормально, иногда нет. Программисты не должны строить никаких гипотез относительно порядка исполнения потоков, т. к. планировщик ОС может запускать и останавливать их в любое время.
Inc(i);
if i = iSomething then
DoSomething;
Здесь i — глобальная переменная, доступная из обоих потоков.
Пусть два или более потоков исполняют этот код одновременно. Поток 1 инкрементировал значение переменной i и хочет проверить ее значение для выполнения тех или иных условий. Но тут активизируется другой поток, который еще увеличивает значение i. В результате первый поток "проскакивает" мимо условия, которое, казалось бы, должно было быть выполнено. Возникновения как ситуаций гонок, так и тупиков можно избежать, если
использовать приемы, обсуждаемые ниже.
3.1 СИНХРОНИЗАЦИЯ
Главные
понятия для понимания
К возможным вариантам относятся четыре объекта, которые разработаны специально для синхронизации: событие (event), взаимное исключение (mutex), семафор (semaphore) и таймер (timer). Но кроме специальных объектов можно организовать ожидание и других
объектов, дескриптор которых используется в основном для иных целей, но может применяться и для ожидания. К ним относятся: процесс (process), поток (thread), оповещение об изменении в файловой системе (change notification) и консольный ввод (console input).
Косвенно
к этой группе может быть добавлена
критическая секция (criticalsection).
4. ФУНКЦИИ WINAPI32
4.1 КРИТИЧЕСКИЕ СЕКЦИИ
Критические секции – это механизм, предназначенный для синхронизации потоков внутри одного процесса. Как и мутекс, критическая секция может в один момент времени принадлежать только одному потоку, однако, она предоставляет более быстрый и эффективный механизм, чем мутексы. Перед использованием критической секции необходимо инициализировать её функцией:
procedure InitializeCriticalSection( var lpCriticalSection: TRTLCriticalSection);
После создания объекта поток, перед доступом к защищаемому ресурсу должен вызвать функцию:
procedure EnterCriticalSection(var lpCriticalSection: TRTLCriticalSection);
В качестве единственного параметра функциям EnterCriticalSection и LeaveCriticalSection необходимо передать адрес стрктуры типа TRTLCriticalSection, проинициализированной предварительно функцией InitializeCriticalSection.
Как работают критические секции? Если в этот момент ни один из потоков в процессе не владеет объектом, то поток становится владельцем критической секции и продолжает выполнение. Если секция уже захвачена другим потоком то выполнение потока, вызвавшего функцию EnterCriticalSection приостанавливается до её освобождения.
Таким образом гарантируется, что фрагмент кода, заключенный между вызовами функций EnterCriticalSection и LeaveCriticalSection, будет выполняться потоками последовательно, если все они работают с одной и той же критической секцией.
Поток, владеющий критической секцией, может повторно вызывать функцию EnterCriticalSection без блокирования своего исполнения. По завершению работы с защищаемым ресурсом поток должен вызвать функцию:
procedure LeaveCriticalSection( var lpCriticalSection: TRTLCriticalSection);
Эта функция освобождает объект независимо от количества предыдущих вызовов потоком функции EnterCriticalSection. Если имеются другие потоки, ожидающие освобождения секции, один из них становится её владельцем и продолжает исполнение. Если поток завершился, не освободив критическую секцию, её состояние становится неопределенным, что может вызвать блокировку работы программы.
Имеется возможность попытаться захватить объект без замораживания потока. Для этого служит функция:
function TryEnterCriticalSection( var lpCriticalSection: TRTLCriticalSection): BOOL;
Она проверяет, захвачена секция ли в момент её вызова. Если да – функция возвращает FALSE, в противном случае – захватывает секцию и возвращает TRUE.
По завершении работы с критической секцией, она должна быть уничтожена вызовом функции:
procedure DeleteCriticalSection(var lpCriticalSection: TRTLCriticalSection);
4.2 ПОТОКИ
Для создания потока существует функция
function CreateThread(nil,
dwStackSize : DWORD, //начальный размер стека в байтах
lpStartAddress : LPTHREAD_START_ROUTINE,
// указатель на функцию потока
Nil,0, //Поток создается и начинает свое выполнение.
lpThreadId : DWORD // указатель на код
//завершения потока
) : HANDLE
Параметры:
lpThreadAttributes
- указатель на структуру
dwStackSize
- указание этого параметра
lpStartAddress
- имя функции, выполняемой как
поток. Функция потока имеет
один 32-битный указатель типа LPVOID
в качестве аргумента и
lpThreadId-адрес переменной типа DWORD для размещения кода созданного потока
Если функция успешна, то возвращаемое значение есть идентификатор потока. Если функция неуспешна, то возвращаемое значение равно NULL.
Пример:
secondThread:=createthread(
2. Для приостановки выполнения текущего потока на определенный интервал времени существует функция
procedure
Sleep(dwMilliseconds : DWORD);
4.3 КАНАЛЫ
Каналы достаточно просты в использовании. Через канал можно передавать данные между двумя процессами или потоками. Один из процессов создает канал, другой открывает его. После этого оба процесса могут передавать данные через канал в одну или в обе стороны, используя для этого функции ReadFile и WriteFile.
Существует две разновидности каналов: именованные и анонимные. Именованному каналу при создании присваивается имя, которое доступно для других процессов. Анонимные каналы обычно используются для организации передачи данных между родительскими и дочерними процессами.
Имя канала имеет вид: \\.\\pipe\имя канала
Для создания
именованных каналов
HANDLE CreateNamedPipe( LPCTSTR lpName,
// адрес строки имени канала
dwOpenMode: PChar, // режим открытия канала
dwPipeMode: Cardinal, // режим работы канала
1,
nOutBufferSize: Cardinal,
// размер выходного буфера в байтах
nInBufferSize: Cardinal,
//
размер входного буфера в
nDefaultTimeOut: Cardinal,
// время ожидания в миллисекундах
nil ); // указатель на структуру атрибутов защиты
Если функция успешна, ее возвращаемое значение есть идентификатор серверного конца именованного канала.
Пример создания канала:
PipeID:=CreateNamedPipe('\\
После того, как серверный процесс создал именованный канал, он может перейти в режим ожидания, пока клиентский процесс не соединится с каналом. Для этого существует функция
ConnectNamedPipe(
HANDLE hNamedPipe, // идентификатор серверного конца именованного канала
nil ) : BOOL;
Если функция отработала успешно, то возвращаемое значение ненулевое.
Если функция выполнена неуспешно, то возвращаемое значение равно 0.
Пример:
ConnectNamedPipe(PipeID,
Для создания канала клиентский процесс должен воспользоваться функцией
HANDLE CreateFile(
LPCTSTR lpFileName: PChar,
//адрес строки с именем файла или именованного канала
dwDesiredAccess: Cardinal, // режим доступа
0,nil, OPEN_EXISTING,0,0 );
dwDesiredAccess
Этот параметр может принимать следующие значения:
| GENERIC_READ | Разрешен доступ на чтение к файлу или каналу. |
| GENERIC_WRITE | Разрешен доступ на запись к файлу или каналу. |
Пример:
ClPipeID:=CreateFile('\\.\
Для чтения данных из канала используется функция
ReadFile(hFile: HANDLE,
// идентификатор канала или файла
lpBuffer: Untyped, // адрес буфера для чтения
nNumberOfBytesToRead: Cardinal,
// число байт, которое необходимо прочитать
lpNumberOfBytesRead: Cardinal, // адрес поля, куда
/// запишется фактически прочитанное число байт
nil ): BOOL;
Если функция отработала успешно, то возвращаемое значение ненулевое. Если при этом число прочитанных байт равно 0, это означает, что указатель файла за пределом текущего конца файла.
Если функция неуспешна, то возвращаемое значение равно 0.
Пример:
ReadFile(PipeID,sam,sizeof(
Для записи данных в канал используется функция
WriteFile(hFile: HANDLE,
//идентификатор канала или файла
lpBuffer: Cardinal,
// адрес буфера для записи
nNumberOfBytesToWrite: Cardinal,
// число байт, которое необходимо записать
lpNumberOfBytesWritten: Cardinal,
// адрес поля, куда запишется фактически
//записанное число байт
nil) : BOOL;
Если функция успешна, то возвращаемое значение ненулевое.
Если
функция неуспешна, то возвращаемое
значение равно 0.
Пример:
WriteFile(PipeID,sam1,
Для закрытия канала существует функция
CloseHandle(hObject : HANDLE // идентификатор закрываемого объекта ): BOOL;
Если функция успешна, то возвращаемое значение ненулевое.
Если
функция неуспешна, то возвращаемое
значение равно 0.
5.ГРАФИЧЕСКОЕ ОТОБРАЖЕНИЕ ВЗАИМОДЕЙСТВИЯ ПОТОКОВ И ОБМЕНА ДАННЫМИ МЕЖДУ НИМИ
6. ОПИСАНИЕ ОСНОВНЫХ СОБЫТИЙ ПРИЛОЖЕНИЯ
При создании главной формы (первый поток) создается и захватывается объект синхронизации – критическая секция. Последовательно создается второй поток, который создает именованный канал, через этот канал будет осуществляться обмен данными между потоками, и ждет подключения первого потока. Первый поток подключается к именованному каналу. Так как объект синхронизации захвачен, второй поток не выполняет никаких действий, ждет освобождения критической секции.
После вода данных пользователем, первый поток записывает параметры запроса и необходимую для проведения расчетов информацию в именованный канал. Всё это время второй поток пребывает в состоянии ожидания.
После
записи необходимых данных в канал
первый поток освобождает критическую
секцию, давая возможность захватить объект
синхронизации второму потоку. Второй
поток захватывает критическую секцию.
Первый поток после этого переходит в
режим ожидания. Второй поток последовательно
считывает данные для осуществления запроса.
После чего обрабатывает их и записывает
в канал результаты работы. Затем он освобождает
критическую секцию, давая возможность
захватить объект синхронизации первому
потоку. Первый поток тут же его захватывает
. Второй поток переходит в режим ожидания.
Первый поток последовательно считывает
результаты из канала и выводит их
на экран, после чего программа переходит
в исходное состояние ожидания команд
пользователя.
7.РЕЗУЛЬТАТЫ ТЕСТОВЫХ ПРОГОНОВ
Рисунок
1. Главное окно
Рисунок
2. Окно после запроса на “Кальций”
Рисунок
3. Окно для добавления
Рисунок
4. Окно для изменения
Рисунок
5. Окно после добавления
Рисунок
6. Окно после удаления “Анальгин”
Рисунок
7. Неправильно введено поле: цена
Рисунок
8. Неправильно введено поле: Номер аптеки
8.ЛИСТИНГ ПРОГРАММЫ
8.1 UNIT1
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, Grids, StdCtrls,
ExtCtrls;
type
TForm1 = class(TForm)
StringGrid1: TStringGrid;
Izmenit: TButton;
Del: TButton;
Dobavit: TButton;
Panel1: TPanel;
Zapros: TButton;
Edit1: TEdit;
procedure FormCreate(Sender: TObject);
procedure StringGrid1Click(Sender: TObject);
procedure IzmenitClick(Sender: TObject);
procedure ZaprosClick(Sender: TObject);
procedure DobavitClick(Sender: TObject);
procedure
DelClick(Sender: TObject);
private
{ Private declarations }
public
{ Public
declarations }
end;
function Zaproc():integer;
procedure Vivod;
type
Apteka=record