Многопоточность в своих приложениях. Часть 1

Источник: webdelphi

Источник: webdelphi

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

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

Благо еще с WindowsNT и Delphi 6, у нас есть возможность простой и удобной реализации многопоточности. В Delphi существует две возможности работы с потоками:

1.    Взаимодействие через идентификатор, полученный при создании потока функцией createthread.

2.    Создание потомка класса TThread и использование его методов.

1-ый способ, в силу особенностей работы потоков, и синхранизации их с главным потоком, как правило не удобен и потому используется не часто. Поэтому мы воспользуемся более наглядным и простым в реализации 2-ым способом.

Работа с TThread

Первое и самое важное что необходимо помнить, то что каждый поток, в том числе и главный, выполняется в отдельном адресном пространстве. Это вовсе не означает что в потоке нельзя использовать константы, переменные и даже функции объявленные в модулях программы. Без их использования тело потока выросло бы до непростительно больших размеров. Однако необходимо помнить что при инициализации потока, в его область памяти, помещаются все используемые им переменные со своими начальными значениями, и их изменение ни каким образом не влияет на содержимое их аналогов в других потоках. Более того все методы самого TThread, которые находятся в адресном пространстве родительского потока, тоже "вне его компетенции". Тут вполне логично появление вопроса, зачем вообще нужен объект, который не может повлиять ни на работу программы, ни даже на самого себя в программе?

Ответ элементарный до смешного - "не нужен". Видимо поэтому, разработчики любезно предоставили нам возможность передачи переменных в поток через его метод Create, просто объявив их в методе потомка.

{...................}

constructor Create(CreateSuspennded: Boolean; MyVar: Integer); override;

{...................}

( CreateSuspennded переменная объявленная в методе TThread, о ее назначении чуть ниже. )
Не стоит забывать что использовать такую возможность, для каждого потока, можно лишь однажды - при его создании. Вопрос с вводом исходных данных в поток решен, а как быть с выводом? И тут все просто. Передавать потоку следует не сами переменные (как мы помним, их изменение будет отражаться лишь внутри потока), а указатель на адрес в памяти, pointer, по которому находится переменная. И работать с переменной (тип переменной значения не имеет), используя этот указатель. Удобнее всего создать для нее "сиамского близнеца", "поселив" свою переменную того же типа, по тому же адресу. Простой пример:

{...................}

var

  list1, list2: tstrings;

begin

  list1 := TStringList.Create;        //создаем первый лист

  pointer(list2) := pointer(list1);   //меняем адрес у лист2 на лист1

  list2.Add('Hello World!');           //добавляем строку вроде-бы как во второй лист

  ShowMessage(list1.strings[0]); //выводим содержимое из первого листа, оно-же содержимое второго)

  list2.destroy;                         //уничтожаем второй лист, он же первый, повторный вызов предыдущей строки вызовет ошибку

end;

{...................}

Основной принцип работы с данными вродебы понятен. Вводим данные с помощью pointer, обрабатываем и выводим. Стоп, а как обрабатывать-то?)) И опять все очень просто. При создании потока, в его тело помещается его же метод Execute. Вот в нем то и нужно производить все необходимые вычисления. И снова предостережение! Вызов метода Execute напрямую не запустит новый поток а просто выполнит его в текущем, что в большинстве случаев хоть и не вызовет ошибки, но как минимум "подвесит" программу на время его выполнения. Чтобы запустить этот метод в отдельном потоке, необходимо вызвать другой его метод, метод Resume который проделывает всю необходимую работу по инициализации и запуску потока. В случае если необходимо временно приостановить работу потока используется метод Suspend. При этом поток будет находится в состоянии "Паузы", до повторного вызова Resume.
Вернемся к конструктору, который уже упоминался выше. Изначально, для TThread, он выглядит так:

constructor Create(CreateSuspennded: Boolean);

Параметр CreateSuspennded предназначен, как следует из названия, для создания потока в приостановленном виде. Для чего это нужно? Для задания некоторых методов создаваемого экземпляра класса. Например, такого как FreeOnTerminate: Boolean который, также следуя из названия, указывает классу на необходимость высвобождения памяти, занимаемой потоком после его уничтожения.

Ну, вроде все нюансы, по работе с данными, решены. Обрабытавать данные в потоке, мы уже умеем. Можно идти дальше? Можно, но есть еще один момент, забывать о котором нельзя ни в коем случае, это "совместный доступ к памяти". Вероятность того что несколько разных потоков (включая главный) одновременно будут работать с одной переменной в отдельных случаях достаточно мала, но когда это происходит наступает "время чудес", и "истина" с завидной легкостью обращается в "ложь".))) И на деле это совсем не смешно.
Это тот подводный камень, который при отладке зачастую не вызывает никаких ошибок и редко дает о себе знать. Поверьте конечному пользователю, по общеизвестному закону, "волшебство" обеспечено. Но только в том случае, если у разработчика достаточно кривые руки, что-бы он не позаботился об организации корректной совместной обработки данных в потоках.

