PDA

Ver la Versión Completa : Problemas con Threads


gluglu
08-02-2011, 17:57:49
Hola a tod@s !

Necesito de nuevo acudir a vuestros sabios consejos para solucionar el siguiente asunto :

Tengo la posibilidad de crear varias instancias de un mismo Formulario. En este caso se trata de elaborar unos listados. Para ello en el Formulario Principal tengo creado
Arr_List : Array of TList_General;

siendo TList_General del tipo de mi formulario para listados.

A su vez, en cada formulario de listados necesito realizar una serie de cálculos adicionales los cuales he metido en un Thread, que se crea en cada una de las instancias de cada formulario de listado.

Me acabo de dar cuenta que (supongo que ahí está el fallo), el Thread creado en cada formulario tiene el nombre Calculate_Total, y por lo tanto cuando tengo dos instancias de formularios de listados, al terminar el thread Calculate_Total del primer formulario, también se para el Calculate_Total del segundo formulario de listados. .... claro, no están diferenciados por nombre.

Cómo soluciono este problema ? Tengo que crear igualmente un 'Array of TThread' ? Es eso factible ? Si es así, donde tengo que declarar ese Array, en el formulario principal ?

Antes de probar y meterme en estos líos, os quería preguntar y pedir consejo.

Gracias como siempre.

Delfino
08-02-2011, 18:12:37
Tienes que llamar a cada metodo o thread del form con el prefijo Self, para q actue sobre esa instancia del form, suponiendo q para cada form le creas su thread especifico..

Neftali [Germán.Estévez]
08-02-2011, 18:15:49
Me acabo de dar cuenta que (supongo que ahí está el fallo), el Thread creado en cada formulario tiene el nombre Calculate_Total, y por lo tanto cuando tengo dos instancias de formularios de listados, al terminar el thread Calculate_Total del primer formulario, también se para el Calculate_Total del segundo formulario de listados. .... claro, no están diferenciados por nombre.

Cómo soluciono este problema ? Tengo que crear igualmente un 'Array of TThread' ? Es eso factible ? Si es así, donde tengo que declarar ese Array, en el formulario principal ?

Lo lógico es que cada thread tenga como parámetro (o tenga acceso) al formulario al que pertenece. Para modificar la variable que corresponde a su formulario y no todos la del primero.

¿Cómo accedes a la variable Calculate_Total al acabar el thread? ¿Puedes poner el código?

gluglu
08-02-2011, 18:40:59
Gracias por contestar.

Entiendo que un Thread es un 'ente autónomo' por lo tanto no sé si puedo utilizar Self en algún lado. Además después se juntaría con otro problema. En el propio Thread, a su vez se llama a otros procedimientos genéricos de cálculos, y en dichos procedimientos tengo que incrementar una variable del propio form o thread (según quien llame al procedimiento). Por lo tanto no veo cómo podría funcionar usando Self.

A Neftalí : Esa es precisamente la cuestión que se me presenta. Si el Thread es tal 'ente autónomo', cómo sé qué formulario creó un thread. Sí que es cierto que le podría pasar un parámetro o variable en la cual sepa qué form de listado lo ha llamado. Pero donde me dí cuenta del problema es que si ejecuto dos formularios de listados que a su vez cada uno llaman a un Thread que está declarado de la siguiente manera :

unit ListGeneral;
...

type
TList_General = class(TForm)
....

TCalculate_Total = class(TThread)
procedure UpdateMainForm;
private
....
public
....
protected
procedure Execute; override;
end;

var
List_General: TList_General;
Calculate_Total: TCalculate_Total;

....
implementation
....


lo que tengo claro es que al definir mi Arr_List en el Mainfom como Array of TList_General, puedo acceder a cada instancia de Arr_List por su posición en el array, es decir, a cada form de listado por su posición en el Array Arr_List.

Pero cuando en Arr_List[0] (p.ej.) se termina Calculate_Total llamando a Terminate y por lo tanto Terminated = True, entonces se me para también Calculate_Total del Arr_List[1] ya que tengo claro que tienen el mismo nombre.

