Подписка

добавить на Яндекс

Наши проекты

Delphi+Google

Google API

Google API в Delphi - проект с открытым исходным кодом.

Chrono

Chrono

Хронометр - программа для ведения списка задач.

ODFProc

ODFProc

ODFProc - работа с документами OpenOffice в Lazarus и FreePascal.

Поддержка блога

А тут я коплю на лицензию Delphi XE на iPad =).
Сумма пожертвования не фиксирована.

Публикации

Год назад

Случайный пост

Последние

Сообщения форума

Комментарии

Свежие комментарии

Социальные сети

Google

Facebook

Twitter

Опрос

Вы сейчас или в ближайшем обозримом будущем планируете разрабатывать кроссплатформенное приложение с использованием Firemonkey?



Loading ... Loading ...

Блоги и сообщества

Статьи по Delphi DelphiFeeds.ru - Все Delphi-блоги Рунета Сообщество умных людей VR-Online.RU Бесплатный журнал для программистов и всех, кто интересуется IT Статьи и уроки по Delphi Новостной блог о высоких технологиях
Система Orphus
Опубликовал SeregaAltmer 15 октября 2009 в 23:00.
Категории: Delphi в Web, Компоненты Delphi, Основы Delphi.


matrix1_200X191Достаточно давно прошло то время, когда использование многопоточности в любых серьезных программах считалось хорошим тоном. На сегодняшний день, это необходимость от которой очень многое зависит, и в первую очередь - удобство использования приложения. Почти любой современный программный продукт (хоть исключения и возможны, я таких примеров не знаю, тот-же стандартный "калькулятор" при расчетах использует 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 описанный выше.
zip Компонент TDownloader(483.76 KB)

-------------------
Решили купить ноутбук? Если хотите знать, где находятся самые лучшие и недорогие ноутбуки в Санкт-Петербурге, то обязательно загляните на landcom.ru. Здесь вы найдете самые различные ноутбуки от двенадцати производителей, таких как Asus, Sony, Apple и других.
-------------------
Понравилась статья? Тогда:
Делись! Загружай! Плюсуй!
   Отправить PDF на   
Читай ещё статьи на WebDelphi.ru

Комментарии (5)

WP_Cloudy
  • Евгений пишет:

    Статья хорошая, но я так и не понял — зачем нужны эти пляски с бубном с указателями ? Ведь все экземпляры объектов и так указатели. И вызов list2 := list1 не будет ни чего копировать, а просто сделает то что вы явно указываете — pointer(list2) := pointer(list1). И смысла в «сеамских» близнецах просто нету. Вы просто усложняете код жтим конструкциями.

  • Юрик пишет:

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

  • Александр пишет:

    я тоже не врубаюсь ,где найти инфу по потокам

  • Wlad пишет:

    Или автор косноязычен, или — не в курсе.
    Есть не просто «перлы», но даже части стаьи друг другу противоречат.
    «Первое и самое важное что необходимо помнить, то что каждый поток, в том числе и главный, выполняется в отдельном адресном пространстве.»
    и
    «Можно, но есть еще один момент, забывать о котором нельзя ни в коем случае, это «совместный доступ к памяти».»
    Вы решите, что всё-таки у вас в памяти на самом деле происходит.
    К тому же метод Synchronize — скорее дань корявости реализации многопотоковости и эксклюзивного доступа к одним и тем же переменным в памяти из разных из потоков.
    Дальше.
    Лучше не говорить про отношения с «родительским» потоком, а говорить «главный поток» вин-приложения. Вот в этом «взаимодействии» и вылезает корявость реализации параллелизма и синхронизации доступа в винде. Потому, что корни её растут ещё из времён, когда многозадачность в виндоузЪ была «на добровольно-передаточных» принципах. Серьёзно над пересмотром синхронизации доступа к графическим объектом никто не поработал и решили пойти самым деревянным путём — весь доступ обеспечивать из одного (главного) потока. Такое решение, как минимум лет на 35 уже отрасль затормозило и прикладные решения в винде.
     
     
     

  • SeregaAltmer пишет:

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

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

    Заодно обещаю новые свершения, а именно:

    - Компонент TOption2, принципиально и качественно отличающийся от первой версии. Как раз сейчас я над ним активно (хоть и крайне редко :) ) работаю.

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

    - Напишу таки небольшой пост про какой-нибудь свой скромный компонент-погремушку из малостроккода. Времени на что-то серьезное все одно нету, а при написании сего комента что-то руки зачесались хоть что-нибудь уже напостить :).

    А Wlad действительно прав, виндоузЪ очень плохая операционная система.

Ваш ответ

Внимание: Все комментарии модерируются, и это может вызвать задержку их публикации. Отправлять комментарий заново не требуется.

Пожалуйста, заключайте исходный код в тэги [code][/code].
Если код большой, то воспользуйтесь Вставкой кода на отдельной странице и оставьте в комментарии ссылку на исходник

   


Тестировали ли вас при приеме на работу, да - нет - не работаю.