Newsgroup: it.comp.lang.delphi
Titolo: Problemi con Delphi7 e ADO in Multithreading

Ciao a tutti,
    ho realizzato un software in Delphi7 per Win32 con accesso ai dati
tramite oggetti ADO (TADOConnection, TADOQuery, ecc...) su base dati
Microsoft Access (provider Microsoft Jet 4.1). Da un po' di tempo ho
incontrato un gravissimo problema, infatti quando effettuo connessioni
multiple al database su pių thread simultaneamente, viene mostrato l'errore
di runtime: "List Index Out Of Bounds (99)", dove il numero indicato tra
parentesi incrementa ad ogni messaggio di errore e solo di recente mi sono
reso conto che tale numero rappresenta il numero di dataset della
connessione ottenibile tramite TADOConnection.DataSetCount. Questi errori
non pregidicano di molto il funzionamento del programma (se non si fa caso
all'aumento esponenziale delle risorse in memoria), tuttavia dopo circa una
ventina di apparizioni di questi messaggi di errore, viene mostrato un ben
pių grave: "Access Violation at Address (...)" che impedisce di continuare a
lavorare con l'applicazione fin quando non si decide di terminarla
drasticamente uccidendo il processo relativo in Task Manager.
    Qualcuno č in grado di aiutarmi o darmi almeno una dritta su come
procedere? (ho giā provato a portare l'applicazione in Delphi2006 ed
utilizzare invece che un'unico oggetto TADOConnection condiviso, un oggetto
Connection creato ad hoc in ogni thread senza alcun risultato).
    Grazie,
    Antonio Scarpelli 

 all'aumento esponenziale delle risorse in memoria),
Magari non ci fai caso tu, ma i clienti si... :-)))
A.

 puntano al db e vedi se capita lo stesso casino.