Cómo diferencia cada instancia de Calculate_Total que se crean en cada instancia propia de List_General ? Supongo que ahí está el problema.

Neftali [Germán.Estévez]
08-02-2011, 19:05:36
Yo creo que te estás liando.
Si cada formulario crea su propio thread (objeto) y lo lanza, no tiene porqué haber problemas con los nombres, y cada thread debería ser independiente.
Puedes lanzar 20 threads de la misma clase y eso no tiene porqué afectar al funcionamiento de cada uno de ellos.

En el código que has puesto, cada form (TList_General) tiene su variable del tipoTCalculate_Total; Si los lanzas adecuadamente no tiene porqué haber problemas con eso.

¿Cómo los lanzas?
¿Cómo sincronizas?
¿Qué haces al terminar los threads?

Como bien dices no puedes utilizar Self en los threads, pero sí puedes acceder al form (si lo pasas como parámetro); Para acceder al formulario desde el thread (si es que te hiciera falta) puedes (y debes) utilizar el método Synchronize. Y no deberías tener ningun problema.

Neftali [Germán.Estévez]
08-02-2011, 19:10:30
Si miras este ejemplo (http://neftali.clubdelphi.com/?p=146) (es muy tonto) veras que a los threads, se les pasa como parámetro un componente de tipo (TProgressbar) y aunque todos los threads son de la misma clase, cada uno modifica su componente; Tu caso es similar, aunque pasándo un TForm; Cada frmulario puede modificar "su form" sin afectar a los demás.

gluglu
08-02-2011, 19:37:55
En el proceso de cálculo de cada instancia de TList_General, cuando ya termino de mostrar los datos en un DBGrid, entonces creo y lanzo el Thread :

procedure TList_General.Initialize;
begin

....
LabelTime.Caption := 'Consultada generada a las ' + FormatDateTime('hh:nn:ss', Time);

Calculate_Total := TCalculate_Total.Create(True);
Calculate_Total.FreeOnTerminate := True;
... paso unas variables a Calculate_Total ...
Calculate_Total.Resume;

end;

En sí mismo Calculate_Total se ejecuta :

procedure TCalculate_Total.Execute;
begin

// Realizo operaciones con la BBDD y Tablas, por lo que genero un
// acceso totalmente independiente en el propio Thread

CTDatabase := TIBDatabase.Create(nil);
CTDataBase.DatabaseName := DM0.IBDatabase1.DatabaseName;
CTDataBase.LoginPrompt := DM0.IBDatabase1.LoginPrompt;
CTDataBase.Params := DM0.IBDatabase1.Params;
CTDataBase.SQLDialect := 3;
CTDataBase.Connected := True;

CTTransaction := TIBTransaction.Create(nil);
CTTransaction.DefaultDataBase := CTDatabase;
CTTransaction.StartTransaction;

CTDataSet1 := TIBDataSet.Create(nil);
CTDataSet1.Transaction := CTTransaction;

....

// Realizo varios Cálculos y sincronizo de esta forma

Update_Var := 1;
Synchronize(UpdateMainForm);

CTDataSet4.First;
while not CTDataSet4.Eof do begin

if Terminated then Break;

with CTDataSet2 do begin
SelectSQL.Clear;
....
Prepare;
Open;
end;

....

if Terminated then Break;

Update_Var := 2;
Synchronize(UpdateMainForm);

CTTransaction.CommitRetaining;
CTDataSet4.Next;

end;

if not Terminated then begin
Update_Var := 3;
Synchronize(UpdateMainForm);
end;

CTTransaction.Active := False;
CTDataBase.Close;
CTDataBase.Free;
CTTransaction.Free;
CTDataSet1.Free;

end;

procedure TCalculate_Total.UpdateMainForm;
begin
MainForm.Arr_List[Pos_Array].UpdateForm(Update_Var);
end;

procedure TList_General.UpdateForm(Update_Var: Integer);
begin

if Update_Var = 1 then begin
...
end;

if Update_Var = 2 then begin
...
end;

if Update_Var = 3 then begin
...
end;

Application.ProcessMessages;

end;

Si cierro anticipadamente (antes de que termine el Thread de realizar sus cálculos) el Form List_General correspondiente :

procedure TList_General.FormClose(Sender: TObject; var Action: TCloseAction);
begin

if PanelTimer.Visible then begin
while not Calculate_Total.Terminated do begin
Calculate_Total.Terminate;
end;
PanelTimer.Visible := False;
Application.ProcessMessages;
end;

...

Action := caFree;
MainForm.Arr_List[Pos_Array] := nil;

end;

No Termino el Thread Calculate_Total en ningún otro sitio de la aplicación.

Programando otros asuntos me dí cuenta de una cosa que me puse a pensar, y ello me llevó a comprobar esta parte del código correspondiente a mi formulario de listados. Ahí pude comprobar lo que indicaba anteriormente, si creo dos instancias de List_General, diferenciadas por su posición en MainForm.Arr_List[i], entonces al terminar el primero, también se para el segundo.

Según lo que indicas, Neftalí, entiendo como tu que no debería ser así.

A ver si el problema podría ser otro : En el Thread a su vez llamo a otro procedimiento que tienen que pasar un valor a una variable declarada como pública del Thread :

TCalculate_Total = class(TThread)
procedure UpdateMainForm;
private
....
public
Aux_Random : Int64;
protected
procedure Execute; override;
end;

Es el único problema que me puedo imaginar, si acaso. En el otro procedimiento al que se llama desde cada uno de los Threads que puedan estar activos concurrentemente, lo que hago es

Calculate_Total.Aux_Random := Aux_R1;

y ahí si me encontraría con el problema de no saber a qué instancia de Calculate_Total estaría pasnado el valor correspondiente.

Cómo se solucinaría esto ? Cómo haría referencia a la instancia del Thread Calculate_Total de un Form List_General en concreto cuya posición es una determinada en el Arr_List del MainForm ?

Se podría hacer esto ?
MainForm.Arr_List[i].Calculate_Total.Aux_Random := Aux_R1;

La verdad es que no lo he probado.

Pues lo dicho, es la única ocasión en la cual se podrían 'liar' las instancias concurrentes de Calculate_Total. De ningun otra forma.

Es por eso que pueda ser que al terminar una instancia termine también la otra ? De hecho es lo que ocurre.

Gracias de nuevo por vuestra ayuda. ;)

