![]() |
| ||
|
|
Новости |
Поиск |
FAQ |
Статьи |
Книги |
Новости VCL
| | Форумы ЧАТ |
Орешник |
|
| |||
| |||
« предыдущая ветвь | форум | следующая ветвь »
TThread + Synchronize = bug [D7, WinXP]
abb777 © (2010-12-19 21:25:00)Hi, All,
Не могу понять, в чем дело. Вроде все, как надо, сделал.
Есть несколько одинаковых нитей, взаимодействующих через события (CreateEvent() / WaitForSingleObject() ) друг с другом. В методе Execute вызывается Synchronize() для отображения хода выполнения каждой нити.
Все работает прекрасно, если запустить эту систему нитей и дать ей доработать до конца. В конце все нити уничтожаются, все TThread-ы уничтожаются, все на ура.
Проблема в принудительном останове. Все виснет, если в основной нити VCL просто вызвать нечто типа
for i := 0 to FLayers.Count - 1 do
begin
FLayers.Layer[i].FThread.Terminate;
end;
Данный код просто завешивает все нити. Они висят, как видно в отладочном окне "Threads" на некоем вызове WaitForSingleObject(), но, насколько я понял, это не "мой" WaitForSingleObject() из метода Execute(), а некий другой. Внутри Execute() все с виду ОК - Terminated там анализируется. Да и дело, похоже не в этом. Если я закомментарю вызов Synchronize(), то все останавливается ОК, все нити завершаются, как только я выйду из кода, приведенного выше.
Исходя из этого факта я предположил, что зависание происходит в коде самого TThread, а именно здесь:
class procedure TThread.Synchronize(ASyncRec: PSynchronizeRecord);
..............
LeaveCriticalSection(ThreadLock);
try
WaitForSingleObject(SyncProc.Signal, INFINITE); <<<<<<<<<
finally
EnterCriticalSection(ThreadLock);
end;
...................
Перепробовал кучу всяких изворотов, убрал Synchronize() из этих нитей, которых может быть и 100 (сейчас только 10), и создал спец. нить для отображения, а всю информацию для отображения "рабочие" нити валят в список, который разгребает эта "нить отображения". Тот же эффект. Все работает и завершается по окончанию метода Execute() нормально, но при принудительно/досрочном завершении все виснет. Причем виснут все нити, а не только та одна, где сейчас есть вызов Synchronize.
Все операции с общими объектами сделаны через критическую секцию, так что с этим все д.б. в порядке.
Кто-нибудь такое встречал?
Есть идеи?
Заранее спасибо.
han_malign (2010-12-22 10:43:00) [1]
> А если мы "подавим лишние нотификации", то инфа будет приходить
> отрывочными кусками по каким-то случайным (а не по всем)
> TRLayer-ам. Разве нет?
- разве нет... для PostMessage, сообщение помещается в конец очереди сообщений.
Пока до него дойдет очередь:
Layers: CritSect(Count:=1;notify),CritSect(Count:=2),...,CritSect(Count:=N)
Main( Message Dispatch Loop): CritSect((Count=N): Count:=0)
Layers: CritSect((Count=0): Count:=1,notify)...
Критическая секция обеспечивает сериализацию, главное чтобы операция чтение+изменение была атомарной(внутри одной блокировки)...
Конечно можно просто проверять, что есть новые данные(Count > 0), но в некоторых сильно-нагруженных сценариях можно запросто словить переполнение очереди сообщений...
> вариант с SendMessage() тоже работает, но вокруг него надо убрать крит. секцию.
- естественно(обычно циклограмму в столбцах рисуют по количеству потоков, но тут все равно линейная сериализация получается):
Layer Thread: EnterCriticalSectionклассический DeadLock
SendMessage/Synchronize { - Layer Thread - в ожидании завершения синхронной операции
Main Thread: EnterCriticalSection- опаньки!!!
Main Thread: LeaveCriticalSection
}
Layer Thread: LeaveCriticalSection
--------------------------------
Layer Thread: EnterCriticalSection
PostMessage()
Main Thread(Message Dispatch) EnterCriticalSection(call) - WAIT ...
Layer Thread: LeaveCriticalSection
Main Thread(Message Dispatch): ... EnterCriticalSection(return)
Main Thread(Message Dispatch): LeaveCriticalSection
- мораль: как можно меньше вложенных синхронизаций, как можно короче критические секции...
Для данного примера это можно было решить так:
EnterCriticalSection();- хотя, в данном случае, для скаляра- можно спокойно использовать оптимистическую модель синхронизации - то есть вообще без критической секции...
haveNewData:= Count > 0;//Layer Threads Owned Data
LeaveCriticalSection();
if( haveNewData )then Synchonize();
Ega23 (2010-12-22 00:21:00) [2]
> При ближайшем рассмотрении нашел неувзку. Вы показываете
> мне, что finally сработает даже при вызове Exit(), так?
> А я с этим и не спорил. Но в моем коде внутри try стоял
> LeaveCriticalSection(). Его два раза подряд можно вызывать?
> Я думал, что это неправильно. А если в finally не вставить
> проверку, то LeaveCriticalSection вызовется два раза.
>
Дело именно в этом.
Например, я захожу в крит.секцию. Получаю какой-то там параметр. На основании значения этого параметра мне надо либо продолжить работу, либо выйти.
Например, параметр - string
Вариант 1
varВариант 2
s: string;
b: Boolean;
begin
b := False;
EnterCritSection
try
s := GetSomeParam;
if (s = 'мама') or (s = 'мыла') or (s = 'раму') then
begin
b := True;
LeaveCritSection;
Exit;
end;
s := DoSomethingWithParam(s);
if (s = 'мама') or (s = 'мыла') or (s = 'раму') then
begin
b := True;
LeaveCritSection;
Exit;
end;
........ Ещё какой-то код
finally
if not b then LeaveCritSection;
end;
Тот же код
var
s: string;
begin
EnterCritSection
try
s := GetSomeParam;
if (s = 'мама') or (s = 'мыла') or (s = 'раму') then
Exit;
s := DoSomethingWithParam(s);
if (s = 'мама') or (s = 'мыла') or (s = 'раму') then
Exit;
........ Ещё какой-то код
finally
LeaveCritSection;
end;
Оба приведённых кода с точки зрения логики равнозначны. Но второй - как минимум тупо меньше писать надо. А как максимум - нет ненужных проверок (которые, теоретически, может выкинуть оптимизатор, но далеко не факт, что выкинет).
Собственно, всё.
Дмитрий Белькевич (2010-12-22 00:15:00) [3]>Внутри Execute() все с виду ОК - Terminated там анализируется
Тред никогда не выходит из Execute (читай - не заканчивается), если виснет на waitfor. Можешь флаг состояния какой-нибудь тестовый дописать или лог и проанализировать, когда треды начинаются, когда заканчивается. Если в реалтайме не удаётся отловить всё, хотя должно.
Мы у себя бывает по десятку разнородных тредов и по 50 одновременных ковыряем - то без логов повесились бы.
Дмитрий Белькевич (2010-12-22 00:03:00) [4]
> Ну может я и балбес, но я вижу где отладчик ходит. А в данном
> случае он ходит именно внутри WaitFor(
Не важно, где ходит отладчик. Важно, что waitfor не может дождаться конца метода execute треда. А пока execute не закончились, треды будут живыми. И кто знает, что они там наворотят после того, как половина программы будет убита. Вызов terminate тред не остановит - это только флаг.
Leonid Troyanovsky (2010-12-21 22:06:00) [5]
> abb777 © (21.12.10 20:05) [53]
Читать Джефа Рихтера.
До просветления.
--
Regards, LVT.
abb777 (2010-12-21 21:15:00) [6]To Ega23:
> Дурость.
>
> Вот смотри:
>
> procedure TForm1.Button1Click(Sender: TObject);
> begin
> try
> if CheckBox1.Checked then Exit;
> ShowMessage('try block');
> finally
> ShowMessage('finally block');
> end;
> end;
При ближайшем рассмотрении нашел неувзку. Вы показываете мне, что finally сработает даже при вызове Exit(), так? А я с этим и не спорил. Но в моем коде внутри try стоял LeaveCriticalSection(). Его два раза подряд можно вызывать? Я думал, что это неправильно. А если в finally не вставить проверку, то LeaveCriticalSection вызовется два раза.
abb777 (2010-12-21 20:25:00) [7]
> EnterCriticalSection(FRCS);
> snapshotDispList:= FDispList;
> FDispList:= TКакойТоТамDispList.Create;
> LeaveCriticalSection(FRCS);//позволяю работать остальным
> потокам!!!
>
Кажется, я понял Вашу мысль. Спасибо.
abb777 (2010-12-21 20:05:00) [8]To han_malign:
............
> FDispList:= TКакойТоТамDispList.Create;
...........
Я этого всего совсем не понял. Извините. У меня объект списка вообще создавался 1 раз. А Вы предлагаете его:
а) создавать в ОСНОВНОМ треде, а не там, где непосредственно появляются данные для него. Кто будет данные туда пихать и зачем, если можно просто опросить все TRLayer-ы об их состоянии?
б) создавать каждый раз при вызове процедуры отображения, а не 1 раз в самом начале.
> //подавляем накопление лишних нотификаций в лаге до захвата
> данных основным потоком
Идея была такая. Как Вы можете видеть, в каждом TRLayer.Execute фигурирует TRLayer.FCurStep, определяющая текущий шаг расчета. Эта переменная устанавливается одновременно со сбросом состояния FIsReady. Затем после опр. вычислений FIsReady устанавливается, что говорит о том, что данный объект (тред) закончил опр. этап расчетов на шаге FCurStep, и информация для его соседа готова. Сосед может начать шаг расчета. Сам же тред идет дальше и еще кое-чем занят какое-то время. И только после этого он сообщает треду отображения о том, что инфа по очередному шагу FCurStep готова и увеличивает FCurStep на 1 переходя к следующему шагу. Поэтому тред отображения должен иметь не только ссылку на TRLayer[i], откуда он берет всю инфу (которой много), но и значение FCurStep, для которого TRLayer[i] это все сосчитал, т.к. в момент работы треда отображения у рабочего треда FCurStep может быть уже другой, а состояние инфы для этого шага еще не подтверждено. Поэтому опрерируем указателем на TRLayer[i] и значением TRLayer[i].FCurStep, которые были переданы. Я не пойму, в чем изъян такого метода?
А если мы "подавим лишние нотификации", то инфа будет приходить отрывочными кусками по каким-то случайным (а не по всем) TRLayer-ам. Разве нет? Ведь список наполняется не одним TRLayer-ом, а разными и независимо друг от друга. И надо учесть, что процедура расчета в каждом "рабочем" треде будет работать на порядки (на много порядков) дольше, чем все эти создания объектов, помещения их в список и т.п. Там будет решаться система из 7 нелин. дифуров, а это довольно трудоемкий процесс, насколько я знаю...
To Ega23 :
Да, что-то я сам себя перехитрил. Решил стать святее папы римского :)
TO ALL:
Всем огромное спасибо за поддержку, только скажите все же, я правильно понимаю, что треды мои висели скорее всего на каком-то входе в крит. секцию, потому что из нее не было сделано выхода вследствие необработки какого-то исключения?
han_malign (2010-12-21 13:57:00) [9]
> EnterCriticalSection(FRCS);
> for i := 0 to FDispList.Count - 1 do
> if i < FDispList.Count then
> FOnStep(Self,
> FDispList.DisplayRec[i].FLayer,
> FDispList.DisplayRec[i].FCurStep); <<<<<<<<<<<<<<<
> FDispList.Clear;
> LeaveCriticalSection(FRCS);
- поясняю схематически(каждый раз создавать/удалять объект моветон, но углубляться в кольцевые буфера мне что то лень):
TREDisplayRec.FLayer - должно быть копией мгновенного состояния, а не ссылкой на рабочий объект текущего состояния!!!
(иначе, вообще говоря, смысла в списке никакого нет)
КакойТоТам
EnterCriticalSection(FRCS);
snapshotDispList:= FDispList;
FDispList:= TDispList.Create;
LeaveCriticalSection(FRCS);//позволяю работать остальным потокам!!!
for i := 0 to snapshotDispList.Count - 1 do
// if i < FDispList.Count then - бред
FOnStep(Self,
snapshotDispList.DisplayRec[i].FLayer,
snapshotDispList.DisplayRec[i].FCurStep);
snapshotDispList.Free;
sapienti sat
Ega23 (2010-12-21 13:16:00) [10]
> А так можно (см. отмеченные строчки)?
Дурость.
Вот смотри:
procedure TForm1.Button1Click(Sender: TObject);
begin
try
if CheckBox1.Checked then Exit;
ShowMessage('try block');
finally
ShowMessage('finally block');
end;
end;
han_malign (2010-12-21 13:15:00) [11]
> procedure TREDisplayThread.Execute;
> begin
> while not Terminated do
> begin
> EnterCriticalSection(FRCS);
> if FEngine.FDispList.Count > 0 then
> PostMessage(FEngine.FWND, WM_RE_NOTIFY, 0, 0);
> LeaveCriticalSection(FRCS);
> Sleep(0);
> end;
> end;
- выкидываешь нафик
procedure TLayerThread.Execute;
begin
.........
EnterCriticalSection(FRCS);
if not FLayer.FTerminating then begin
FLayer.FEngine.FDispList.Add(TREDisplayRec.Create(FLayer, FLayer.FCurStep));
if FEngine.FDispList.Count = 1then//подавляем накопление лишних нотификаций в лаге до захвата данных основным потоком
PostMessage(FLayer.FEngine.FWND, WM_RE_NOTIFY, 0, 0);
end;
LeaveCriticalSection(FRCS);
abb777 (2010-12-21 13:15:00) [12]Ничего не сказано про результат в случае ошибки. Только про GetLastError.
У меня оно действительно не анализируется. Пока что не думал об этом. А процитированный код связан вот с этим:
destructor TRLayer.Destroy;
begin
CancelIteration;
if FEvent <> INVALID_HANDLE_VALUE then
begin
CloseHandle(FEvent);
FEvent := INVALID_HANDLE_VALUE;
end;
han_malign (2010-12-21 12:47:00) [13]так, к слову:
if ... (FLayer.FEvent = INVALID_HANDLE_VALUE) ...
- почитай Return Values для CreateEvent
abb777 (2010-12-21 12:45:00) [14]Поправил. С SendMessage() 15 сек полет нормальный...
А так можно (см. отмеченные строчки)?
procedure TREngine.CancelIteration;
var
i : integer;
f : boolean;
begin
WaitOn;
for i := 0 to FLayers.Count - 1 do
begin
EnterCriticalSection(FRCS);
f := true;
try
if FLayers.Layer[i].FIsActive then
begin
FLayers.Layer[i].FThread.Terminate;
LeaveCriticalSection(FRCS);
f := false;
while FLayers.Layer[i].FIsActive do
begin
Sleep(20);
end;
end else
begin
LeaveCriticalSection(FRCS);
f := false;
end;
finally
if f then <<<<<<<<<<<<<<<<
LeaveCriticalSection(FRCS); <<<<<<<<<<<<<<<<
end;
end;
..........................
end;
abb777 (2010-12-21 12:26:00) [15]Спасибо, за ценное замечание. Хотите сказать, что это и есть причина первоначальной проблемы? Я, признаться, о таком не подумал. Там, с виду, негде было исключениям взяться. Сейчас везде вставлю.
После убирания Application.ProcessMessages из обработчика события у формы вариант с SendMessage() тоже работает, но вокруг него надо убрать крит. секцию. Иначе не работает.
Ega23 (2010-12-21 12:16:00) [16]
> Sleep(0);
Не надо 0, поставь ну там 20, например.
Ega23 (2010-12-21 12:15:00) [17]
EnterCriticalSection(FRCS);
for i := 0 to FDispList.Count - 1 do
if i < FDispList.Count then
FOnStep(Self,
FDispList.DisplayRec[i].FLayer,
FDispList.DisplayRec[i].FCurStep); <<<<<<<<<<<<<<<
FDispList.Clear;
LeaveCriticalSection(FRCS);
А вот и дедлок.
Запомни навсегда, выучи как Присягу:
EnterCriticalSection
try
finally
LeaveCriticalSection
end;
Иначе любое исключение после входа приведёт к тому, что выхода не будет. И все будут ждать.
abb777 (2010-12-21 12:06:00) [18]
> > Зацикливается внутри TThread.> WaitFor.
>
> Нет, не там. А в TThread.Execute.
Ну может я и балбес, но я вижу где отладчик ходит. А в данном случае он ходит именно внутри WaitFor(). А все треды висят (видно в окошке Threads) на том же самом WaitForSingleObject() который ну никак не может быть моим, я это проверял выставляя перед ним флаг. Так что не крутятся мои треды внутри Execute, а именно стоят. Иначе окошко Threads показало бы другое.
После полученного просветления относительно SendMessage/PostMessage попробовал их. SendMessage работает точно так же, как и Synchronize() хотя где при этом треды гуляют или стоят, я не смотрел. Сразу попробовал PostMessage(). Оно сработало. Правда был конфликт - в обработчике события у формы был вставлен Application.ProcessMessages, что приводило к рекурсии и, как результату EListError в этом месте:
procedure TREngine.DoDisplay;
var
i : integer;
begin
if Assigned(FOnStep) then
begin
begin
EnterCriticalSection(FRCS);
for i := 0 to FDispList.Count - 1 do
if i < FDispList.Count then
FOnStep(Self,
FDispList.DisplayRec[i].FLayer,
FDispList.DisplayRec[i].FCurStep); <<<<<<<<<<<<<<<
FDispList.Clear;
LeaveCriticalSection(FRCS);
end;
end;
end;
Остался еще вопрос. Все работает с виду ОК, но только если и в вышеприведенном коде и в коде ниже вставлены крит. секции:
procedure TREngine.WndProc(var Msg: TMessage);
begin
with Msg do
if Msg = WM_RE_NOTIFY then
begin
DoDisplay;
Result := 1;
end else
begin
Result := DefWindowProc(FWND, Msg, WParam, LParam);
end;
end;
procedure TREDisplayThread.Execute;
begin
while not Terminated do
begin
EnterCriticalSection(FRCS);
if FEngine.FDispList.Count > 0 then
PostMessage(FEngine.FWND, WM_RE_NOTIFY, 0, 0);
LeaveCriticalSection(FRCS);
Sleep(0);
end;
end;
Если убрать КС в TREDisplayThread.Execute, то отображение сильно тормозится. А в TREngine.DoDisplay() вообще убирать нельзя, иначе AV. Если насчет причины AV еще понятно, то насчет тормозов с отображением не очень. Можете объяснить причину? Просто интересно.
Дмитрий Белькевич (2010-12-21 10:45:00) [19]
> Зацикливается внутри TThread.> WaitFor.
Нет, не там. А в TThread.Execute.
Дмитрий Белькевич (2010-12-21 10:41:00) [20]>вот это FreeOnTerminate := true; - забудь как страшный сон...
лучше без этого, хотя если логика хорошо отработана - то можно.