Поиск:

Ответ в темуСоздание новой темы Создание опроса
> VCL и многопоточность, Вопросы безопасного отображения 
:(
    Опции темы
_Tanatos_
Дата 30.11.2007, 01:50 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 28
Регистрация: 20.11.2007

Репутация: нет
Всего: нет



Помогите решить задачку:
Есть класс отвечающий за ведение журнала (лога). Работает он следующим образом:
- в процессе инициализации класса ему передаются указатели на объекты "подписчики"
- когда вызывается функция регистрации нового сообщения поочередно вызывается функция для каждого подписчика. В эту функцию передается ссылка на структуру с данными сообщения.

В моем случае имеется один подписчик (класс TForm, для этого он наследуется он класса "подписчика").
Функция OnLogAdd(...) соответственно вызывается при регистрации нового сообщения в журнале. Ее задача отображать на экране все сообщения (аля консоль). Для отображения я использую TTree. 

В программе могут стартовать и останавливаться потоки выполняющие некоторые вычисления. И основной поток и вспомогательные ведут журнал при помощи глобального объекта (того самого класса журнала). Сам класс вродебы потокобезопасный, во всяком случае с его стороны глюков не ловил.

Проблема заключается в том, что программа периодически зависает. Причиной зависаний 99% работа с TTree, во всяком случае закоментировав работу с ним я не поймал ни одного зависания. Помогите как организовать потокобезопасный вывод?

Попробовал такой вариант:
в классе TForm написал функцию void Update() в которую вынес все что касается обновления дерева с журналом.
в функцию OnLogAdd(...) поместил только сохранение копии сообщения (которая далее используется в Update()
В потоке, после записи сообщения в журнал, принудительно вызываю Synchronize(Form1->Update);
Так же после записи сообщения в журнал в основном потоке вызываю Update()
Все отлично работает за исключением того, что пихать повсюду Update() ИМХО некорректно.
И потом нет гарантий, что пока будет вызываться Update не будет перезаписана копия сообщения, т.е. реально можно просто потерять сообщение в журнале.

Может я немного путанно написал, ежели что непонятно, спрашивайте.
Мне очень важно найти решение этой задачи!
Надеюсь на Вашу помощь!

Код

// класс подписчик
class CtLibLogSubscribe
{
public:
    virtual void OnLogAdd(CtLibLogEngine* LogEngine, TtLibLogRecord& Record)=0;
};

// класс логгер (сокращенно)
class CtLibLogEngine
{
    inline void SubscriberAdd(CtLibLogSubscribe* Subscriber);
    inline void RegisterMsg(TtLibLogRecord& Record);
}

// Класс TForm (сокращенно)
class TForm1 : public TForm, CtLibLogSubscribe
{
public:
    void OnLogAdd(CtLibLogEngine* LogEngine, TtLibLogRecord& Rec);
}

// Реализация обработчика (в минимальном варианте)
void TForm1::OnLogAdd(CtLibLogEngine* LogEngine, TtLibLogRecord& Record)
{
    Tree_Log->Items->BeginUpdate();

    pChild = Tree_Log->Items->AddChild(NULL, (AnsiString)Record.m_RowID);

    pChild->ColumnText->Add(Record.m_Message.begin());
    pChild->ColumnText->Add((AnsiString)Record.m_ThreadID);
    pChild->ColumnText->Add((AnsiString)Record.m_ThreadName->begin());

    Tree_Log->Items->EndUpdate();
}

PM MAIL   Вверх
Artemon
Дата 30.11.2007, 11:06 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


а ты мне нравишься
***


Профиль
Группа: Завсегдатай
Сообщений: 1771
Регистрация: 24.2.2004
Где: Челябинск

Репутация: 7
Всего: 20



1. Если обращаешься к визуальным компонентам VCL, то обязательно нужно использовать Synchronize, что ты и сделал и от этого никуда не уйти.

2. Я так понял у тебя один объект и его могут менять все, на этот случай можно воспользоваться мнтодами синхронизации, например Mutex или Семафор.


--------------------
Контроль топлива на топливозаправщиках, мониторинг автотранспорта, расчет зарплаты водителей www.rscat.ru
PM MAIL   Вверх
Lazin
Дата 30.11.2007, 12:24 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Завсегдатай
Сообщений: 3820
Регистрация: 11.12.2006
Где: paranoid oil empi re

Репутация: 18
Всего: 154



без Synchronize никуда, так как компоненты VCL используют очередь сообщений
PM MAIL Skype GTalk   Вверх
_Tanatos_
Дата 30.11.2007, 13:01 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 28
Регистрация: 20.11.2007

Репутация: нет
Всего: нет



Не имею ничего против Synchronize, но как его грамотно использовать в моем случе?
Вот фрагмент исходного кода:
Код

//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
// Код потока
void __fastcall CThread::Execute()
{
    g_LogEngine->ReginsterThread("Thread");
    //---- Place thread code here ----
    while(!Terminated)
    {
        {
            CtLibLogHelper LogHelper(false);
            LogHelper.Add() << "ThreadTest" << loPost;
            Synchronize(Form1->Update);
            Sleep(10);
        }
    }

    FreeOnTerminate = true;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
// Код из основного процесса (обработчик отображения записи журнала)
void TForm1::OnLogAdd(CtLibLogEngine* LogEngine, TtLibLogRecord& Rec)
{
    EnterCriticalSection(&m_Lock);
    Record = Rec;
    LeaveCriticalSection(&m_Lock);
}
//---------------------------------------------------------------------------
// Код который работает с VCL
// изначально этот код был в OnLogAdd() 
// Соответственно никаких Synchronize() или Update() после записи в журнал я не вызывал
// Код этой функции урезал до предела (но суть и проблематика сохраняется)
void __fastcall TForm1::Update()
{
    Tree_Log->Items->BeginUpdate();
    pChild = Tree_Log->Items->AddChild(NULL, (AnsiString)Record.m_RowID);
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
// Код добавления записи в журнал
    inline void RegisterMsg(TtLibLogRecord& Record)
        {
            EnterCriticalSection(&m_Lock);
            // Инициализация системных полей
            ...
            LeaveCriticalSection(&m_Lock);

            // Оповещение всех подписчиков
            // Отсюда и вызывается функция основного потока для отображения на форме новой строчки журнала
            for(std::size_t i=0; i<m_Subscribers.size(); i++)
                m_Subscribers[i]->OnLogAdd(this, Record);

            EnterCriticalSection(&m_Lock);
            ...
            LeaveCriticalSection(&m_Lock);
        }


В результате
Код

вместо
            LogHelper.Add() << "ThreadTest" << loPost;
в основном потоке пишем
            LogHelper.Add() << "ThreadTest" << loPost;
            Update();
а в дочерних потоках:
            LogHelper.Add() << "ThreadTest" << loPost;
            Synchronize(Form1->Update);

Причем нет гарантии, что реальный вызов не будет таким:
main_thread                         LogHelper.Add() << "ThreadTest Main" << loPost;
thread_1                               LogHelper.Add() << "ThreadTest sub" << loPost;
thread_1                               Synchronize(Form1->Update);
main_thread                         Update();

в результате получим две одинаковые строчки:
12 "ThreadTest sub"
12 "ThreadTest sub"
вместо правильных
11 "ThreadTest Main"
12 "ThreadTest sub"
PM MAIL   Вверх
xvr
Дата 30.11.2007, 13:35 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Комодератор
Сообщений: 7046
Регистрация: 28.8.2007
Где: Дублин, Ирландия

Репутация: 48
Всего: 223



Цитата(_Tanatos_ @ 30.11.2007,  01:50)
Помогите решить задачку:
Есть класс отвечающий за ведение журнала (лога). Работает он следующим образом:
- в процессе инициализации класса ему передаются указатели на объекты "подписчики"
- когда вызывается функция регистрации нового сообщения поочередно вызывается функция для каждого подписчика. В эту функцию передается ссылка на структуру с данными сообщения.

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

Порядок действий (функция OnLogAdd):
1) Блокируется очередь (критической секцией или семафором, неважно)
2) Сообщение добавляется в конец очереди
3) Разблокируется очередь
4) Вызывается Synchronize(Stage2)