yapt
08-02-2011, 20:50:46
unit ListGeneral;

...

type
TList_General = class(TForm)
....

TCalculate_Total = class(TThread)
procedure UpdateMainForm;
private
....
public
....
protected
procedure Execute; override;
end;

var
List_General: TList_General;
Calculate_Total: TCalculate_Total;

....
implementation
....






Hola, no quisiera estorbar. Y seguro que no es de mucha ayuda, pero precisamente esto que dice Neftalí:

En el código que has puesto, cada form (TList_General) tiene su variable del tipoTCalculate_Total; Si los lanzas adecuadamente no tiene porqué haber problemas con eso.

Es precisamente lo que yo no veo.


Calculate_Total: TCalculate_Total;

No tendría que estar definido como variable privada para cada instancia del formulario ?

Bueno, espero no añadir ruido en la conversación. Si es así, lo siento.

Saludos.

Neftali [Germán.Estévez]
09-02-2011, 10:04:41
Calculate_Total: TCalculate_Total;

No tendría que estar definido como variable privada para cada instancia del formulario ?

Bueno, aunque no aparece asumo que es así; Cada formulario debe tener definida su propia variable para "su" thread. Y debe ser privada, como tú dices.

Neftali [Germán.Estévez]
09-02-2011, 10:08:04
Es el único problema que me puedo imaginar, si acaso. En el otro procedimiento al que se llama desde cada uno de los Threads que puedan estar activos concurrentemente, lo que hago es


Calculate_Total.Aux_Random := Aux_R1;





No me ha quedado claro dónde está colocada esta asignación.
¿Dónde se encuentra esta línea?
¿Dentro del thread?

