Files
2026-08-12 13:22:40 +02:00

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 FanucRuntime.CreateNC + tryConnect);
  • il thread macchina dedicato WorkerLoopMachine (AdapterForm.cs:2542) → doMachineTask (Generic.cs:723) → metodi process* override di Fanuc che chiamano FANUC_ref direttamente;
  • il thread worker server (Task.Run pool) → doServerTaskAsync (AdapterForm.cs:1556/1713) e tryConnect (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 di FANUC_ref (incluso Runtime.CreateNC).
  • ExecuteProxy<T>(Func<T>): marshalling sincrono per il chiamante (blocca su tcs.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)

  1. _commQueue (BlockingCollection<Action>) + _commThread (campo).
  2. 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.
  3. StopCommThread(): CompleteAdding() + Join(timeout) + _commThread = null.
  4. ExecuteProxy<T>(Func<T>):
    • se MachWLoopSingleThread && _commThread != null && _commThread.IsAlive: accoda l'azione e ritorna tcs.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).

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 di Runtime.CreateNC; la creazione NC + assegnazione FANUC_ref vengono marshallate sul comm-thread.
  • startAdapter override: se _commThread non è vivo → StartCommThread() + ricreazione NC marshallata sul nuovo thread (copre il path stop→start sullo stesso oggetto senza duplicare CreateNC sul path di avvio normale); poi base.startAdapter.
  • stopAdapter override: await base.stopAdapter(...) (che marshalla la Disconnect) poi StopCommThread() (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.ConnectedExecuteProxy(() => FANUC_ref.Connected) (incluso dentro le stringhe di log interpolate).
  • F_RW_*, F_Read_macro, getAllDynData, getSysInfo, getPrgNameMain, Is30Series con ref a variabili localiExecuteProxy(() => FANUC_ref.X(..., ref locale)).
  • Connect/Disconnect con ref locale → ExecuteProxy(() => { FANUC_ref.X(ref l); return true; }).
  • 4 helper copy-in/out per i wrapper pubblici con ref parametro: ProxyMacroShort, ProxyMacroDouble (per FanucMemMacroRW), ProxyByte, ProxyByteArr (per FanucMemRW byte/byte[]).

5. Validazione e Test

  • Log: tutte le chiamate a FANUC_ref devono presentare lo stesso ManagedThreadId del comm-thread (FANUC-Comm-Thread), NON del WorkerLoopMachine.
  • Test di connessione: tryConnect deve funzionare attraverso il proxy.
  • Test stop→start e restart: verificare che _commThread venga 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 ExecuteProxy resta difensivo.
  • Hang: il fallback inline sotto lock evita tcs.Task.Result che si blocca se il thread è morto.
  • Shutdown: StopCommThread() su stopAdapter; niente CompleteAdding durante il run.
  • MachWLoopSingleThread è forzato a true nel costruttore (Fanuc.cs:67): il ramo proxy è sempre attivo per FANUC; il ramo else con lock è un fallback di sicurezza.