Delphi programming blog
Источник: http://teran.karelia.pro/articles/item_4501.html
 

Проблема с потоками (TIdHTTP?).

Опубликовано 25.12.2011 г. 23:26

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

Суть проблемы такова. Имеется 3 модуля: 1. форма 2. Класс представлющий список сериалов и т.п. и реализующий вызовы к API сервиса. 3. Класс реализующий API сервиса, и класс потока для использования API. Первый модуль формы ничего по сути не делает. просто запускает работу, и сигнализирует о том, что потоки завершены:
procedure TForm2.FormCreate(Sender: TObject);
begin
        FShows := TMyShows.Create();
        FShows.OnShowsLoaded := ShowsLoaded;
        FShows.OnUnwatchedUpdate := UnwatchedUpdate;
        FShows.Load();
end;

procedure TForm2.ShowsLoaded(sender: TObject);
begin
    button1.Caption := 'shows loaded';
end;

procedure TForm2.UnwatchedUpdate(sender: TObject);
begin
    button2.Caption := 'unwatched episodes loaded';
end;
В обработчике события создания формы создается экземпляр объекта TMyShows. Назначаются 2 обработчика событий - событие того, что список сериалов получен от сервиса, и событие - от сервиса получены непросмотренные эпизоды. Сами обработчики просто, как видно, меняют надписи на кнопках, говоря нам о том, что поток завершен. Поскольку данные получаются от web-сервиса, то работа по их получению была вынесена в дополнительные потоки, чтобы из за таймаутов программа не подвисала, как это делает старая версия, которая однопоточна. Старт потоков вызовов к API происходит в FShows.Load(). Модуль №2. Управление сериалами - TMyShows. Тут осталось всего три метода. Первый - Load, который запускает потоки вызовов API. Вторые два метод SetShows & SetUnwatched() эмулируют что новые данные загружены, и соответственно вызывают события главной формы (код подчищен):
procedure TMyShows.SetShows();
begin
    if assigned(FOnShowsLoaded) then
        FOnShowsLoaded(nil);
end;

procedure TMyShows.SetUnwatched();
begin
    if assigned(FOnUnwatchedUpdate) then
        FOnUnwatchedUpdate(nil);
end;
Теперь метод Load(). Он создает два потока - TApiThread. Поток имеет свойство DoneProc - анонимный метод, который вызывается (в главном потоке, используя Synchronize), при завершении работы потока:
procedure TMyShows.Load();
var at : TApiThread;
begin
    at := TApiThread.Create();
    at.NameThreadForDebugging('Shows thread');
    at.DoneProc := procedure(data : pointer)
            begin
                SetShows();
            end;

    at.Start();

    at := TApiThread.Create();
    at.NameThreadForDebugging('unwatched thread');
    at.DoneProc := procedure(data : pointer)
            begin
                SetUnwatched();
            end;
    at.Start();
end;
Т.е допустим первый поток стартовал, получил нужные данные от сервиса, и потом в главном потоке выполнил DoneProc, которая в свою очередь вызывает SetShows, которая уже уведомляет форму, что надо бы что данные получены, обработаны, и можно обновить интерфейс. Третий модуль - доступ к API. Здесь находится два класса - первый TShowsApi, просто описывает(ал) все возможные вызовы к API. Для доступа к API он использует TIdHttp. Поскольку работа ведется в несколько потоков, то имеется общий TIdCookieManager, которые хранит куки сессии. Общим он является за счет того, что объявлен классовой переменной (после урезания кода, переменная эта нигде не инициализируется, и не используется, но если ее закомментировать, то ошибка исчезает). Конструктор создает рабочий экземпляр TIdHttp, который разрушается в деструкторе:
    TShowsAPI = class(TObject)
      strict private
        class var FCookie : TIdCookieManager;
        FHttp : TIdHttp;
      public
        constructor Create();
        destructor Destroy(); override;
    end;
............

constructor TShowsAPI.Create();
begin
    inherited Create();
    FHttp := TIdHttp.Create(nil);
end;

destructor TShowsAPI.Destroy();
begin
    FHttp.Free();
    inherited;
end;
Последняя составляющая - поток. В своем распоряжении поток имеет во-первых - экземпляр TShowsApi, и во-вторых метод DoneProc (TApiDoneProc), который надо вызвать по завершении:
    TApiDoneProc = reference to procedure(data : pointer);

    TApiThread = class (TThread)
      strict private
        FApi : TShowsApi;
        FData : pointer;

        FDoneProc : TApiDoneProc;
        procedure Done();
      public
        constructor Create();
        procedure Execute(); override;

        property DoneProc : TApiDoneProc read FDoneProc write FDoneProc;
        property Api : TShowsApi read FApi;
    end;