AÑADO:
(1) No acabo de ver de dónde sale o dónde está definida la variable Pos_Array que utilizas para acceder al array.
(2) Sigo pensando que en lugar de llegar al formulario que está asociado al thread utilizando el array, sería más sencillo que pasaras el propio formulario como parámetro al thread.

Neftali [Germán.Estévez]
09-02-2011, 10:58:48
Aquí tienes un ejemplo con estructura similar al tuyo; Faltan algunas cosas como la de la variable que te he comentado antes, pero hasta aquí, con lo que hemos comentado no da problemas.

UPDATE: Corregidos los adjuntos

gluglu
09-02-2011, 17:46:38
Hola Neftalí !

Antes de nada, decirte que el último ZIP añadido no está accesible en estos momentos. El contenido es ilegible y da error. Así que no he podido revisar este último código que adjuntas.

He estado revisando todo mi código y todos vuestros comentarios.

... y mira que hace algunos años ya que estuve revisando el ejemplo que pones en un mensaje más arriba.

Ya me queda claro que no declaré el Thread Calculate_Total en la unit List_General como private. Entiendo que en cualquier caso, también se podría declarar como público, no es verdad ?

También llevo muchos años siguiendo el foro y he denotado en todas mis consultas, que cada uno pasa parámetros de manera diferente a procedimientos. En el primer ejemplo tu utilizas el propio método Create para pasar los parámetros a cada Thread. Personalmente nunca lo he utilizado así y no sé si es mejor o peor (aunque a mi me funciona hasta el día de hoy perfectamente) el pasar los parámetros mediante variables públicas en el procedimiento llamado.

Por lo tanto entiendo también que puedo pasar tanto la referencia de la instancia del Form List_General en concreto que ejecuta el Thread al propio Thread, o puedo pasar una variable Pos_Array que es la cual me indica la posición en el Array Arr_List declarado en el MainForm, y después hacer referencia al listado en concreto como MainForm.Arr_List[Pos_Array] y a continuación el elemento que quiera.

En respuesta a tu pregunta de donde sale precisamente esa variable Pos_Array, cuando creo en el MainForm una nueva instancia de List_General lo que hago es añadir un elemento al Array Arr_List y su posición en el Array la asigno a la variable Pos_Array que a su vez se la paso a List_General para saber en cada form qué referencia es en concreto del MainForm.Arr_List

Además indico aquí que cuando hago un Free de una instancia de List_General, en MainForm.Arr_List busco la referencia en concreto y le asigno el valor Nil. Por lo tanto, respecto al párrafo anterior, al crear un nuevo List_General, no siempre lo añado al final, sino busco la primera posición en MainForm.Arr_List que esté a nil, y si lo encontrase, asigno esa posición del MainForm.Arr_List a la instancia que acabo de crear de List_General. Si no hay ningun nil en el Array, entonces sí que lo añado al final.

procedure TMainForm.Create_New_List;
var
i : integer;
Aux_L : Integer;
begin

Screen.Cursor := crHourGlass;

Aux_L := Length(Arr_List);
if Aux_L = 0 then
I := 0
else begin
for I := 0 to Aux_L - 1 do begin
if Arr_List[I] = nil then break;
end;
end;

if I >= Aux_L then begin
SetLength(Arr_List,I+1);
I := length(Arr_List)-1;
end;

Arr_List[I] := TList_General.Create(Self);
Arr_List[I].FormStyle := fsMDIChild;
....
Arr_List[I].Pos_Array := I;
....
Arr_List[I].Show;

Screen.Cursor := crDefault;

end;

Cada instancia de List_General tiene por lo tanto una variable pública Pos_Array.

Pienso que si le paso esta variable Pos_Array al Thread Calculate_Total, también podré hacer referencia dentro del Thread a la instancia concreta de List_General que quiera. Como tu bien dices, también podría pasar el Form en sí mismo como referencia.

Todavía no me ha quedado claro la otra cuestión, que a su vez te intento aclarar a ti.

El Thread Calculate_Total a su vez llama a otro procedimiento almacenado en una unit .pas .

