12 KiB
Piano di Implementazione: Single-Thread Communication Proxy (Siemens S7)
1. Obiettivo
Garantire che tutte le interazioni con l'oggetto currPLC (libreria S7.Net / S7netplus, Plc)
avvengano esclusivamente su un unico thread proprietario, eliminando i fallimenti di comunicazione
e le eccezioni causate dall'accesso concorrente a una libreria non thread-safe, quando il
parametro MachWLoopSingleThread è true.
2. Diagnosi del problema (verificata sul codice)
2.1 currPLC viene toccato da ≥3 thread diversi
- Thread di init/UI: il costruttore
Siemens(AdapterFormNext, IobConfTree)chiamasetParamPlc()(Siemens.cs:1451) che creacurrPLC = new Plc(...)(:1501) e apre la connessione (tryConnect→currPLC.Open():1042) sul thread UI (vialoadIobType). - Thread macchina dedicato
WorkerLoopMachine(AdapterForm.cs) →doMachineTask(Generic.cs:723) →readSemafori/process*→S7ReadBB→currPLC.ReadBytes(:469). - Thread worker server (
Task.Runpool) →doServerTaskAsync(Generic.cs:853) →S7WriteBB→currPLC.WriteBytes(:573,:636) etryConnect(:1042).
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. S7.Net Plc non è thread-safe → Open/ReadBytes/WriteBytes/Close concorrenti su
thread diversi → errori di connessione / dati corrotti / mai comunicazione.
2.2 Verdetto sull'ipotesi "forzare MachWLoopSingleThread=true"
Necessario ma NON sufficiente. Verificato con grep su tutto il repo: il flag
MachWLoopSingleThread (IobDto.cs:101, caricato da IobConfTree.cs:132) non ha alcun
consumer nel codice condiviso (Generic.cs/AdapterForm.cs). I _singleFactory /
_singleScheduler (SingleThreadTaskScheduler) sono dichiarati in AdapterForm.cs:868/873 ma
mai usati; doMachineTask/doServerTaskAsync girano sul thread chiamante. Quindi forzare il
flag da solo non cambia il threading. Serve il proxy (che è l'unico consumer del flag, come in
FANUC Fanuc.cs:1828). Il flag va comunque forzato a true per attivare il ramo proxy.
2.3 Particolarità Siemens: sottoclassi accedono currPLC direttamente
A differenza di FANUC (tutti gli accessi dentro Fanuc.cs), currPLC è un campo
protected Plc acceduto direttamente da 17+ sottoclassi (SiemensAprochim, SiemensTorri,
SiemensAt2001, ...) con currPLC.IsConnected, LastErrorCode, LastErrorString, CPU,
MaxPDUSize, Rack, Slot, Open, Close, ClearLastError, IsAvailable. C'è anche una
classe legacy IobSiemensTorri_legacy.cs con un proprio currPLC.
3. Architettura Tecnica: Pattern Proxy
Componenti (interni alla classe Siemens):
_commQueue:BlockingCollection<Action>coda thread-safe._commThread: thread unico e persistente, avviato durante l'inizializzazione, unico proprietario dicurrPLC(inclusa la creazionenew Plc(...)).ExecuteProxy<T>(Func<T>): marshalling sincrono per il chiamante (tcs.Task.Result), con guardia di rientro (Thread.CurrentThread == _commThread→ inline) e fallback anti-hang sottolockquando il thread è morto (stesso pattern di FANUC).
Strategia di marshalling
Dato l'accesso diretto di currPLC da base + sottoclassi, la soluzione a punto unico è un
PlcProxy: un wrapper che espone la stessa superficie usata di S7.Net Plc e marshalla
internamente ogni chiamata sul comm-thread. Così nessuna sottoclasse va modificata.
Superficie da replicare nel PlcProxy (verificata dai call-site):
ReadBytes(DataType,int,int,int)→byte[], WriteBytes(DataType,int,int,byte[])→ErrorCode,
Open(), Close(), IsConnected(bool), IsAvailable(bool), LastErrorCode(ErrorCode),
LastErrorString(string), ClearLastError(), CPU(CpuType), MaxPDUSize(int), Rack(int),
Slot(int), Dispose().
Gestione ref/out
S7.Net restituisce byte[]/ErrorCode come valori di ritorno (niente ref nei call-site di
Siemens), quindi il marshalling è semplice: ExecuteProxy(() => currPLC.Real.ReadBytes(...)).
Nessun copy-in/copy-out necessario (a differenza di FANUC).
4. Modifiche ai Componenti
A. Infrastruttura (interna a Siemens.cs)
_commQueue,_commThread,_lockSync,_isProxyActive.StartCommThread()/StopCommThread()(come FANUC:CompleteAdding+Join(timeout)).ExecuteProxy<T>(Func<T>)con guardia di rientro e fallback inline sotto lock.
B. PlcProxy (nuova classe interna)
- Espone la superficie sopra; internamente tiene il Plc reale e marshalla ogni chiamata:
ExecuteProxy(() => _real.ReadBytes(...)),ExecuteProxy(() => _real.Open()), ecc. - I costruttori /
new Plc(...)vengono marshallati sul comm-thread (il Plc nasce sul thread proprietario). IsConnected,IsAvailable,LastErrorCode/LastErrorString,CPU... → proprietà marshalled (ExecuteProxy(() => _real.IsConnected)).
C. Lifecycle del thread
- Costruttore/
setParamPlc:StartCommThread()PRIMA dinew Plc(...); la creazione + assegnazionecurrPLCpassano dal proxy. tryConnect/tryDisconnect:Open/Closemarshallati (già coperti dalPlcProxy).startAdapteroverride: se_commThreadnon vivo → riavvio + ricreazione Plc sul nuovo thread (path stop→start), senza duplicarenew Plcsull'avvio normale.stopAdapter/Dispose: fermare il thread;Dispose()marshallacurrPLC.Dispose().MachWLoopSingleThreadforzato atruenel costruttore (per attivare il ramo proxy).
D. Classe legacy IobSiemensTorri_legacy.cs
Ha un proprio currPLC con lo stesso problema. Se non è più usata in produzione: documentare e
lasciare. Se è usata: applicare lo stesso PlcProxy.
5. Validazione e Test
- Log: tutte le chiamate a
currPLCdevono presentare lo stessoManagedThreadIddel comm-thread. - Test di connessione:
tryConnect(Open) e read/write devono funzionare attraverso il proxy. - Test stop→start e restart:
_commThreadricreato con ricreazionePlc, senza double-create.
6. Note di sicurezza (pitfall)
- Il flag da solo non basta: senza proxy,
MachWLoopSingleThread=truenon cambia il threading. - Reentrancy: guardia
Thread.CurrentThread == _commThreadinExecuteProxy. - Hang: fallback inline sotto lock se il thread è morto.
- Shutdown:
StopCommThread()sustopAdapter/Dispose; nienteCompleteAddingdurante il run. - Sottoclassi: con
PlcProxynessuna modifica necessaria; il campo cambia tipo (protected Plc currPLC→protected PlcProxy currPLC).
7. Stato implementazione (aggiornato a build Release passata)
Implementato in IOB-WIN-SIEMENS\IobSiemens\Siemens.cs
- Infrastruttura proxy:
_commQueue,_commThread,_lockSync,_isProxyActive+StartCommThread()/StopCommThread()/ExecuteProxy<T>()(guardia di rientro + fallback inline anti-hang). PlcProxy: classe annidata che marshalla ogni chiamata sul comm-thread; ilPlcreale nasce sul comm-thread (ExecuteProxy(() => new Plc(...))). Copre tutta la superficie usata da base + sottoclassi:ReadBytes,WriteBytes,Open,Close,ClearLastError,IsConnected,IsAvailable,LastErrorCode,LastErrorString,CPU,MaxPDUSize,Rack,Slot,Dispose.- Campo:
protected Plc currPLC→protected PlcProxy currPLC. - Costruttore:
MachWLoopSingleThread = true+StartCommThread()prima disetParamPlc(). setParamPlc:currPLC = new PlcProxy(this, ...)(creazione marshalled).- Lifecycle: override
startAdapter(riavvia thread + ricreaPlcProxyse non vivo) estopAdapter(base +StopCommThread());Dispose()marshallacurrPLC.Dispose()+StopCommThread().
Deviazioni riscontrate durante l'implementazione
- CS0052 ("tipo di campo meno accessibile del campo"):
PlcProxydeve essereprotected sealed class(coerente col campoprotected), noninternal. - Tipi esatti S7.Net (verificati con reflection sulla
S7.Net.dll):CPUèCpuType,Rack/Slot/MaxPDUSizesonoshort(Int16),Open()ritornaErrorCode(nel wrapperOpen()èvoid, il ritorno viene scartato),LastErrorCodeèErrorCode,IsConnected/IsAvailablesonobool.
Verifica
- Build Release
IOB-WIN-SIEMENS: exit 0, 0 errori CS (base + tutte le sottoclassi che accedono acurrPLC.IsConnectedecc. compilano controPlcProxy). - Non è stato modificato nessun altro file (sottoclassi, config, csproj).
8. Fix successivi scoperti durante la verifica runtime (memMap nullo + affinità)
8.1 Bug di affinità introdotto inizialmente (corretto)
setParamPlc() è chiamato due volte: nel costruttore Generic (Generic.cs:75) e nel
costruttore Siemens. La prima chiamata crea currPLC = new PlcProxy(...) prima che
StartCommThread() parta (avviato nel corpo del costruttore Siemens, che gira DOPO base(...)),
quindi ExecuteProxy usava il fallback inline → il Plc reale nasceva sul thread sbagliato, e la
seconda setParamPlc (con needRefresh=false) NON ricreava currPLC → affinità S7 rotta.
Fix: MachWLoopSingleThread=true + StartCommThread() spostati all'inizio dell'override
setParamPlc (Siemens.cs:1636), così la prima chiamata (dal costruttore Generic) avvia il
comm-thread e crea currPLC marshalled sul thread corretto; la seconda è un no-op (thread già vivo).
8.2 memMap nullo all'avvio (fixato)
Per molte config Siemens IOBConfFull.Memory è null (nessuna memory map: mMapRead/mMapWrite
non configurati). memMap è un campo public plcMemMapExt con default null (BaseObj.cs:169).
loadMemConf() lo setta, ma getDynData (Siemens.cs:181) legge memMap.mMapRead.Count senza
guardia → NRE all'avvio se memMap resta null.
Fix:
- Nel costruttore
Siemens, doposetParamPlc(): sememMap == null→memMap = new plcMemMapExt()(mappa vuota, non serve per molte config Siemens) +lgWarn. - Guardie difensive nelle letture:
getDynData(memMap != null && mMapRead != null) eplcWriteParams(memMap != null && mMapWrite != null).
Verifica finale
- Build Release
IOB-WIN-SIEMENS: exit 0, 0 errori CS. - Il costruttore ora garantisce
memMapnon-null ecurrPLCcreato sul comm-thread.
9. Fix della doppia costruzione + mancato auto-avvio (AdapterForm.cs, classe condivisa)
9.1 Problema
AdapterForm_Load (AdapterForm.cs:1186) chiamava await loadIobType() incondizionatamente.
Con autoLoadConf=true, loadIniFile creava già iobObj (Siemens #1) e poi AdapterForm_Load
ne creava un secondo (Siemens #2) sostituendo iobObj:
- il costruttore
Siemensgirava 2 volte; - l'
AvviaAdapterautomatico (autoStartOnLoad=trueinApp.config) avviava il 1° oggetto orfanizzato, mentre l'oggetto attivo (2°) restava non avviato → nessuna comunicazione PLC, e l'utente doveva premerestart_Click. - Inoltre la connessione del 1° Siemens restava aperta (leak): il
Open()del 2° poteva fallire (connessioni PLC limitate) →connectionOk=false→ nessuna comunicazione PLC.
9.2 Fix
In AdapterForm_Load, loadIobType() viene chiamato solo se iobObj è ancora null:
if (iobObj == null)
{
await loadIobType();
displayTaskAndLog("Waiting for config file selection");
}
Con autoLoadConf=true (iobObj già creato da loadIniFile) non si ricrea → singola costruzione,
e l'AvviaAdapter automatico agisce sull'oggetto attivo. Il fix è nella classe condivisa e vale
per tutti gli adapter (FANUC, SIEMENS, ecc.).
9.3 Verifica
- Build Release
IOB-WIN-FORMeIOB-WIN-SIEMENS: exit 0, 0 errori CS. BaseObj.cs(init memMap) è la tua modifica, lasciata invariata.