Фуннкция Stage2 (в классе логера):
1) Блокируется очередь
2) Из очереди вычитываются ВСЕ содержащиеся в ней сообщения, для каждого вызывается callback от подписчиков. (после этого очередь пуста)
3) Разблокируется очередь

Для очереди можно применить класс deque из stl

PM MAIL   Вверх
_Tanatos_
Дата 30.11.2007, 13:51 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 28
Регистрация: 20.11.2007

Репутация: нет
Всего: нет



Цитата(xvr @  30.11.2007,  13:35 Найти цитируемый пост)

Порядок действий (функция OnLogAdd):
1) Блокируется очередь (критической секцией или семафором, неважно)
2) Сообщение добавляется в конец очереди
3) Разблокируется очередь
4) Вызывается Synchronize(Stage2)

Фуннкция Stage2 (в классе логера):
1) Блокируется очередь
2) Из очереди вычитываются ВСЕ содержащиеся в ней сообщения, для каждого вызывается callback от подписчиков. (после этого очередь пуста)
3) Разблокируется очередь


Функция OnLogAdd и есть callback-подписчик!

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

ИМХО в идеале надо решить эту проблему в пределах функции OnLogAdd,
вот бы вызывать Synchronize(Update()); именно из нее, но она относится к основному потоку, а там нет никаких Synchronize.