procedure TCalculate_Total.Execute;
begin

CTDatabase := TIBDatabase.Create(nil);
CTDataBase.DatabaseName := DM0.IBDatabase1.DatabaseName;
CTDataBase.LoginPrompt := DM0.IBDatabase1.LoginPrompt;
CTDataBase.Params := DM0.IBDatabase1.Params;
CTDataBase.SQLDialect := 3;
CTDataBase.Connected := True;

CTTransaction := TIBTransaction.Create(nil);
CTTransaction.DefaultDataBase := CTDatabase;
CTTransaction.StartTransaction;

CTDataSet1 := TIBDataSet.Create(nil);
CTDataSet1.Transaction := CTTransaction;

CTDataSet2 := TIBDataSet.Create(nil);
CTDataSet2.Transaction := CTTransaction;

CTDataSet3 := TIBDataSet.Create(nil);
CTDataSet3.Transaction := CTTransaction;

CTDataSet4 := TIBDataSet.Create(nil);
CTDataSet4.Transaction := CTTransaction;

CTDataSetCheck := TIBDataSet.Create(nil);
CTDataSetCheck.Transaction := CTTransaction;

CTDataSetCheck2 := TIBDataSet.Create(nil);
CTDataSetCheck2.Transaction := CTTransaction;

CTDataSetCheck3 := TIBDataSet.Create(nil);
CTDataSetCheck3.Transaction := CTTransaction;

with CTDataSet1 do begin
SelectSQL.Clear;
SelectSQL.Add('..... ');
....
ModifySQL.Clear;
ModifySQL.Add('..... ');
....
InsertSQL.Clear;
InsertSQL.Add('..... ');
end;

...

if Type_Listing = 1 then Calculate_Detail(6, CTDataSet1, CTDataSetCheck,
CTDataSetCheck2, CTDataSetCheck3, CTDataSet2, Random_No4, Today, Today);
...

CTTransaction.Active := False;
CTDataBase.Close;
CTDataBase.Free;
CTTransaction.Free;
CTDataSet1.Free;
CTDataSet2.Free;
CTDataSet3.Free;
CTDataSetCheck.Free;
CTDataSetCheck2.Free;
CTDataSetCheck3.Free;

end;

El procedimiento Calculate_Detail, como decía, está en otra Unit diferente y puede ser llamado por otros muchos forms de mi aplicación, al igual que se llama a este procedimiento desde el propio Thread. Pienso que esto es posible, no ? Es correcto hacerlo así, no ?

Mi duda es cómo actualizo una variable del Thread (Calculate_Total) desde el procedimiento Calculate_Detail ?

Hasta ahora lo hacía tal y como indiqué en un hilo anterior
procedure Calculate_Detail(Modus: Integer; pDataSet1: TIBDataSet;
pDataSetBDM: TIBDataSet; pDataSetBDM2: TIBDataSet; pDataSetBDM3: TIBDataSet;
pDataSetBook: TIBDataSet; Random_No: Int64; StayOverFrom: TDate; StayOverTo: TDate);
begin
...
Calculate_Total.Aux_Random := Aux_R1;
...
end;

Pero está claro que está mal hecho lo que hacía hasta ahora, porque en verdad no esta referiéndome a ninguna instancia en concreto del Thread Calculate_Total creado en cada List_General. No había declarado la variable Calculate_Total correctamente dentro de List_General.

Cómo puedo actualizar entonces la variable Aux_Random declarada como pública dentro del Thread Calculate_Total ? Cómo me puedo referir a cada instancia en concreto de Calculate_Total, que a su vez ha sido creada por cada instancia de List_General ?

Pensaba que podía ser igual que ponía en mi último post :
MainForm.Arr_List[i].Calculate_Total.Aux_Random := Aux_R1;
pero lo he probado y no me funciona.

Si pasase el Form (List_General) como referencia al Thread (Calculate_Total), y este Thread (Calculate_Total) a su vez se lo pasase al procedimiento Calculate_Detail, como sería posible la actualización desde Calculate_Detail de la variable Aux_Random declarada en el Thread Calculate_Total ?