Для того что-бы сообщить, вызвавшему потоку, об определенном этапе работы потока используется метод Synchronize(AMethod: TThreadMethod), вызываемый внутри потока. При этом параметром AMethod выступает метод нашего потомка TThread не имеющий параметров. Этот метод будет выполнен в "родительском" потоке, при этом значение свойств объекта TThread будут синхронизированы для этих потоков, а работа нашего потока приостановлена до завершения метода Synchronize. Таким образом можно сообщать программе о возникающих ошибках, передавать промежуточные значения вычислений, сообщать о завершении работы потока, а также вносить коррективы в его работу. После завершения метода синхронизации поток возвращается к своей работе, если конечно в Synchronize он не был приостановлен или уничтожен)).

Следует отметить что уничтожение потока очень грубая и крайняя мера. Но иногда приходится идти и на это, так-что вот вам функция для уничтожения:

function TerminateThread(hThread: THandle; dwExitCode: DWORD): BOOL;

Хэндл потока можно получить методом ThreadID, а в dwExitCode достаточно указать "0′, в случае успешного уничтожения функция вернет True.

Корректно завершить работу потока можно лишь дождавшись окончания работы метода Execute. Но что если нам не очень хочется ждать пока поток полностью отработает свою "программу". Для этого предусмотрен метод Terminate. Но представьте себе, не смотря на свое звучное название, он не делает ничего для завершения потока, ну или почти ничего). Все что он делает это задает свойству Terminated, нашего компонента в потоке, значение True. И что же это дает? Да практически все что нам необходимо. Достаточно в каждом затяжном цикле, или после особо продолжительных функций и процедур, вставить проверку значения этой переменной, и вслучае если она равна True завершать работу корректно, высвободить память от созданных объектов, закрыть открытые сетевые подключения, или просто сразу вызвать Exit.

Пример реализации потока. Компонент TDownloader

В качестве примера выбран компонент для скачивания файлов из Internet, в связи с продолжительностью выполнения сетевых запросов.

Для скачивания файла в поток необходимо передать как минимум два значения - Урл файла и Контейнер для полученных данных. Ссылку логично передавать в переменной типа String, а для контейнера лучше использовать TMemoryStream, так-как он хранит данные в неизменном виде, и позволяет легко выводить их в файл или TStrings. Но для совместной обработки контейнера передаем только указатель на него.

{....................}

type

  PMemoryStream = ^TMemoryStream;

{....................}

private

    fURL: String; //урл

    MemoryStream: TMemoryStream; //наш "Сиамский близнец"

{....................}

public

    constructor Create(CreateSuspennded: Boolean; const URL: String; Stream: PMemoryStream);

{....................}

constructor TDownloadThread.Create(CreateSuspennded: Boolean; const URL: String; Stream: PMemoryStream);

begin

  inherited Create(CreateSuspennded); //метод предка

  FreeOnTerminate := True; //очистка при уничтожении

  Pointer(MemoryStream) := Stream; //прописываем memorystream по полученному адресу

  fURL := URL; //запоминаем урл

end;

{....................}

Далее определяемся с самой скачкой, пишем метод Execute.

{....................}

procedure TDownloadThread.Execute;

var

  pInet, pUrl: Pointer; //дескрипторы для работы с WinInet

  Buffer: array[0..1024] of Byte; //буфер для получения данных

  BytesRead: Cardinal; //количество прочитанных байт

  i: Integer;

begin  //тело потока

  pInet := InternetOpen('Dowloader', INTERNET_OPEN_TYPE_PRECONFIG, nil, nil, 0);//открываем сессию

  try

    pUrl := InternetOpenUrl(pInet, PChar(URL), nil, 0, INTERNET_FLAG_PRAGMA_NOCACHE or INTERNET_FLAG_RELOAD, 0);//стучимся по ссылке.

    repeat //качаем файл

      if Terminated then //если поток необходимо завершить

        Break;               //в данном случае просто выходим из цикла

      FillChar(Buffer, SizeOf(Buffer), 0); //заполняем буфер нолями

      if InternetReadFile(pUrl, @Buffer, Length(Buffer), BytesRead) then //читаем кусок в буфер

        MemoryStream.Write(Buffer, BytesRead) //пишем буфер в поток

    until (BytesRead = 0); //прочитано все

      MemoryStream.Position := 0; //позицию потока в ноль

  finally

    if pUrl  nil then //открывалось?

      InternetCloseHandle(pUrl); //закрываем

    if pInet  nil then //открывалось?

      InternetCloseHandle(pInet); //закрываем

  end;

  pointer(MemoryStream) := nil; //обрываем связь

end;

{....................}

Вроде-бы все? Все да не все), а как-же Synchronize? А их я написал немного много), и решил сюда не выкладывать. Так-что для завершения изучения данного вопроса, качайте файлик. Кроме самого компонента в архиве пример его использования, и пример работы с pointer описанный выше.


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=23280