c# - run - .NET 4.5 Async/Await y el recolector de basura
task.run c# (6)
¿Recibirá basura recolectada mientras espera?
Nop.
¿La memoria estará ocupada durante la duración de la función?
Sí.
Abra su ensamblado compilado en Reflector. Verá que el compilador generó una estructura privada que hereda de IAsyncStateMachine, las variables locales de su método async son un campo de esa estructura. Los campos de datos de la clase / estructura nunca se liberan mientras la instancia propietaria todavía está activa.
Me pregunto sobre el comportamiento de async/await
en relación con la basura que recoge variables locales. En el siguiente ejemplo, he asignado una porción considerable de memoria y entrado en un retraso significativo. Como se ve en el código, Buffer
no se usa después de await
. ¿Obtendrá basura recogida mientras espera, o la memoria estará ocupada durante la duración de la función?
/// <summary>
/// How does async/await behave in relation to managed memory?
/// </summary>
public async Task<bool> AllocateMemoryAndWaitForAWhile() {
// Allocate a sizable amount of memory.
var Buffer = new byte[32 * 1024 * 1024];
// Show the length of the buffer (to avoid optimization removal).
System.Console.WriteLine(Buffer.Length);
// Await one minute for no apparent reason.
await Task.Delay(60000);
// Did ''Buffer'' get freed by the garabage collector while waiting?
return true;
}
¿Recibirá basura recolectada mientras espera?
Tal vez. El recolector de basura puede hacerlo, pero no es obligatorio.
¿La memoria estará ocupada durante la duración de la función?
Tal vez. El recolector de basura puede hacerlo, pero no es obligatorio.
Básicamente, si el recolector de basura puede saber que nunca se volverá a tocar el búfer, puede liberarlo en cualquier momento. Pero el GC nunca está obligado a liberar nada en un horario particular.
Si está particularmente preocupado, siempre puede establecer el local en null
, pero no me molestaría en hacerlo a menos que haya demostrado un problema. Alternativamente, puede extraer el código que manipula el búfer en su propio método no asincrónico y llamarlo de forma síncrona desde el método async; entonces el local se convierte en un local ordinario de un método ordinario.
La
await
se realiza como unreturn
, por lo que el local saldrá del alcance y su tiempo de vida habrá terminado; la matriz se recopilará en la siguiente colección, que debe estar durante elDelay
, ¿verdad?
No, ninguna de esas afirmaciones es verdadera.
Primero, await
es solo un return
si la tarea no se completa; ahora, por supuesto, es casi imposible que se complete el Delay
, así que sí, esto volverá, pero no podemos concluir, en general, que una await
regresa a la persona que llama.
En segundo lugar, lo local solo desaparece si realmente se realiza en IL por el compilador de C # como local en el grupo temporal. El jitter lo hará como una ranura o registro de pila, que se desvanece cuando la activación del método finaliza a la await
. ¡Pero el compilador de C # no está obligado a hacer eso!
Le parecerá extraño a una persona en el depurador poner un punto de interrupción después del Delay
y ver que el local se ha desvanecido, por lo que el compilador podría darse cuenta de que el local es un campo en una clase generada por el compilador que está vinculada a la duración de la clase generado para la máquina de estado. En ese caso, es mucho menos probable que el jitter se dé cuenta de que este campo nunca se vuelve a leer y, por lo tanto, es mucho menos probable que lo descarte pronto. (Aunque está permitido hacerlo. Y también se permite que el compilador de C # configure el campo como nulo en su nombre si puede demostrar que ha terminado de usarlo. De nuevo, esto sería extraño para la persona en el depurador que de repente ve su valor de cambio local sin ninguna razón aparente, pero el compilador puede generar cualquier código cuyo comportamiento de subproceso sea correcto).
En tercer lugar, nada requiere que el recolector de basura recolecte nada en un cronograma particular. Esta matriz grande se asignará en el montón de objetos grandes, y esa cosa tiene su propio calendario de recolección.
En cuarto lugar, nada en absoluto requiere que haya una colección del gran montón de objetos en un intervalo dado de sesenta segundos. Eso no necesita ser recogido si no hay memoria de presión.
Estoy bastante seguro de que se ha recopilado, ya que aguarda que finalice su tarea actual y "continúa con" otra tarea, por lo que las variables locales deben limpiarse cuando no se utilizan después de la espera.
PERO: Lo que el compilador realmente podría ser algo diferente, entonces no dependería de tal comportamiento.
Hay una actualización sobre este tema con el compilador Rolsyn.
Ejecutar el siguiente código en Visual Studio 2015 Actualización 3 en la configuración de lanzamiento produce
True
False
Entonces los lugareños son basura recolectada.
private static async Task MethodAsync()
{
byte[] bytes = new byte[1024];
var wr = new WeakReference(bytes);
Console.WriteLine(wr.Target != null);
await Task.Delay(100);
FullGC();
Console.WriteLine(wr.Target != null);
await Task.Delay(100);
}
private static void FullGC()
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
Tenga en cuenta que si modificamos MethodAsync para usar la variable local después de esperar, el buffer de array no será recolectado.
private static async Task MethodAsync()
{
byte[] bytes = new byte[1024];
var wr = new WeakReference(bytes);
Console.WriteLine(wr.Target != null);
await Task.Delay(100);
Console.WriteLine(bytes.Length);
FullGC();
Console.WriteLine(wr.Target != null);
await Task.Delay(100);
FullGC();
Console.WriteLine(wr.Target != null);
}
El resultado para esto es
True
1024
True
True
Los ejemplos de código están tomados de este problema de rolsyn.
Lo que dijo Eric Lippert es cierto: el compilador de C # tiene bastante libertad de acción sobre qué IL debería generar para el método async
. Entonces, si se pregunta qué dice la especificación sobre esto, entonces la respuesta es: la matriz puede ser elegible para la recolección durante la espera, lo que significa que puede ser recolectada.
Pero otra pregunta es qué hace realmente el compilador. En mi computadora, el compilador genera Buffer
como un campo de tipo de máquina de estado generado. Ese campo se establece en la matriz asignada, y luego nunca se establece de nuevo. Eso significa que la matriz será elegible para la recopilación cuando lo haga el objeto de máquina de estado. Y ese objeto se referencia desde el delegado de continuación, por lo que no será elegible para la recopilación hasta que finalice la espera. Lo que esto significa es que la matriz no será elegible para la recopilación durante la espera, lo que significa que no se recopilará.
Algunas notas más:
- El objeto de máquina de estado es en realidad una
struct
, pero se usa a través de una interfaz que implementa, por lo que se comporta como un tipo de referencia para la recolección de basura. - Si realmente determina que el hecho de que la matriz no se recopile es un problema para usted, podría valer la pena establecer el local en
null
antes de laawait
. Pero en la gran mayoría de los casos, no tiene que preocuparse por esto. Ciertamente, no digo que deba establecer regularmente que los localesnull
antes deawait
. - Esto es en gran medida un detalle de implementación. Puede cambiar en cualquier momento y las diferentes versiones del compilador pueden comportarse de manera diferente.
Su código se compila (en mi entorno: VS2012, C # 5, .NET 4.5, modo de lanzamiento) para incluir una estructura que implementa IAsyncStateMachine
, y tiene el siguiente campo:
public byte[] <Buffer>5__1;
Por lo tanto, a menos que el JIT y / o el GC sean realmente inteligentes, (ver la respuesta de Eric Lippert para más información al respecto) sería razonable suponer que el byte[]
grande byte[]
permanecerá en el alcance hasta que se complete la tarea asincrónica.