Многопоточность в своих приложениях. Часть 2.Источник: webdelphi
Источник: webdelphi В предыдущей статье, в примере, я не стал описывать принцип работы с методом Synchronize, и как я теперь понимаю напрасно. Конечно, я выложил архив с примером, где этот метод встречается в нескольких местах, и конечно эти моменты были мной прокомментированы. Однако есть несколько нюансов, касающихся синхронизации, да и не только ее, о которых мне все же следовало упомянуть в статье, а не в нескольких строках кода. В этой статье я попытаюсь восполнить этот пробел. СинхронизацияПрежде чем добавлять в любой поток методы синхронизации, нужно четко определиться, на каких этапах работы потока и с какой целью это необходимо. И предварительно подготовить сами методы синхронизации. Вернемся к нашему примеру компонент TDownloader, его основная задача - скачивание одиночного файла из интернета по заданному URL. Все базовые процедуры для работы с интернетом работают по простому принципу - "Отправили запрос, получили ответ, вернули результат, или ошибку в случае неудачи". Можно было бы остановиться на таком же принципе, и после обработки запроса по скачиванию файла просто возвращать результат. Однако процесс скачивания файлов дело продолжительное, а значит банальная надпись "Подождите качаю" нас едва ли устроит. Хотелось бы так сказать какой-нибудь информации, о том, на каком конкретно этапе находится скачка. Сам по себе поток работает не заметно, и на работе программы его труды ни как не отражаются. А значит, нужна какая-то система "оповещения" о текущем состоянии процесса скачки. Тут-то нам и пригодится замечательный метод Synchronize. Смотрим внимательно на наш алгоритм получения файла из интернета, и делим его на этапы, по которым потоку необходимо отчитываться перед программой, чтобы та реагировала сама, и давала необходимую информацию пользователю. Самой первой строкой, поток открывает сессию функцией InternetOpen, которая в случае успешного выполнения возвращает идентификатор сессии, либо nil в случае неудачи. Далее аналогичная функция InternetOpenUrl. На данный момент нет смысла отчитываться по каждой успешно произведенной операции, порой подобная информативность может оказаться даже излишней. А вот об ошибках поток сообщить обязан. Причем, как пользователю, так и программе важен не только факт ошибки, но и причины по которым не может быть завершена выполняемая операция. А значит, нам нужен метод синхронизации сообщающий об ошибке и о причинах ее возникновения. Как я упоминал ранее, любой метод, предназначенный для синхронизации, не может содержать параметры, а значит, кроме самого метода нам еще понадобится переменная с кодом возникшей ошибки. Лучше всего объявить для нее отдельный тип, пока он будет состоять только из двух типов ошибок: TDownloadError = (deInternetOpen, deInternetOpenUrl); Далее объявляем переменную и сам метод в секции private: err: TDownloadError; procedure toError; Что конкретно должен делать этот метод? Можно было бы сразу выводить сообщение пользователю, или записать сообщение об ошибке в лог-файл. Но мы пишем не конечную программу, а компонент, а значит, что делать с ошибкой решать программе. А значит нам нужно объявить событие OnError. Type {......................} TErrorEvent = procedure(Sender: TObject; E: TDownloadError) of object; {......................} private fOnError: TErrorEvent; {......................} public property OnError: TErrorEvent read fError write fError; Теперь нам ничего не стоит дописать метод toError: procedure TDownloadThread.toError; begin if Assigned(fError) then OnError(Self, err); end; Вносим изменения в метод Execute: {..........................} pInet := InternetOpen('Dowload Master', INTERNET_OPEN_TYPE_PRECONFIG, nil, nil, 0); if pInet = nil then //если функция вернула ошибку begin err := deInternetOpen; //ложим сообщение об ошибке в переменную Synchronize(toError); //синхронизируем потоки Exit; //завершаем поток end; try pUrl := InternetOpenUrl(pInet, PChar(URL), nil, 0, INTERNET_FLAG_PRAGMA_NOCACHE or INTERNET_FLAG_RELOAD, 0); if pUrl = nil then //если функция вернула ошибку begin err := deInternetOpenUrl; //ложим сообщение об ошибке в переменную Synchronize(toError); //синхронизируем потоки Exit; //завершаем поток end; Как видите ничего сложного. Кода, конечно, стало гораздо больше, но это легко устранимый недостаток. Как сократить данную запись синхронизации до одной простой строки вы можете посмотреть в архиве, приложенном к статье. А пока двигаемся дальше по методу Execute, и видим простой цикл скачивания файла по частям. {.............................} 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); {.............................} Напомню, ранее мы уже добавили в него проверку свойства Terminated, на случай если работу потока захочет прервать пользователь или программа. И тут у нас снова функция для работы с сетью, которая в случае ошибки возвращает False, дописываем сюда наш метод обработки ошибок. if InternetReadFile(pUrl, @Buffer, Length(Buffer), BytesRead) then MemoryStream.Write(Buffer, BytesRead) else begin // если функция вернула ошибку err := deDownloadingFile; // ложим сообщение об ошибке в переменную Synchronize(toError); //синхронизируем потоки Exit; //Завершаем поток end; И не забудем добавить новое значение ошибки в наш "ошибочный" тип: TDownloadError = (deInternetOpen, deInternetOpenUrl, deDownloadingFile); Чтение файла, в нашем потоке, самый продолжительный процесс. И было бы неплохо разделить и его на этапы. Хоть в данном примере мы и не знаем общего размера скачиваемого файла, все же мы можем выводить информацию о уже скачанном объеме. Для этого достаточно в конце цикла вставить новый метод синхронизации, который мы сейчас и напишем, по аналогии с предыдущим. Type {................................} TDownloadingEvent = procedure(Sender: TObject; AccepteSize: Cardinal) of object; {................................} private AccepteSize: Cardinal; fDownloading: TDownloadingEvent; procedure toDownloading; {................................} public property OnDownloading: TDownloadingEvent read fDownloading write fDownloading; {................................} procedure TDownloadThread.toDownloading; begin if Assigned(fDownloading) then fDownloading(Self, AccepteSize); end; {................................} И вносим изменения в Execute, тут нам достаточно дописать всего одну строку: if InternetReadFile(pUrl, @Buffer, Length(Buffer), BytesRead) then MemoryStream.Write(Buffer, BytesRead) else begin err := deDownloadingFile; Synchronize(toError); Exit; end; Synchronize(toDownloading); // Синхронизируем методом toDownloading Осталось только добавить событие на окончание закачки. А вариантов окончаний у нас может быть целых 2. В первом случае скачивание завершается, с окончанием файла, а во втором по желанию пользователя (или программы), ведь мы предусмотрели возможность прерывания цикла закачки. {...................................} repeat if Terminated then Break; {...................................} А значит нам нужно еще два новых события. Для них можно не создавать отдельный тип, поскольку они не подразумевают ни каких параметров. Воспользуемся стандартным TNotifyEvent. {...................................} private fAccepted: TNotifyEvent; fBreak: TNotifyEvent; procedure toAccepted; procedure toBreak; {...................................} public property OnAccepted: TNotifyEvent read fAccepted write fAccepted; property OnBreak: TNotifyEvent read fBreak write fBreak; {...................................} procedure TDownloadThread.toAccepted; begin if Assigned(fAccepted) then fAccepted(Self); end; procedure TDownloadThread.toBreak; begin if Assigned(fBreak) then fBreak(Self); end; {...................................} Завершаем Execut: {...................................} if Terminated then Synchronize(toBreak) else Synchronize(toAccepted); end; {...................................} Теперь наш поток научился отчитываться в своих действиях. Однако использовать его в программе все еще неудобно. Так как класс TThread, не является компонентом (в модуле Classes он объявлен как "TThread = class"), мы не сможем добавить его потомка в палитру. А значит, все обработчики событий нам придется прописывать вручную, каждый раз при его использовании. Чтобы устранить этот недостаток напишем класс "посредник", который сделаем потомком TComponent. Компонент посредникВ этом компоненте нужно продублировать все объявленные события, а также для удобства добавить несколько свойств и методов. {......................................} TDownloader = class(TComponent) private Downloader: TDownloadThread; fOutStream: TMemoryStream; fURL: string; fOnError: TErrorEvent; fOnAccepted: TNotifyEvent; fOnBreak: TNotifyEvent; fOnDownloading: TDownloadingEvent; public procedure Download; procedure BreakDownload; property OutStream: TMemoryStream read fOutStream; published property URL: string read fURL write fURL; property OnError: TErrorEvent read fOnError write fOnError; property OnAccepted: TNotifyEvent read fOnAccepted write fOnAccepted; property OnBreak: TNotifyEvent read fOnBreak write fOnBreak; property OnDownloading: TDownloadingEvent read fDownloading write fDownloading; end; {......................................} procedure TDownloader.Download; begin if Assigned(Downloader) then Downloader.Terminate; if Assigned(fOutStream) then FreeAndNil(fOutStream); fOutStream := TMemoryStream.Create; Downloader := TDownloadThread.Create(True, URL, Pointer(fOutStream)); Downloader.OnError := OnError; Downloader.OnAccepted := OnAccepted; Downloader.OnBreak := OnBreak; Downloader.OnDownloading := OnDownloading; Downloader.Resume; end; procedure TDownloader.BreakDownload; begin if Assigned(Downloader) then Downloader.Terminate; end; {......................................} К слову, напомню, что конструктор у TDownloadThread объявлен как: constructor Create(CreateSuspended: Boolean; const URL: String; Stream: PMemoryStream); Где CreateSuspended в случае если оно равно True, не дает запуститься потоку до выполнения метода Resume. Чем мы и пользуемся для задания обработчиков событий. После чего запускаем сам поток. А в процедуре BreakDownload, и вовсе все просто, она всего лишь сообщает потоку методом Terminate, что ему необходимо завершить свою работу. Добавлю, что метод Synchronize приостанавливает работу потока, однако с Delphi 2009, появился новый метод для синхронизации Queue, который не дожидаясь окончания синхронизации возобновляет работу потока. Такая необходимость, как правило, возникает не часто, но когда она возникает, этот метод помогает избавиться от не нужного простоя потока. |