Создается поток спящим, в Execute создается экземпляр TShowsApi, потом якобы у нас была полезная работа по обращению к сервису, потом мы вызываем DoneProс. Последний анонимный метод обернут в TApiThread.Done, чтобы его было можно использовать в Synchronize:
constructor TApiThread.Create();
begin
    inherited Create(true);
    FreeOnTerminate := true;
end;


procedure TApiThread.Execute();
begin
    FApi := TShowsApi.Create();
    //FData := FApi.getSomething()

    if assigned(FDoneProc) then begin
        Synchronize(Done);
    end;

    FApi.Free();
end;

procedure TApiThread.Done();
begin
    FDoneProc(FData);
end;
Вроде как бы все нормально. Но при работе возникает ошибка - Invalid Pointer Operation. Судя по отладчику возникает она, когда завершается второй поток и в конце Execute уничтожает свой экземпляр TShowsApi (FApi.Free). В свою очередь внутри деструктора FApi исключение происходит при уничтожении экземпляра FHttp : TIdHttp. По отладчику ошибка возникает после вызова деструктора TIdCustomHTTP в _ClassDestroy (system.pas) -> Instance.FreeInstace() -> _FreeMem(self), где вызов MemoryManager.FreeMem возвращает ошибку. Как я уже говорил, если закомментировать неиспользуемый FCookie : TIdCookieManager (общая классовая переменная для использования между потоками) то ошибки нет. Впрочем, если закомментировать создание/разрушение FHttp : TIdHttp, то ошибки тоже нет. Третья ситуация, при которой исчезает ошибка - закомментировать вызов Synchronize(Done). Однако тут стоит отметить, что если закомментить только внутренность Done - вызов FOnDone(FData), то ошибка остается. Перерыл несколько страниц гугла, но что то не нашел ответа. Сейчас использую XE2, мб в этом причина? Завтра попробую на работе проверить, что получится, там 2010я. Вот что то и не понятно мне в чем причина такого поведения. К статье прилагаю исходник VCL проекта (пространства имен убраны, так что код должен работать в любой версии Delphi, которая знает об анонимных методах), который сию проблему иллюстрирует: исходный код проекта Буду благодарен за советы в решении проблемы (: Обновление решение проблемы оказалось тривиальным до невозможности. Дело, как это часто бывает, в самой обычной невнимательности. Если посмотреть на начало объявления класса TShowsApi -
    TShowsAPI = class(TObject)
      strict private
        class var FCookie : TIdCookieManager;
        FHttp : TIdHttp;
то видно что переменная FCookie объявлена как class var, т.е классовая переменная. Но вот тут то ошибка и находится - секция class не заканчивается на одной этой переменной, и FHttp тоже является классовой. Так что все что нужно сделать - завершить секцию class, которая в свою очередь оканчивается при следующих условиях:
  1. Начинается секция var или другая секция class var
  2. Встречается объявление метода или функции (в т.ч классового)
  3. встречается объявление свойства (в т.ч. классового)
  4. Встречается объявление конструктора или деструктора
  5. указатель области видмости private/protected/public/published
Владу спасибо, что откликнулся (:
Метки:  threads  |  error  |  idhttp  |  anonymous methods 

Комментарии

Vlad
26.12.2011 в 16:37
Скачал исходник, собрал и запустил проект - никаких ошибок, Caption кнопок нормально изменились...Вывел на OnClick кнопки:
procedure TForm2.Button1Click(Sender: TObject);
begin
  FShows.Load();
end;

Запустил, кликнул - в дебаггере видно, что два потока запускаются и успешно завершаются...ter, надо больше информации по ошибке, т.к. пока она невоспроизводима. У меня Delphi XE2 Architect Update 3. Версия Indy та, которая идет по дефолту с Delphi..по-моему 10.5.7
ter
26.12.2011 в 16:09
Влад, версия делфей у меня такая же. ты запускал с отладкой или без? Хотя коли говоришь что в дебаггере видно, значит с отладкой.

странно вобще. у меня просто валетает с эксепшеном. Потоки то как бы отрабатывают, надписи меняются, а потом уже при уничтожении выпадет EInvalidPointer. он у тебя случаем в игнор не попал? :)

зы: на обоих тестовых машинах Win 7 x32,
если запускать вне среды, то ошибок не видать никаких.
Vlad
26.12.2011 в 17:24
ter, кто по-твоему будет EInvalidPointer в игнор ставить? =) У меня вообще игнор-лист пустой. Правда Win 7 x64 стоит, но, думаю, что не в этом дело...
ter
26.12.2011 в 19:50
Странно конечно тогда. в чем же может быть подвох то.
ты в аське бываешь часто или не? :)
Vlad
26.12.2011 в 19:39
я в основном в скайпе сейчас по работе нахожусь. QIP переустановил и оказалось, что все контакты похерились куда-то =(
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно