Realizzazione di un sistema multiprocesso in VB6
a cura di Rodolfo Calzetti e Maurizio Brasca (requisiti: Conoscenza Programmazione Orientata agli Oggetti)Una semplice esperienza mostra che la chiamata ad un metodo di un oggetto Activex blocca tutti i processi del programma chiamante ivi inclusi gli eventi di un timer.
Ciò potrebbe non essere ammissibile nei casi in cui il processo chiamato impieghi del tempo per essere portato a termine.
E non rappresenta una soluzione quella di rinunciare agli Activex includendo tutto il codice sorgente in un eseguibile standard, poiché certe funzionalità potrebbero essere contenute in componenti Activex forniti da altri.Ecco un esempio di programma Visual Basic mono processo, per il quale abbiamo utilizzato ADO come componente Activex nel quale la chiamata di un metodo
blocca gli eventi di un timer (si suppone che la query in esso effettuata abbia una durata sensibile):Option Explicit Dim I As Double Private Sub Command1_Click() Dim DbConn As ADODB.Connection Dim Rec As ADODB.Recordset Command1.Enabled = False Set DbConn = New ADODB.Connection Set Rec = New ADODB.Recordset DbConn.Open "Provider=SQLOLEDB;Data Source=SVILUPPO;Initial" + _ "Catalog=MioDB;User ID=sa;Password=;" Rec.Open "Select * from MiaTabella where Campo='Pippo'", DbConn Rec.Close Set Rec = Nothing Set DbConn = Nothing Command1.Enabled = True End Sub Private Sub Timer1_Timer() I = I + 1 Label1 = I End SubPer ovviare a questi inconvenienti ci siamo avvalsi di uno schema che, più in generale, permette di tenere sotto controllo più programmi come se fossero un'entità sola mediante un eseguibile Activex che agisca da collante.
L'uso di più eseguibili è dettato dalla necessità di avere altri processi VB6 disponibili, qualora il primo sia bloccato dall'esecuzione di una chiamata a un metodo di un oggetto Activex. L'eseguibile Activex "collante" è il metodo più conveniente per far comunicare tra loro eseguibili separati.Scendendo maggiormente nel dettaglio, supponiamo che E sia il nostro programma mono processo iniziale che implementi delle funzioni Funz1, Funz2, ...
Per poter trasformare E in un programma multithreading conviene fare prima una operazione "preparatoria" per rendere lo schema che stiamo proponendo più universale e versatile.
Si implementa una funzione ChiamataProcesso che abbia come parametri due documenti in formato XML, chiamiamoli XmlDocIn (documento input) e XmlDocOut (documento output).XmlDocIn potrebbe avere una struttura del tipo:
<xml><parametri Funzione="Funz3" Arg1="5" Arg2="Pippo" /></xml>Nel documento XmlDocIn si definisce un parametro Funzione assegnandogli il nome convenzionale (alias) indicante una delle funzioni FunzK.
Il documento XmlDocIn specifica anche i parametri da passare alla funzione.
Nel corpo della funzione ChiamataProcesso una Select Case fatta sulla variabile Funzione effettuerà una chiamata alla FunzK opportuna.
Public Sub ChiamataProcesso(XmlDocIn as DOMDocument30, XmlDocOut as DOMDocument30) Dim Funzione As String Dim Arg1 As Integer Dim Arg2 As String Funzione = XmlDocIn Select Case Funzione Case "Funz1": Funz1 Arg1, Arg2 Case "Funz2": Funz2 Arg1, Arg2 End Select End SubCon ciò abbiamo un'interfaccia standard per poter attivare le funzionalità del nostro programma.
Il secondo passo sarà quello di prendere il codice della funzione ChiamataProcesso e di tutte le FunzK e farne un programma T a sé stante.
In particolare ChiamataProcesso diventerà un metodo pubblico, che chiameremo Run, del form F di avvio del programma T stesso.Supponiamo anche di prevedere di lanciare un determinato numero n di volte T e di avere, quindi, di esso n istanze T1, T2, ..., Tn di T.
Il terzo passo sarà quello di fare un eseguibile Activex A che implementa due classi pubbliche: ClasseGlobale e ClasseServizio.
In qualche modo legheremo un oggetto G di tipo ClasseGlobale al nostro eseguibile iniziale E e un oggetto S (S1, S2, ..., Sn) di tipo ClasseServizio a ciascun form F delle istanze di T (T1, T2, ..., Tn).Riassumendo:
Sia E un eseguibile standard il quale ha la necessità di effettuare chiamate a più funzioni e che esse siano eseguite contemporaneamente in processi separati.
L'implementazione effettiva delle funzioni è fatta in n eseguibili standard T1, T2, ..., Tn lanciati come n istanze di un programma T.
L'insieme dei T1, T2, ..., Tn rappresenta il nostro pool di thread.
L'esecuzione di una funzione si effettua chiamando un metodo pubblico del form F di uno degli eseguibili T (F.Run).Un eseguibile Activex G implementa due classi pubbliche:
- ClasseGlobale da istanziarsi solo una volta da parte di E;
- ClasseServizio da istanziarsi n volte da parte di ciascuna istanza Tn.
Il funzionamento dello schema è il seguente:
- l'eseguibile E istanzia un oggetto G di tipo ClasseGlobale (fornito dall'Activex)
- l'oggetto G lancia n istanze del programma T
- ciascun eseguibile T istanzia un oggetto S di tipo ClasseServizio
- ciascun oggetto S imposta l'elemento n-esimo di un vettore di puntatori a oggetto in G ponendolo uguale al form Fn. In tal modo l'Activex è a conoscenza di un opportuno oggetto di interfaccia Fn per ciascun processo Tn.
A questo punto per chiamare una funzione in un processo separato T dovremo:
- Farci rilasciare un puntatore t a un thread libero mediante un metodo G.AllocaThread
Potrebbe sembrare inutile la conoscenza di tale puntatore, ma poiché l'esito e i dati in uscita dovranno essere restituiti mediante un evento di fine funzione
(G_FineProcesso) dobbiamo avere questa informazione per distinguere un ritorno da un altro.- Alzare un flag semaforo Attesa(t) (da abbassarsi al termine dell'evento FineProcesso)
- Chiamare un metodo G.Attivazione a cui si passi il puntatore al thread (t) e i parametri di attivazione della funzione
- Nella funzione G.Attivazione si chiama il metodo F.Run del form di interfaccia appartentente al processo Tt e tramite il metodo stesso si passano i parametri per l'attivazione del servizio
- Il metodo F.Run abilita un timer con la proprietà Interval settata a un millesimo di secondo e si memorizzano i parametri di attivazione. Questo è un
punto cruciale poichè la F.Run che bloccherebbe i processi di E dura pochissimo; la funzione effettiva viene chiamata dall'evento timer che, asincrono, ci permette di disaccoppiare i processi di E da quelli di T.- Nell'evento timer si disabilita il timer stesso e si chiama la funzione finale. Al termine si chiama il metodo G.ScatenaFineProcesso nel quale si esegue
la RaiseEvent dell'evento FineProcesso in E- Nell'evento FineProcesso si memorizzano i dati passati e si abbassa il flag semaforo Attesa(t)
- Attendere in un loop (con DoEvents) che il flag Attesa(t) sia basso
- Reperire la variabile di ritorno passata via evento FineProcesso
Queste operazioni possono essere incapsulate in una funzione di E.
I codici di esempio
La form del nostro eseguibile di esempio consta di due pulsanti (uno per funzione) e d'una label nella quale un Timer si occupa di scrivere il momento corrente, al puro scopo didattico di mostrare che nessun processo 'blocca' l'esecuzione dell'eseguibile principale (né di alcun altro).
Il codice iniziale di questa form effettua la dichiarazione delle variabili di modulo e la loro inizializzazione.Option Explicit Dim WithEvents G As ClasseGlobale Dim FlagAttesa() As Boolean Dim VettXmlOut() As String Private Sub Form_Load() Set G = New ClasseGlobale G.Inizializza ReDim FlagAttesa(G.MaxProc) ReDim VettXmlOut(G.MaxProc) End SubL'oggetto G viene istanziato e si inizializza:
Option Explicit Public ObjGlobale As ClasseGlobale Public MaxProcessi As Integer Public ExeServizio As String Public AllocazioneServizi As String Type StructServizio ObjForm As Object ObjServizio As ClasseServizio End Type Public VettServizi() As StructServizio Private Sub Class_Initialize() MaxProcessi = 3 ExeServizio = App.Path + "\thread.exe" ReDim VettServizi(1 To MaxProcessi) AllocazioneServizi = String$(MaxProcessi, "N") End SubEd attiva n processi (3), attendendo che ciascuno di essi sia caricato, registrato e pronto ad agire
Public Sub Inizializza() Dim I As Long Set ObjGlobale = Me For I = 1 To UBound(VettServizi) Shell ExeServizio + Str$(I), vbNormalNoFocus Do DoEvents Loop While VettServizi(I).ObjForm Is Nothing Do DoEvents Loop Until VettServizi(I).ObjForm.Pronto Next I End SubL'applicazione 'Processo' contiene una Shape circolare (semaforo verde-rosso) ed il Timer per eseguire le funzioni richieste, simulando un più o meno lungo tempo di elaborazione.
Ciascuno dei processi istanzia un oggetto ClasseServizio, si registra e si dichiara 'pronto'.Option Explicit Public Indice As Long Public Pronto As Boolean Dim S As Object Dim InputXmlIn As String Private Sub Form_Load() Indice = Val(Command$) Set S = CreateObject("CollanteActiveX.ClasseServizio") S.Registrazione Me Me.Pronto = True Shape1.FillColor = vbGreen End SubL'oggetto S riceve l'indice del processo cui appartiene, ed imposta i membri della relativa struttura StructServizio nel vettore dei servizi creato nell'oggetto G.
Option Explicit Dim IndiceProcesso As Long Public Sub Registrazione(Frm As Object) IndiceProcesso = Frm.Indice Set VettServizi(IndiceProcesso).ObjForm = Frm Set VettServizi(IndiceProcesso).ObjServizio = Me End SubCiascuno dei due pulsanti dell'applicazione E chiama la funzione ChiamataProcesso passandogli i parametri XmlDocIn e XmlDocOut (qui indicati come semplici stringhe); ricevuto di nuovo il controllo del flusso di programma, espone in una MessageBox il risultato.
Private Sub BtFunzione1_Click() Dim Buffer As String ChiamataProcesso "Stringa1", Buffer MsgBox Buffer End Sub Private Sub BtFunzione2_Click() Dim Buffer As String ChiamataProcesso "Stringa2", Buffer MsgBox Buffer End SubLa funzione ChiamataProcesso alloca il processo, alza il flag di attesa, attiva il processo, attende che il flag venga abbassato, ...
Public Sub ChiamataProcesso(XmlDocIn As String, XmlDocOut As String) Dim PtThread As Long PtThread = G.AllocaThread FlagAttesa(PtThread) = True G.AttivaProcesso PtThread, XmlDocIn Do DoEvents Loop Until FlagAttesa(PtThread) = False XmlDocOut = VettXmlOut(PtThread) G.RilasciaThread PtThread End SubIl metodo AllocaThread cerca tra i processi in esecuzione il primo libero e ne restituisce l'indice. (nota)
Public Function AllocaThread() As Long Dim PtThread As Long Do PtThread = InStr(AllocazioneServizi, "N") DoEvents Loop Until PtThread > 0 Mid$(AllocazioneServizi, PtThread) = "S" AllocaThread = PtThread End FunctionIl metodo AttivaProcesso passa il parametro di ingresso al processo indicato, chiamandone il metodo Run che attiva il Timer del processo cui appartiene, nel cui evento Timer viene effettivamente svolta l'operazione richiesta dall'eseguibile E. Nell'esempio, ci si limita a fare 'semaforo rosso' ("sono occupato!") e ad attendere un ipotetico tempo di esecuzione a seconda della funzk da eseguire.
Alla fine, viene impostato il risultato da restituire, tramite il metodo FineServizio dell'oggetto S, e il semaforo 'torna verde' ("sono di nuovo a disposizione").Private Sub Timer1_Timer() Dim StrDocOut As String Dim T As Single Shape1.FillColor = vbRed Timer1.Enabled = False Select Case InputXmlIn Case "Stringa1" T = Timer Do DoEvents Loop Until Abs(Timer - T) > 5 StrDocOut = "Prima risposta" Case "Stringa2" T = Timer Do DoEvents Loop Until Abs(Timer - T) > 10 StrDocOut = "Seconda risposta" Case Else StrDocOut = "Comando sconosciuto" End Select S.FineServizio StrDocOut Shape1.FillColor = vbGreen End SubIl metodo FineServizio dell'oggetto servizio 'riferisce' quanto di competenza al proprio oggetto ClasseGlobale:
Public Sub FineServizio(StrDocOut As String) ObjGlobale.ScatenaFineServizio IndiceProcesso, StrDocOut End SubIl metodo ScatenaFineServizio dell'oggetto G scatena l'evento fine del servizio indicato passando anche il risultato.
Friend Sub ScatenaFineServizio(PtThread As Long, StrXmlOut As String) RaiseEvent FineServizio(PtThread, StrXmlOut) End SubNell'eseguibile E, tale evento permette di impostare il relativo elemento del vettore risultati e di abbassare il flag di attesa.
Private Sub G_FineServizio(PtThread As Long, StrXmlOut As String) VettXmlOut(PtThread) = StrXmlOut FlagAttesa(PtThread) = False End Sub... A questo punto, la funzione ChiamataProcesso riceve il risultato del processo attivato, rilascia il processo, che cosė č pronto a ricevere un altro eventuale input.
Public Sub RilasciaThread(PtThread As Long) Mid$(AllocazioneServizi, PtThread) = "N" End SubResta ancora da dire che, ovviamente, la chiusura dell'applicazione E si occupa di annientare l'oggetto G istanziato e tramite esso i processi T avviati.
I sorgenti dei progetti relativi all'articolo sono scaricabili dall'Area Download. (nota)
Gli autori
Rodolfo Calzetti coordina il tema di sviluppo software di Centro Data Srl; laureato in matematica, ha esperienza di programmazione avanzata in VB e in C; ha realizzato diverse soluzioni Activex.
Maurizio Brasca è il responsabile di prodotto di Centro Data Srl, di cui è uno dei soci; si occupa della progettazione del software in ambito finanziario (tesoreria, titoli, ecc.).
Centro Data Srl è attiva nel software per la gestione finanziaria delle imprese, in particolare nella gestione della tesoreria e del portafoglio titoli.Centro Data Srl
Via Pedretti 1/B
20095 Cusano Milanino (MI)
Tel. 02/6134635
Fax 02/66400818Nota del 22/09/03:
Riguardando il codice vi segnalo un errore: se le richieste superano i thread disponibili il programma principale rimane bloccato in una attesa infinita. Sostituire in globale.cls il codicePublic Function AllocaThread() As Long Dim PtThread As Long Do PtThread = InStr(AllocazioneServizi, "N") DoEvents Loop Until PtThread > 0 Mid$(AllocazioneServizi, PtThread) = "S" AllocaThread = PtThread End Functioncon il codice:
Public Function AllocaThread() As Long Dim PtThread As Long PtThread = InStr(AllocazioneServizi, "N") If PtThread > 0 Then Mid$(AllocazioneServizi, PtThread) = "S" End If AllocaThread = PtThread End FunctionIl chiamante deve poi testare che AllocaThread() dia risultato maggiore di zero poiché altrimenti significa che il sistema non è in grado di soddisfare la richiesta.