2014-09-08 3 views
17

In uno dei progetti a cui partecipo c'è un vasto uso di WeakAction. Questa è una classe che consente di mantenere il riferimento a un'istanza di azione senza che la sua destinazione non sia raccolta di dati inutili. Il modo in cui funziona è semplice, richiede un'azione sul costruttore e mantiene un riferimento debole all'obiettivo dell'azione e al metodo, ma elimina il riferimento all'azione stessa. Quando arriva il momento di eseguire l'azione, controlla se il bersaglio è ancora vivo e, in caso affermativo, invoca il metodo sulla destinazione.Bug in WeakAction in caso di azione di chiusura

Funziona tutto bene tranne che per un caso - quando l'azione viene istanziata in una chiusura. consideri il seguente esempio:

public class A 
{ 
    WeakAction action = null; 

    private void _register(string msg) 
    { 
     action = new WeakAction(() => 
     { 
      MessageBox.Show(msg); 
     } 
    } 
} 

Dal momento che l'espressione lambda sta usando la variabile locale msg, l'auto compilatore C# genera una classe annidata per contenere tutte le variabili di chiusura. La destinazione dell'azione è un'istanza della classe nidificata invece dell'istanza di A. L'azione passata al costruttore WeakAction non viene referenziata una volta che il costruttore è terminato e quindi il garbage collector può eliminarlo istantaneamente. Successivamente, se viene eseguito il WeakAction, non funzionerà perché la destinazione non è più attiva, anche se l'istanza originale di A è attiva.

Ora non riesco a cambiare il modo in cui viene chiamato il WeakAction (poiché è ampiamente utilizzato), ma posso modificarne l'implementazione. Stavo pensando di provare a trovare un modo per accedere all'istanza di A e forzare l'istanza della classe nidificata a rimanere in vita mentre l'istanza di A è ancora viva, ma non so come ottenerla.

Ci sono un sacco di domande su ciò che A ha a che fare con qualsiasi cosa, e suggerimenti per cambiare il modo in A crea una debole azione (che non siamo in grado di fare) ecco una precisazione:

Un'istanza della classe A desidera un'istanza della classe B per notificarlo quando succede qualcosa, quindi fornisce una richiamata utilizzando un oggetto Action. A non sa che B utilizza azioni deboli, fornisce semplicemente un Action da utilizzare come richiamata. Il fatto che B utilizzi WeakAction è un dettaglio di implementazione non esposto. B deve memorizzare questa azione e usarla quando necessario. Ma lo B potrebbe vivere molto più a lungo di A, e mantenendo un forte riferimento a una normale azione (che di per sé contiene un riferimento forte dell'istanza di A che lo ha generato) fa sì che A non venga mai sottoposto a garbage collection. Se A fa parte di un elenco di elementi che non sono più in vita, ci si aspetta che A venga raccolto in modo non corretto e poiché il riferimento è valido per B dell'azione, che di per sé punta a A, si verifica una perdita di memoria.

Così, invece di B in possesso di un azione che A fornito, B avvolge in una WeakAction e memorizza solo l'azione deboli. Quando arriva il momento di chiamarlo, B lo fa solo se lo WeakAction è ancora attivo, che dovrebbe essere finché lo A è ancora attivo.

A crea quell'azione all'interno di un metodo e non mantiene un riferimento a se stesso da solo - è un dato. Poiché il Action è stato creato nel contesto di un'istanza specifica di A, quell'istanza è la destinazione di A e quando A muore, tutti i riferimenti deboli ad esso diventano null quindi B sa di non chiamarlo e smaltisce l'oggetto WeakAction.

Ma a volte il metodo che ha generato il Action utilizza le variabili definite localmente in tale funzione. Nel qual caso il contesto in cui viene eseguita l'azione include non solo l'istanza di A, ma anche lo stato delle variabili locali all'interno del metodo (che è chiamato "chiusura"). Il compilatore C# lo fa creando una classe nidificata nascosta per contenere queste variabili (chiamiamola A__closure) e l'istanza che diventa la destinazione di Action è un'istanza di A__closure, non di A. Questo è qualcosa di cui l'utente non dovrebbe essere a conoscenza. Tranne che questa istanza di A__closure è referenziata solo dall'oggetto Action. E poiché creiamo un riferimento debole all'obiettivo e non manteniamo un riferimento all'azione, non vi è alcun riferimento all'istanza A__closure e il garbage collector può (e di solito lo fa) eliminarlo istantaneamente. Quindi, A__closure vive, A__closure, e nonostante il fatto che A si aspetti ancora che il callback venga richiamato, lo B non può farlo.

Questo è l'errore.

La mia domanda era se qualcuno conosce un modo in cui il WeakAction costruttore, l'unico pezzo di codice che in realtà contiene l'oggetto di azione originale, in via temporanea, può in qualche modo magico estrarre l'istanza originale del A dall'istanza A__closure che trova nello Target dello Action. In tal caso, potrei forse estendere il ciclo di vita A__Closure a quello dello A.

+0

È stato risolto nelle versioni successive di C#? –

risposta

7

Dopo un po 'di ricerche e dopo aver raccolto tutti i bit utili di informazioni dalle risposte che sono state pubblicate qui, mi sono reso conto che non ci sarà una soluzione elegante e sigillata al problema. Poiché questo è un problema di vita reale, siamo andati con l'approccio pragmatico, cercando di ridurlo almeno gestendo il maggior numero possibile di scenari, quindi volevo pubblicare ciò che facevamo.

Un'indagine approfondita dell'oggetto Action passato al costruttore di WeakEvent, e in particolare alla proprietà Action.Target, ha mostrato che esistono effettivamente 2 diversi casi di oggetti di chiusura.

Il primo caso è quando Lambda utilizza le variabili locali dall'ambito della funzione chiamante, ma non utilizza alcuna informazione dall'istanza della classe A. Nell'esempio seguente, supponiamo EventAggregator.Register è un metodo che accetta un'azione e memorizza un'azione Weak che lo avvolge.

public class A 
{ 
    public void Listen(int num) 
    { 
     EventAggregator.Register<SomeEvent>(_createListenAction(num)); 
    } 

    public Action _createListenAction(int num) 
    { 
     return new Action(() => 
     { 
      if (num > 10) MessageBox.Show("This is a large number"); 
     }); 
    } 
} 

Il lambda creato qui utilizza la variabile num, che è una variabile locale definita nell'ambito della funzione _createListenAction. Quindi il compilatore deve avvolgerlo con una classe di chiusura per mantenere le variabili di chiusura. Tuttavia, poiché lambda non accede a nessuno dei membri di classe A, non è necessario memorizzare un riferimento ad A. Il target dell'azione non includerà quindi alcun riferimento all'istanza A e non vi è assolutamente alcun modo per il costruttore di WeakAction per raggiungerlo.

Il secondo caso è illustrato nel seguente esempio:

public class A 
{ 
    int _num = 10; 

    public void Listen() 
    { 
     EventAggregator.Register<SomeEvent>(_createListenAction()); 
    } 

    public Action _createListenAction() 
    { 
     return new Action(() => 
     { 
      if (_num > 10) MessageBox.Show("This is a large number"); 
     }); 
    } 
} 

Ora _num non è fornito come parametro della funzione, viene dalla classe A istanza. L'uso della reflection per conoscere la struttura dell'oggetto Target rivela che l'ultimo campo definito dal compilatore contiene un riferimento all'istanza della classe A. Questo caso si applica anche quando il lambda contiene chiamate ai metodi membri, come il seguente esempio:

public class A 
{ 
    private void _privateMethod() 
    { 
     // do something here 
    }   

    public void Listen() 
    { 
     EventAggregator.Register<SomeEvent>(_createListenAction()); 
    } 

    public Action _createListenAction() 
    { 
     return new Action(() => 
     { 
      _privateMethod(); 
     }); 
    } 
} 

_privateMethod è una funzione membro, così è chiamato nel contesto della classe A esempio, così la chiusura deve tenere un riferimento ad esso per invocare il lambda nel giusto contesto.

Quindi il primo caso è una chiusura che contiene solo funzioni variabili locali, la seconda contiene un riferimento all'istanza A genitore. In entrambi i casi, non vi sono riferimenti duri all'istanza di Closure, quindi se il costruttore di WeakAction lascia le cose come sono, la WeakAction "morirà" istantaneamente malgrado il fatto che l'istanza di classe A sia ancora attiva.

Ci troviamo di fronte qui con 3 diversi problemi:

  1. come identificare che l'obiettivo dell'azione è annidata chiusura istanza di classe, e non l'originale Un esempio?
  2. Come ottenere un riferimento all'istanza di classe A originale?
  3. Come prolungare la durata dell'istanza di chiusura in modo che viva finché l'istanza A è attiva, ma non oltre?

La risposta alla prima domanda è che ci affidiamo a 3 caratteristiche del l'istanza di chiusura: - È privato (per essere più precisi, non è "visibile".Quando si utilizza il compilatore C#, il tipo riflesso ha IsPrivate impostato su true ma con VB no. In tutti i casi, la proprietà IsVisible è false). - È annidato. - Come @DarkFalcon menzionato nella sua risposta, è decorato con l'attributo [CompilerGenerated].

private static bool _isClosure(Action a) 
{ 
    var typ = a.Target.GetType(); 
    var isInvisible = !typ.IsVisible; 
    var isCompilerGenerated = Attribute.IsDefined(typ, typeof(CompilerGeneratedAttribute)); 
    var isNested = typ.IsNested && typ.MemberType == MemberTypes.NestedType; 


    return isNested && isCompilerGenerated && isInvisible; 
} 

Anche se questo non è un predicato sigillata 100% (un programmatore malintenzionato può generare una classe privata nidificato e decorare con l'attributo CompilerGenerated), in scenari di vita reale questo è abbastanza accurata, e ancora una volta, stiamo costruendo una soluzione pragmatica, non accademica.

Quindi il problema numero 1 è risolto. Il costruttore di azioni deboli identifica le situazioni in cui l'obiettivo dell'azione è una chiusura e risponde a questo.

Il problema 3 è anche facilmente risolvibile. Come @usr ha scritto nella sua risposta, una volta ottenuta l'istanza della classe A, aggiungendo una ConditionalWeakTable con una singola voce dove l'istanza della classe A è la chiave e l'istanza di chiusura è la destinazione, risolve il problema. Il garbage collector sa di non raccogliere l'istanza di chiusura finché dura l'istanza della classe A. Quindi va bene.

L'unico problema non risolvibile è il secondo, come ottenere un riferimento all'istanza di classe A? Come ho detto, ci sono 2 casi di chiusure. Uno in cui il compilatore crea un membro che detiene questa istanza e uno in cui non lo fa. Nel secondo caso, semplicemente non c'è modo di ottenerlo, quindi l'unica cosa che possiamo fare è creare un riferimento difficile all'istanza di chiusura per evitare che venga immediatamente raccolto dalla garbage collection. Ciò significa che potrebbe vivere l'istanza di classe A (infatti vivrà fino a quando l'istanza di WeakAction vivrà, che potrebbe essere per sempre). Ma il non è un caso così terribile dopo tutto. La classe di chiusura in questo caso contiene solo alcune variabili locali e nel 99,9% dei casi è una struttura molto piccola. Mentre questa è ancora una perdita di memoria, non è sostanziale.

Ma proprio al fine di consentire agli utenti di evitare anche quella perdita di memoria, si aggiunge oggi un costruttore aggiuntivo alla classe WeakAction, come segue:

public WeakAction(object target, Action action) {...} 

E quando questo costruttore viene chiamato, aggiungiamo un Voce ConditionalWeakTable in cui il target è la chiave e il target delle azioni è il valore. Manteniamo anche un debole riferimento sia al target che all'azione target e se qualcuno di loro muore, cancelliamo entrambi. In modo che l'obiettivo delle azioni non sia inferiore né inferiore all'obiettivo fornito. Ciò consente in sostanza all'utente di WeakAction di dire di trattenere l'istanza di chiusura fino a quando il bersaglio è vivo. Quindi ai nuovi utenti verrà detto di usarlo per evitare perdite di memoria. Ma nei progetti esistenti, dove questo nuovo costruttore non viene utilizzato, ciò minimizza almeno le perdite di memoria alle chiusure che non hanno alcun riferimento all'istanza di classe A.

Il caso di chiusure che fanno riferimento al genitore è più problematico perché interessa la raccolta di garbase. Se teniamo un riferimento forte alla chiusura, causiamo una perdita di memoria molto più drastica perché l'istanza di classe A non verrà mai cancellata. Ma questo caso è anche più facile da trattare. Poiché il compilatore aggiunge un ultimo membro che contiene un riferimento all'istanza di classe A, utilizziamo semplicemente reflection per estrarlo e fare esattamente ciò che facciamo quando l'utente lo fornisce nel costruttore. Identifichiamo questo caso quando l'ultimo membro dell'istanza di chiusura è dello stesso tipo del tipo dichiarante della classe annidata di chiusura. (Di nuovo, non è preciso al 100%, ma per casi reali è abbastanza vicino).

Per riassumere, la soluzione che ho presentato qui non è una soluzione sigillata al 100%, semplicemente perché non sembra esserci una soluzione del genere. Ma dal momento che dobbiamo fornire una risposta a questo fastidioso bug, questa soluzione riduce almeno il problema in modo sostanziale.

2

a.Target fornisce l'accesso all'oggetto che contiene i parametri lambda. Eseguendo un GetType questo restituirà il tipo generato dal compilatore. Un'opzione consisterebbe nel controllare questo tipo per l'attributo personalizzato System.Runtime.CompilerServices.CompilerGeneratedAttribute e mantenere in questo caso un forte riferimento all'oggetto.

Now I can't change the way the WeakAction is called, (since it's in wide use) but I can change it's implementation. Si noti che questo è l'unico modo finora che può tenerlo in vita senza richiedere modifiche a come è stato costruito il WeakAction. Inoltre, non raggiunge l'obiettivo di mantenere viva la lambda fino a quando l'oggetto A (lo manterrebbe attivo finché il WeakAction invece). Non credo che sarà raggiungibile senza cambiare il modo in cui lo WeakAction è costruito come è fatto nelle altre risposte. Come minimo, lo WeakAction deve ottenere un riferimento all'oggetto A, che attualmente non si fornisce.

+0

In effetti il ​​target ha l'attributo generato dal compilatore ma se tengo un forte riferimento ad esso, posso causare perdite di memoria. Ad esempio, se l'azione chiama i metodi da A, in realtà mantiene un riferimento all'istanza di A e quindi questa istanza non sarà mai raccolta. –

+0

Quindi non hai scelta, DEVI cambiare la modalità di costruzione di 'WeakAction'. Penso che 'ConditionalWeakTable' sia un buon approccio (Fai' WeakAction' usarne uno.) –

+0

È un buon approccio per usi futuri, ma dobbiamo semplicemente supportare gli usi a ritroso dell'azione debole senza modifiche alla firma. Altrimenti non è una soluzione accettabile. –

5

Si desidera estendere la durata dell'istanza della classe di chiusura in modo che corrisponda esattamente all'istanza A. Il CLR ha uno speciale tipo di handle GC per questo: the Ephemeron, implementato come internal struct DependentHandle.

  1. Questa struttura è esposta solo come parte della classe ConditionalWeakTable. È possibile creare una tabella di questo tipo per WeakAction con esattamente un elemento al suo interno. La chiave sarebbe un'istanza di A, il valore sarebbe l'istanza della classe di chiusura.
  2. In alternativa, è possibile forzare lo DependentHandle utilizzando la riflessione privata.
  3. Oppure, è possibile utilizzare un'istanza ConditionalWeakTable condivisa a livello globale. Probabilmente richiede l'uso della sincronizzazione. Guarda i documenti.

Considerare l'apertura di un problema di connessione per rendere pubblico lo DependentHandle e fornire un caso d'uso.

+0

grazie, questi sembrano grandi suggerimenti tranne che non so come ottenere una sospensione dell'istanza A. C'è un modo per farlo con le classi annidate? –

+0

@KobiHari potresti prendere una dipendenza dagli interni del compilatore C# ed estrarre un riferimento dalla chiusura usando la riflessione. Decompila alcuni assembly per scoprire come generano i nomi .; Meglio: fai in modo che il chiamante del nuovo passaggio di WeakAction sia in una serie di elementi che desidera utilizzare come "radici di GC". Il chiamante deve passare in un 'A'. – usr

+0

Mi piacerebbe sentire ulteriori dettagli sulla prima opzione. Per quanto riguarda il secondo, non posso modificare la firma delle azioni deboli. –