PM MAIL   Вверх
xvr
Дата 30.11.2007, 14:58 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Комодератор
Сообщений: 7046
Регистрация: 28.8.2007
Где: Дублин, Ирландия

Репутация: 48
Всего: 223



Цитата(_Tanatos_ @ 30.11.2007,  13:51)
Цитата(xvr @  30.11.2007,  13:35 Найти цитируемый пост)

Порядок действий (функция OnLogAdd):
1) Блокируется очередь (критической секцией или семафором, неважно)
2) Сообщение добавляется в конец очереди
3) Разблокируется очередь
4) Вызывается Synchronize(Stage2)

Фуннкция Stage2 (в классе логера):
1) Блокируется очередь
2) Из очереди вычитываются ВСЕ содержащиеся в ней сообщения, для каждого вызывается callback от подписчиков. (после этого очередь пуста)
3) Разблокируется очередь


Функция OnLogAdd и есть callback-подписчик!


Пардон, я имел в виду функцию по добавлению записи в лог.

Цитата

Если я правильно понял, то написанное Вами есть точно то что я и имею сейчас с одним лишь отличием добавлена очередь сообщений а не одно сообщение. 

Это и есть одно из 2х главных ее отличий.

Цитата

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

Это второе отличие - она должна САМА вызывать эту функцию обновления (п. 4)

Цитата

ИМХО в идеале надо решить эту проблему в пределах функции OnLogAdd,
вот бы вызывать Synchronize(Update()); именно из нее, но она относится к основному потоку, а там нет никаких Synchronize.

Поместите весь логер в отдельный поток и вызывайте наздоровье smile

Или скопируйте реализацию метода Synchronize из TThread к себе в логер.
PM MAIL   Вверх
_Tanatos_
Дата 30.11.2007, 15:43 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 28
Регистрация: 20.11.2007

Репутация: нет
Всего: нет



Цитата(xvr @  30.11.2007,  14:58 Найти цитируемый пост)
Или скопируйте реализацию метода Synchronize из TThread к себе в логер

Интересно, где ты видел реализацию этой функции?
А подвязывать класс журнала на VCL однозначно неправильно!!!, так как этот класс должен быть независим ни от каких библиотек кроме std

Добавлено через 3 минуты и 4 секунды
ИМХО
Класс журнала должен безопасно регистрировать новое сообщение (пусть будет очередь сообщений)
Затем от должен поочередно безопасно вызвать функци OnLogAdd
Затем уже в пределах функции OnLogAdd должно быть выполнено безопасное действие про сохранению, отображению и т.д. сообщения.

Вопрос в том, что внутри метода Synchronize, может буть это можно сделать самомму в пределах функции OnLogAdd() ?
PM MAIL   Вверх
xvr
Дата 30.11.2007, 18:31 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Комодератор
Сообщений: 7046
Регистрация: 28.8.2007
Где: Дублин, Ирландия

Репутация: 48
Всего: 223



Цитата(_Tanatos_ @ 30.11.2007,  15:43)
Цитата(xvr @  30.11.2007,  14:58 Найти цитируемый пост)
Или скопируйте реализацию метода Synchronize из TThread к себе в логер

Интересно, где ты видел реализацию этой функции?

В исходниках vcl

Цитата

А подвязывать класс журнала на VCL однозначно неправильно!!!, так как этот класс должен быть независим ни от каких библиотек кроме std

Тогда его клиенты должны быть готовы к тому, что их будут вызывать на любых потоках, к чему vcl не готов.

Цитата

Добавлено @ 15:46
ИМХО
Класс журнала должен безопасно регистрировать новое сообщение (пусть будет очередь сообщений)
Затем от должен поочередно безопасно вызвать функци OnLogAdd
Затем уже в пределах функции OnLogAdd должно быть выполнено безопасное действие про сохранению, отображению и т.д. сообщения.

Это зависит от того, сколько будет этих OnLogAdd. Если придется делать много разновидностей OnLogAdd с синхронизацией, то лучше ее поместить в сам логер, или сделать какой-нибдь wrapper для синхронизации

Цитата

Вопрос в том, что внутри метода Synchronize, может буть это можно сделать самомму в пределах функции OnLogAdd() ?

Посмотри сам в файле Source\vcl\classes.pas

Собственно можно сделать и проще (так было сделано в BCB 3) - Synchronize посылала спец. сообщение в handle Application, по которому та в оконной процедуре вызывала запрошенную процедуру (теперь спец. сообщения нет, а производится переодический контроль на сообщении WM_NULL)

(от BCB 6.0)

Код

procedure TThread.Synchronize(Method: TThreadMethod);
var
  SyncProc: TSyncProc;