.... que lío no ??? :rolleyes:

Gracias de nuevo !

Chris
09-02-2011, 18:32:57
Creo que el problema es exactamente lo mismo que piensas. Estás compartiendo una variable global (Calculate_Total) entre las distintas instancias del form, por lo que al final todos trabajan con el mismo Thread. Creo que necesitarás declarar e implementar un nuevo constructor para TCalculate_Total que te permita pasarle un nuevo parámetro que indique cuál es el formulario padre.

Sin embargo, debes de saber que la biblioteca cliente de Firebird no es Thread Safe (hasta la versión 2.5) (http://tracker.firebirdsql.org/browse/CORE-707?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel), por lo que la independencia que crees estar creando es subliminal y que además talvez podría dar origen a problemas que sean muy difíciles de rastrear.

Saludos,
Chris

Neftali [Germán.Estévez]
10-02-2011, 10:26:36
Antes de nada, decirte que el último ZIP añadido no está accesible en estos momentos. El contenido es ilegible y da error. Así que no he podido revisar este último código que adjuntas.


Lo he vuelto a subir. No se porqué con segun qué programas utilizo para comprimir, luego a algunas personas les da problemas al descomprimir. Hay un .RAR.ZIP que hay que quitarle la extension ZIP y dejarlo en RAR.


(1) Ya me queda claro que no declaré el Thread Calculate_Total en la unit List_General como private. Entiendo que en cualquier caso, también se podría declarar como público, no es verdad ?

(2) En el primer ejemplo tu utilizas el propio método Create para pasar los parámetros a cada Thread. Personalmente nunca lo he utilizado así y no sé si es mejor o peor.


(1) No es que no pueda ser público, simplemente es que si lo declaras como privado de alguna forma te aseguras de que nadie accede a él por error, porque desde fuera no es visible. Si está declarado como público significa que cualquiera puede acceder a él. Pero lo que es funcionar, debe funcionar igual.

(2) En este caso es cuestion de gustos. Yo utilizo en algunos casos el create para pasar pasar parámetros y en otros, primero hago el create y luego asigno valor a las propiedades. No hay diferencia es cuestión de gustos.


Por lo tanto entiendo también que puedo pasar tanto la referencia de la instancia del Form List_General en concreto que ejecuta el Thread al propio Thread, o puedo pasar una variable Pos_Array que es la cual me indica la posición en el Array Arr_List declarado en el MainForm, y después hacer referencia al listado en concreto como MainForm.Arr_List[Pos_Array] y a continuación el elemento que quiera.


Lo que no tengo claro es que no te de problemas al acceder desde el Thread a cosas que hay fuera, como los formularios. Aunque los pasemos como parámetro, no quiere decir que no fallen al acceder. De ahí que acceder al MainForm, para luego al array, y desde ahí al formulario tenga bastantes número de dar problemas.


El procedimiento Calculate_Detail, como decía, está en otra Unit diferente y puede ser llamado por otros muchos forms de mi aplicación, al igual que se llama a este procedimiento desde el propio Thread. Pienso que esto es posible, no ? Es correcto hacerlo así, no ?


También creo que te puede dar problemas. Para utilizar algo externo al thread utilizas el Synchronize, para evitar conflictos entre diferentes threads. Cuando accedes a este procedimiento y a todo lo que hay dentro, deberías asegurarte de que lo haces de forma exclusiva; Si no es así puedes obtener errores.


Mi duda es cómo actualizo una variable del Thread (Calculate_Total) desde el procedimiento Calculate_Detail ?


El problema creo que no es cómo actualizar la variable (puede ser como en un procedimiento normal), sino que ese procedimiento se ejecute de forma "thread-safe", que creo que es lo que no estás asegurando ahora.

¿Porqué no colocas ese procedimiento Calculate_Detail como un procedimiento interno (private) del thread?
¿Es posible? Si no utiliza nada externo no debes tener problemas, y si utiliza algo externo para no poder ponerlo como privado, ese es el problema.

Un saludo.