Il test l'ho fatto, ed effettivamente il casino succede ugualmente. Posto 
qui la porzione di codice del test per poter verificare se ho commesso 
qualche errore di cui non mi sono reso conto. Ho creato appunto un form a 
cui sono associati una TADOConnection ed un TButton che genera 100 thread 
che accedono simultaneamente al database (quindi per la lettura del codice 
conviene partire dall'evento Button1Click).
unit Unit1;
interface
uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ExtCtrls, DB, ADODB, StdCtrls;
type
  TForm1 = class(TForm)
    Button1: TButton;
    ADOConnection1: TADOConnection;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;
var
  Form1: TForm1;
implementation
uses
  ActiveX;
{$R *.dfm}
function f(p: Pointer): Integer;
var
  Q: TADOQuery;
begin
  Q:= TADOQuery.Create(nil);
  Q.Connection:=form1.ADOConnection1;    // In alternativa si può anche
                                                                     // 
creare qui direttamente un
                                                                     // 
nuovo oggetto TADOConnection
                                                                     // o 
utilizzare la proprietà
                                                                     // 
ConnectionString di
                                                                     // 
TADOQuery
  Q.SQL.Add('select * from TABELLA');
  Q.Open;
  while not Q.Eof do begin
    sleep(10);     // simulo un'attesa che potrebbe essere
                        // utilizzata per compiere alcune
                        // operazioni sul record corrente
    Q.Next;       // e quindi passo al record successivo
  end;
  Q.Free;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
  i: Integer;
  tid: Cardinal;
begin
  for i:=0 to 100 do begin
    BeginThread(nil,0,f,nil,0,tid);
  end;
end;
initialization
  CoInitialize(nil);
finalization
  CoUninitialize;
end.
Grazie per l'aiuto,
    Antonio 

 Ho provato sia con TADOConnection condivisa (soluzione che preferirei)
Nelle applicazioni multithread non è mai possibile utilizzare lo stesso
componente di connessione tra diversi thread, se non vi è qualche
meccanismo di sincronizzazione tra essi, che renda l'accesso alla
connessione serializzato.  Avere però un'unica connessione dall'accesso
serializzato può compromettere gravemente le performance, perché ogni
thread dovrà aspettare che la risorsa si liberi.  Non volendo scalare nel
tempo, quindi, è bene scalare nello spazio.  Il mio consiglio è di
implementare il connection-pooling a mano, con una classe che istanzi N
connessioni attive.
Ogni volta che un thread richiede una connessione, la classe di
connection-pooling scorre il pool, trova la prima libera, la blocca
logicamente e gliela fornisce.  Quando il thread rilascia la connessione,
la classe elimina il blocco logico da quella connessione, lasciandola però
attiva.  Ovviamente ogni richiesta/rilascio di connessione con
blocco/sblocco va serializzata (per evitare che banalmente più thread
ricevano la stessa connessione), ma l'esecuzione delle query procederà in
parallelo su strade distinte.  In queste circostanze, più thread non si
pestano i piedi tra loro.  Nel caso peggiore un thread richieda una
connessione quando tutte sono "bloccate", lo si potrà mettere in attesa o
far scalare il pool (N = N+1).
Spero d'essere stato chiaro...
-- 
questo articolo e` stato inviato via web dal servizio gratuito 
http://www.newsland.it/news
 segnala gli abusi ad 
ab...@newsland.it

 puntano al db e vedi se capita lo stesso casino.
Non capita, perché la colpa non è di Access, ma di come viene usato.


 piů grave: "Access Violation at Address (...)"
Antonio, č evidente che il malfunzionamento comincia nel momento in cui
appaiono quegli errori, quindi non puoi dire che "non pregiudicano di
molto il funzionamento del programma".  L'aumento esponenziale delle
risorse di memoria e il blocco conseguente alla ventina di quei messaggi
d'errore significa ECCOME che quel malfunzionamento incide.  Imparate a
fare un'analisi attenta dei problemi...


 Il test l'ho fatto, ed effettivamente il casino succede ugualmente.
Io intendevo un test senza il tuo thread: un semplice form e via.
A parte cio, il tuo codice, imho, non e' molto pulito.
Un componente di accesso ai dati su un form (dovrebbe stare su un
datamodule) usato da n thread...
io butterei via il from e creerei una classe che incapsula la
tadoconnection e che espone un metodo x creare le istanze del tuo
thread, che dovrebbe prevedere un costruttore dedicato che accetti come
parametro la tadoconnection.
A.

 connessione serializzato.
Andrea, parlando specificatamente di delphi su win32, questa cosa vale
x tutte le tecnologie standard tipo ado e dbx?
Cioe', tadoconnection e tsqlconnection non sono threadsafe?
A.

 Cioe', tadoconnection e tsqlconnection non sono threadsafe?
Sě ed č una cosa che vale anche fuori dall'ambito delphi/win32.


 Sì ed è una cosa che vale anche fuori dall'ambito delphi/win32.
Correggimi se sbaglio, ma dell'utilizzo di connessioni simultanee (e 
l'implementazione di metodi threadsafe) non dovrebbe occuparsene l'oggetto 
che effettua le interrogazioni al database?
Ho realizzato metodi ed applicazioni simili in java (anche se non con 
Database Access) e non ho avuto alcun problema di questo tipo (è anche vero 
che però l'oggetto utilizzato gestiva un pool di connessioni simultanee al 
db, cosa che credevo facesse anche TADOConnection). 

 parametro la tadoconnection.
Premetto che l'esempio è puramente indicativo (giusto per far capire dove e 
come viene generato l'errore), ma ho necessità di avere la TADOConnection 
sul form perché in questo modo può essere richiamata dai TADOQuery che a 
loro volta sono richiamati dagli oggetti che effettuano il binding dei dati 
(ad esempio una ListView, ecc...). In alternativa cosa avrei potuto 
utilizzare di più efficiente ed elegante? 

 fare un'analisi attenta dei problemi...
Il mio era sono un "eufemismo"... è ovvio che il cliente che si trova 
davanti un programma che in partenza occupava una decina di MB ed ora ne 
occupa qualche centinaio (impedendogli di avviare efficientemente altre 
applicazioni) e con i messaggi di errore (per lui incomprensibili) che gli 
appaiono, si trova un attimino spazientito (altro eufemismo)! 

 che effettua le interrogazioni al database?
Non ricordo se gli oggetti ADO sono intrinsecamente "thread safe", ma di 
certo non lo sono i componenti dbGo for ADO con cui accedi a tali 
oggetti dalla tua applicazione.
Anche se ADO supportasse il multithreading, dovresti comunque proteggere 
in qualche modo l'accesso asincrono da più thread ai componenti ADO, 
usando una delle modalità che ti sono state già illustrate.
Ciao,
   Marco.
-- 
MARCO BREVEGLIERI
  (
http://www.marco.breveglieri.name
)

 che effettua le interrogazioni al database?
Dipende che intendi per "oggetto che effettua le interrogazioni al
database".
In generale non esistono meccanismi impliciti thread-safe nelle librerie
di accesso ai DB. In Java, invece, effettivamente mi sembra che qualcosa
del genere sia già implementato a livello di libreria.


 utilizzare di più efficiente ed elegante?
E' bene seguire sempre il modello MVC.  Qualora tu non applicassi il
metodo che io ti ho suggerito, per l'implementazione del
connection-pooling (con N istanze delle "connection" a run-time, quindi),
il tuo componente TADOConnection dovrebbe risiedere su un datamodule e
così pure i componenti TADOQuery.  Sarebbe bene distinguere i datamodule
tra loro per area funzionale.


 Il mio era sono un "eufemismo"...
Sarà, ma ho conosciuto troppa gente che considera qualcosa "funzionante",
perché tutto sommato si blocca solo 3 volte su 10...


 usando una delle modalità che ti sono state già illustrate.
Già, il problema è solo il tempo (che non ho) a disposizione per effettuare 
queste modifiche.


 queste modifiche.
Il problema vero è che non hai alternative ;)


 tra loro per area funzionale.
    Senz'alcun dubbio la creazione del pool di connessioni risolverebbe 
tutti i miei problemi, ma volendo cercare di utilizzare una soluzione per 
così dire "ibrida" per cercare di non dover mettere mano a tutto il codice 
(e cercare di risparmiare un po' di tempo almeno in prima istanza), avevo 
pensato di modificare la funzione GetConnection già esistente, facendole 
restituire, anziché una connessione condivisa, una connessione dedicata, 
prendendo la prima connessione inutilizzata dal pool di connessioni. Il 
problema sta nel capire quale connessione non è utilizzata senza dover 
dichiararlo esplicitamente ogni qualvolta si prenda una connessione dal 
pool.
    Esiste una funzione che indichi se la connection non è più referenziata 
da alcun oggetto? 

 prendendo la prima connessione inutilizzata dal pool di connessioni.
Troppo complicata come soluzione tampone.  Dal mio punto di vista, la cosa
più semplice da fare è istanziare una critical section (v.
TCriticalSection) visibile ad ogni thread e far sì che prima di qualunque
operazione verso il DB, si acquisisca la critical section.
In buona sostanza, se ora hai del codice: 
ADOQuery.SQL.Text := 'insert into....';
ADOQuery.ExecSQL;
il codice diventerebbe:
CS.Acquire;
try
  ADOQuery.SQL.Text := 'insert into....';
  ADOQuery.ExecSQL;
finally
  CS.Release;
end;
Ovviamente CS sarà un'istanza di TCriticalSection definita in una parte di
codice a monte, rispetto ad ogni thread.
In questo modo rendi thread-safe ciò che prima non lo era.


Ok, mi metterò subito al lavoro!
    Grazie a tutti per i consigli,
    Antonio