begin
  if GetCurrentThreadID = MainThreadID then
    Method
  else
  begin
{$IFDEF MSWINDOWS}
    SyncProc.Signal := CreateEvent(nil, True, False, nil);
    try
{$ENDIF}
{$IFDEF LINUX}
      FillChar(SyncProc, SizeOf(SyncProc), 0);  // This also initializes the cond_var
{$ENDIF}
      EnterCriticalSection(ThreadLock);
      try
        FSynchronizeException := nil;
        FMethod := Method;
        SyncProc.Thread := Self;
        SyncList.Add(@SyncProc);
        ProcPosted := True;
        if Assigned(WakeMainThread) then
          WakeMainThread(Self);
{$IFDEF MSWINDOWS}
        LeaveCriticalSection(ThreadLock);
        try
          WaitForSingleObject(SyncProc.Signal, INFINITE);
        finally
          EnterCriticalSection(ThreadLock);
        end;
{$ENDIF}
{$IFDEF LINUX}
        pthread_cond_wait(SyncProc.Signal, ThreadLock);
{$ENDIF}
      finally
        LeaveCriticalSection(ThreadLock);
      end;
{$IFDEF MSWINDOWS}
    finally
      CloseHandle(SyncProc.Signal);
    end;
{$ENDIF}
    if Assigned(FSynchronizeException) then raise FSynchronizeException;
  end;
end;

type
  TSyncProc = record
    Thread: TThread;
{$IFDEF MSWINDOWS}
    Signal: THandle;
{$ENDIF}
{$IFDEF LINUX}
    Signal: TCondVar;
{$ENDIF}
  end;
  PSyncProc = ^TSyncProc;

var
  ProcPosted: Boolean;
  SyncList: TList = nil;

function CheckSynchronize: Boolean;
var
  SyncProc: PSyncProc;
begin
  if GetCurrentThreadID <> MainThreadID then
    raise EThread.CreateResFmt(@SCheckSynchronizeError, [GetCurrentThreadID]);
  if ProcPosted then
  begin
    EnterCriticalSection(ThreadLock);
    try
      Result := (SyncList <> nil) and (SyncList.Count > 0);
      if Result then
      begin
        while SyncList.Count > 0 do
        begin
          SyncProc := SyncList[0];
          SyncList.Delete(0);
          try
            SyncProc.Thread.FMethod;
          except
            SyncProc.Thread.FSynchronizeException := AcquireExceptionObject;
          end;
{$IFDEF MSWINDOWS}
          SetEvent(SyncProc.signal);
{$ENDIF}
{$IFDEF LINUX}
          pthread_cond_signal(SyncProc.Signal);
{$ENDIF}
        end;
        ProcPosted := False;
      end;
    finally
      LeaveCriticalSection(ThreadLock);
    end;
  end else Result := False;
end;

{  Call CheckSynchronize periodically within the main thread in order for
   background threads to synchronize execution with the main thread.  This
   is mainly for applications that have an event driven UI such as Windows
   or XWindows (Qt/CLX).  The best place this can be called is during Idle
   processing.  This guarantees that the main thread is in a known "good"
   state so that method calls can be safely made.  Returns True if a method
   was synchronized.  Returns False if there was nothing done.
}
function CheckSynchronize: Boolean;

{ Assign a method to WakeMainThread in order to properly force an event into
  the GUI thread's queue.  This will make sure that non-GUI threads can quickly
  synchronize with the GUI thread even if no events are being processed due to
  an idle state }
var
  WakeMainThread: TNotifyEvent = nil;


procedure TApplication.WndProc(var Message: TMessage);
...
begin
...
    with Message do
      case Msg of
...
        WM_NULL:
          CheckSynchronize;
...
      end;
...
end;



PM MAIL   Вверх
  
Ответ в темуСоздание новой темы Создание опроса
Правила форума "С++ Builder"
Rrader

Запрещается!

1. Публиковать ссылки на вскрытые компоненты

2. Обсуждать взлом компонентов и делиться вскрытыми компонентами

  • Литературу по С++ Builder обсуждаем здесь
  • Действия модераторов можно обсудить здесь
  • С просьбами о написании курсовой, реферата и т.п. обращаться сюда
  • Настоятельно рекомендуем заглянуть в DRKB (Delphi Russian Knowledge Base) - крупнейший в рунете сборник материалов по Дельфи


Если Вам понравилась атмосфера форума, заходите к нам чаще! С уважением, Rrader.

 
1 Пользователей читают эту тему (1 Гостей и 0 Скрытых Пользователей)
0 Пользователей:
« Предыдущая тема | C++ Builder | Следующая тема »


 




[ Время генерации скрипта: 0.0845 ]   [ Использовано запросов: 21 ]   [ GZIP включён ]


Реклама на сайте     Информационное спонсорство

 
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности     Powered by Invision Power Board(R) 1.3 © 2003  IPS, Inc.