6.0 KiB
Piano di Implementazione: Single-Thread Communication Proxy (Fanuc)
1. Obiettivo
Garantire che tutte le interazioni con l'oggetto FANUC_ref avvengano esclusivamente sul
thread che ha effettuato l'inizializzazione della connessione, eliminando i crash e le
instabilità causate dalla mancanza di thread-safety del driver nativo, quando il parametro
MachWLoopSingleThread è impostato su true.
2. Diagnosi del problema (perché il pattern proxy è la soluzione)
FANUC_ref viene toccato da almeno 3 thread diversi:
- il thread di init/UI (costruttore
Fanuc→Runtime.CreateNC+tryConnect); - il thread macchina dedicato
WorkerLoopMachine(AdapterForm.cs:2542) →doMachineTask(Generic.cs:723) → metodiprocess*override diFanucche chiamanoFANUC_refdirettamente; - il thread worker server (
Task.Runpool) →doServerTaskAsync(AdapterForm.cs:1556/1713) etryConnect(2498).
Il _plcLock (SemaphoreSlim) in AdapterForm serializza solo i tempi di esecuzione tra
macchina e server, non l'affinità di thread: chi vince il lock esegue su thread diversi nel
tempo. Il driver FOCAS richiede tutte le chiamate sullo stesso thread → crash atteso. Il check
validThread in WorkerLoopMachine (2556) verifica solo il loop macchina, non l'accesso a
FANUC_ref, quindi non basta.
Soluzione: marshalling di TUTTE le chiamate a FANUC_ref su un unico thread proprietario
(_commThread), che è anche il thread che crea la connessione NC.
3. Architettura Tecnica: Pattern Proxy
Componenti del Proxy (interni alla classe Fanuc):
_commQueue:BlockingCollection<Action>coda thread-safe delle azioni da eseguire._commThread: thread unico e persistente, avviato durante l'inizializzazione. Unico proprietario diFANUC_ref(inclusoRuntime.CreateNC).ExecuteProxy<T>(Func<T>): marshalling sincrono per il chiamante (blocca sutcs.Task.Result), asincrono rispetto al thread del proxy.
Strategia di marshalling (LEAF, non body)
Ogni accesso diretto a FANUC_ref.X(...) viene sostituito con una chiamata a ExecuteProxy.
Questo garantisce l'affinità di thread per ogni singola operazione FOCAS e non introduce
reentrancy (le lambda sul comm-thread chiamano solo FANUC_ref direttamente, mai metodi
proxied), quindi niente deadlock da rientro.
Gestione dei parametri ref/out
I thread non possono catturare ref/out di un parametro di metodo in una lambda. Per i
metodi wrapper pubblici con ref parametro servono helper dedicati con copy-in/copy-out:
copiano il valore in una variabile locale, marshallano la chiamata che scrive la locale, e al
ritorno riscrivono la locale nel parametro. Per i ref a variabili locali (che una lambda
può catturare) si usa il marshalling inline.
4. Modifiche ai Componenti (dettaglio)
A. Infrastruttura (interna a Fanuc.cs)
_commQueue(BlockingCollection<Action>) +_commThread(campo).StartCommThread(): crea/avvia il thread dedicato se non è vivo; crea una coda nuova se serve (riavvio). Il thread drena la coda in loop, inghiottendo le eccezioni delle singole azioni per non morire.StopCommThread():CompleteAdding()+Join(timeout)+_commThread = null.ExecuteProxy<T>(Func<T>):- se
MachWLoopSingleThread && _commThread != null && _commThread.IsAlive: accoda l'azione e ritornatcs.Task.Result(propaga le eccezioni al chiamante); - altrimenti (thread non attivo / flag off): esegue inline sotto
lock(_lockSync)come fallback anti-hang (durante lo stop nessuna chiamata deve arrivare comunque).
- se
B. Lifecycle del thread (CORRETTO rispetto alla versione precedente)
Il thread dedicato deve essere lo stesso che crea FANUC_ref (affinità FOCAS). Non va mai fermato/riavviato a metà vita dell'oggetto, altrimenti il nuovo thread ≠ thread creatore.
- Costruttore:
StartCommThread()PRIMA diRuntime.CreateNC; la creazione NC + assegnazioneFANUC_refvengono marshallate sul comm-thread. startAdapteroverride: se_commThreadnon è vivo →StartCommThread()+ ricreazione NC marshallata sul nuovo thread (copre il path stop→start sullo stesso oggetto senza duplicare CreateNC sul path di avvio normale); poibase.startAdapter.stopAdapteroverride:await base.stopAdapter(...)(che marshalla laDisconnect) poiStopCommThread()(nessun leak sul restart che ricrea l'oggetto).
C. Rifattorizzazione dei metodi di I/O
Sostituzione delle chiamate dirette a FANUC_ref con il proxy:
FANUC_ref.Connected→ExecuteProxy(() => FANUC_ref.Connected)(incluso dentro le stringhe di log interpolate).F_RW_*,F_Read_macro,getAllDynData,getSysInfo,getPrgNameMain,Is30Seriesconrefa variabili locali →ExecuteProxy(() => FANUC_ref.X(..., ref locale)).Connect/Disconnectconreflocale →ExecuteProxy(() => { FANUC_ref.X(ref l); return true; }).- 4 helper copy-in/out per i wrapper pubblici con
refparametro:ProxyMacroShort,ProxyMacroDouble(perFanucMemMacroRW),ProxyByte,ProxyByteArr(perFanucMemRWbyte/byte[]).
5. Validazione e Test
- Log: tutte le chiamate a
FANUC_refdevono presentare lo stessoManagedThreadIddel comm-thread (FANUC-Comm-Thread), NON del WorkerLoopMachine. - Test di connessione:
tryConnectdeve funzionare attraverso il proxy. - Test stop→start e restart: verificare che
_commThreadvenga ricreato con ricreazione NC e che non ci siano double-create sull'avvio normale.
6. Note di sicurezza (pitfall)
- Reentrancy: con marshalling leaf non c'è rientro, ma
ExecuteProxyresta difensivo. - Hang: il fallback inline sotto lock evita
tcs.Task.Resultche si blocca se il thread è morto. - Shutdown:
StopCommThread()sustopAdapter; nienteCompleteAddingdurante il run. MachWLoopSingleThreadè forzato atruenel costruttore (Fanuc.cs:67): il ramo proxy è sempre attivo per FANUC; il ramoelsecon lock è un fallback di sicurezza.