stringtranslate.com

Patrón de grupo de objetos

El patrón de pool de objetos es un patrón de diseño de creación de software que utiliza un conjunto de objetos inicializados que se mantienen listos para usar (un " pool ") en lugar de asignarlos y destruirlos a pedido. Un cliente del pool solicitará un objeto del pool y realizará operaciones en el objeto devuelto. Cuando el cliente haya terminado, devuelve el objeto al pool en lugar de destruirlo ; esto se puede hacer de forma manual o automática.

Los grupos de objetos se utilizan principalmente para mejorar el rendimiento: en algunas circunstancias, mejoran significativamente el rendimiento. Los grupos de objetos complican la vida útil de los objetos , ya que los objetos obtenidos de un grupo y devueltos a él no se crean ni se destruyen en ese momento y, por lo tanto, requieren cuidado en la implementación.

Descripción

Cuando es necesario trabajar con numerosos objetos cuya instanciación es especialmente costosa y cada objeto solo se necesita durante un breve período de tiempo, el rendimiento de toda la aplicación puede verse afectado negativamente. En casos como estos, puede resultar conveniente utilizar un patrón de diseño de grupo de objetos.

El patrón de diseño de grupo de objetos crea un conjunto de objetos que pueden reutilizarse. Cuando se necesita un nuevo objeto, se lo solicita al grupo. Si hay un objeto preparado previamente disponible, se lo devuelve inmediatamente, lo que evita el costo de instanciación. Si no hay objetos presentes en el grupo, se crea y devuelve un nuevo elemento. Cuando el objeto se ha utilizado y ya no se necesita, se lo devuelve al grupo, lo que permite usarlo nuevamente en el futuro sin repetir el costoso proceso de instanciación computacional. Es importante tener en cuenta que una vez que se ha utilizado y devuelto un objeto, las referencias existentes dejarán de ser válidas.

En algunos grupos de objetos, los recursos son limitados, por lo que se especifica una cantidad máxima de objetos. Si se alcanza esta cantidad y se solicita un nuevo elemento, es posible que se genere una excepción o que el subproceso se bloquee hasta que se libere un objeto nuevamente en el grupo.

El patrón de diseño de grupo de objetos se utiliza en varios lugares de las clases estándar de .NET Framework. Un ejemplo es el proveedor de datos de .NET Framework para SQL Server. Como las conexiones de base de datos de SQL Server pueden ser lentas de crear, se mantiene un grupo de conexiones. Cerrar una conexión no significa realmente renunciar al vínculo con SQL Server. En cambio, la conexión se mantiene en un grupo, desde el cual se puede recuperar al solicitar una nueva conexión. Esto aumenta sustancialmente la velocidad de realización de las conexiones.

Beneficios

La agrupación de objetos puede ofrecer un aumento significativo del rendimiento en situaciones en las que el costo de inicializar una instancia de clase es alto y la tasa de creación de instancias y destrucción de una clase es alta; en este caso, los objetos se pueden reutilizar con frecuencia y cada reutilización ahorra una cantidad significativa de tiempo. La agrupación de objetos requiere recursos (memoria y posiblemente otros recursos, como sockets de red) y, por lo tanto, es preferible que la cantidad de instancias en uso en un momento dado sea baja, pero esto no es obligatorio.

El objeto agrupado se obtiene en un tiempo predecible, mientras que la creación de nuevos objetos (especialmente a través de la red) puede llevar un tiempo variable. Estos beneficios son principalmente válidos para objetos que requieren mucho tiempo, como conexiones de bases de datos, conexiones de sockets, subprocesos y objetos gráficos grandes, como fuentes o mapas de bits.

En otras situaciones, la agrupación simple de objetos (que no contienen recursos externos, sino que solo ocupan memoria) puede no ser eficiente y podría reducir el rendimiento. [1] En el caso de la agrupación simple de memoria, la técnica de gestión de memoria de asignación de bloques es más adecuada, ya que el único objetivo es minimizar el costo de la asignación y desasignación de memoria al reducir la fragmentación.

Implementación

Los grupos de objetos se pueden implementar de manera automatizada en lenguajes como C++ a través de punteros inteligentes . En el constructor del puntero inteligente, se puede solicitar un objeto del grupo, y en el destructor del puntero inteligente, el objeto se puede devolver al grupo. En lenguajes con recolección de basura, donde no hay destructores (que se garantiza que se invoquen como parte de un desenrollado de pila), los grupos de objetos se deben implementar manualmente, solicitando explícitamente un objeto de la fábrica y devolviendo el objeto llamando a un método dispose (como en el patrón dispose ). Usar un finalizador para hacer esto no es una buena idea, ya que generalmente no hay garantías sobre cuándo (o si) se ejecutará el finalizador. En cambio, se debe usar "try ... finally" para garantizar que la obtención y liberación del objeto sea neutral ante excepciones.

Los grupos de objetos manuales son fáciles de implementar, pero más difíciles de usar, ya que requieren una gestión manual de la memoria de los objetos del grupo.

Manejo de piscinas vacías

Los grupos de objetos emplean una de tres estrategias para manejar una solicitud cuando no hay objetos adicionales en el grupo.

  1. No se puede proporcionar un objeto (y se devuelve un error al cliente).
  2. Asignar un nuevo objeto, aumentando así el tamaño del pool. Los pools que hacen esto suelen permitir establecer el límite superior (la cantidad máxima de objetos utilizados).
  3. En un entorno multiproceso , un grupo puede bloquear al cliente hasta que otro hilo devuelva un objeto al grupo.

Trampas

Se debe tener cuidado para garantizar que el estado de los objetos devueltos al grupo se restablezca a un estado razonable para el próximo uso del objeto; de lo contrario, el objeto puede estar en un estado inesperado para el cliente, lo que puede provocar que falle. El grupo es responsable de restablecer los objetos, no los clientes. Los grupos de objetos llenos de objetos con un estado peligrosamente obsoleto a veces se denominan pozos negros de objetos y se consideran un antipatrón .

El estado obsoleto no siempre es un problema; se vuelve peligroso cuando hace que el objeto se comporte de manera inesperada. Por ejemplo, un objeto que representa detalles de autenticación puede fallar si el indicador de "autenticación exitosa" no se restablece antes de volver a usarse, ya que indica que un usuario está autenticado (posiblemente como otra persona) cuando no es así. Sin embargo, no restablecer un valor utilizado solo para depuración, como la identidad del último servidor de autenticación utilizado, puede no plantear problemas.

El restablecimiento inadecuado de los objetos puede provocar fugas de información. Los objetos que contienen datos confidenciales (por ejemplo, los números de tarjetas de crédito de un usuario) deben borrarse antes de pasarlos a nuevos clientes; de lo contrario, los datos pueden revelarse a terceros no autorizados.

Si el grupo lo utilizan varios subprocesos, es posible que se necesiten medios para evitar que los subprocesos paralelos intenten reutilizar el mismo objeto en paralelo. Esto no es necesario si los objetos agrupados son inmutables o seguros para subprocesos.

Crítica

Algunas publicaciones no recomiendan el uso de agrupación de objetos con ciertos lenguajes, como Java , especialmente para objetos que solo usan memoria y no contienen recursos externos (como conexiones a bases de datos). Los oponentes suelen decir que la asignación de objetos es relativamente rápida en lenguajes modernos con recolectores de basura ; mientras que el operador newnecesita solo diez instrucciones, el par clásico newque deletese encuentra en los diseños de agrupación requiere cientos de ellas ya que realiza un trabajo más complejo. Además, la mayoría de los recolectores de basura escanean referencias de objetos "vivos", y no la memoria que estos objetos usan para su contenido. Esto significa que cualquier cantidad de objetos "muertos" sin referencias se puede descartar con poco costo. Por el contrario, mantener una gran cantidad de objetos "vivos" pero sin usar aumenta la duración de la recolección de basura. [1]

Ejemplos

Ir

El siguiente código Go inicializa un grupo de recursos de un tamaño específico (inicialización concurrente) para evitar problemas de carrera de recursos a través de los canales y, en el caso de un grupo vacío, establece el procesamiento de tiempo de espera para evitar que los clientes esperen demasiado.

// grupo de paquetes grupo de paquetes importar ( "errores" "registro" "matemática/rand" "sincronización" "tiempo" ) const getResMaxTime = 3 * tiempo . Segundo     var ( ErrPoolNotExist = errors . New ( " el grupo no existe" ) ErrGetResTimeout = errors . New ( "obtener tiempo de espera del recurso" )     // Tipo de recurso Recurso struct { resId int }    //NewResource Simular la creación lenta de inicialización de recursos // (por ejemplo, la conexión TCP, la adquisición de clave simétrica SSL y la autenticación consumen mucho tiempo) func NewResource ( id int ) * Resource { time . Sleep ( 500 * time . Millisecond ) return & Resource { resId : id } }        // Los recursos de simulación consumen mucho tiempo y el consumo aleatorio es de 0 a 400 ms func ( r * Resource ) Do ( workId int ) { time.Sleep ( time.Duration ( rand.Intn ( 5 ) ) * 100 * time.Milisecond ) log.Printf ( " usando recurso # % d trabajo finalizado %d terminado \ n " , r.resId , workId ) }           //Pool basado en la implementación del canal Go, para evitar problemas de estado de carrera de recursos tipo Pool chan * Resource   // Nuevo grupo de recursos del tamaño especificado // Los recursos se crean simultáneamente para ahorrar tiempo de inicialización de recursos func New ( size int ) Pool { p := make ( Pool , size ) wg := new ( sync.WaitGroup ) wg.Add ( size ) for i : = 0 ; i < size ; i ++ { go func ( resId int ) { p < - NewResource ( resId ) wg.Done ( ) } ( i ) } wg.Wait ( ) return p }                       //GetResource según el canal, se evita el estado de carrera de recursos y se establece el tiempo de espera de adquisición de recursos para el grupo vacío func ( p Pool ) GetResource ( ) ( r * Resource , errerror ) { select { caser : = < - p : returnr , nilcase < -time.After ( getResMaxTime ) : returnnil , ErrGetResTimeout } }                 //GiveBackResource devuelve recursos al grupo de recursos func ( p Pool ) GiveBackResource ( r * Resource ) error { if p == nil { return ErrPoolNotExist } p <- r return nil }              // paquete principal paquete principal importar ( "github.com/tkstorm/go-design/creational/object-pool/pool" "log" "sync" ) func main () { // Inicializa un pool de cinco recursos, // que se puede ajustar a 1 o 10 para ver la diferencia size := 5 p := pool . New ( size )      // Invoca un recurso para realizar el trabajo id doWork := func ( workId int , wg * sync.WaitGroup ) { defer wg.Done () // Obtiene el recurso del grupo de recursos res , err : = p.GetResource () if err ! = nil { log.Println ( err ) return } // Recursos a devolver defer p.GiveBackResource ( res ) // Utiliza recursos para manejar el trabajo res.Do ( workId ) }               // Simular 100 procesos simultáneos para obtener recursos del grupo de activos num := 100 wg := new ( sync . WaitGroup ) wg . Add ( num ) for i := 0 ; i < num ; i ++ { go doWork ( i , wg ) } wg . Wait () }              

DO#

En la biblioteca de clases base de .NET hay algunos objetos que implementan este patrón. System.Threading.ThreadPoolestá configurado para tener una cantidad predefinida de subprocesos para asignar. Cuando se devuelven los subprocesos, están disponibles para otro cálculo. Por lo tanto, se pueden usar subprocesos sin pagar el costo de creación y eliminación de subprocesos.

A continuación se muestra el código básico del patrón de diseño de grupo de objetos implementado con C#. Para abreviar, las propiedades de las clases se declaran utilizando la sintaxis de propiedades implementada automáticamente en C# 3.0. Estas se pueden reemplazar con definiciones de propiedades completas para versiones anteriores del lenguaje. El grupo se muestra como una clase estática, ya que no es habitual que se requieran varios grupos. Sin embargo, es igualmente aceptable utilizar clases de instancia para los grupos de objetos.

espacio de nombres DesignPattern.Objectpool ; // La clase PooledObject es el tipo que es costoso o lento de instanciar, // o que tiene disponibilidad limitada, por lo que se debe mantener en el grupo de objetos. public class PooledObject { private DateTime _createdAt = DateTime . Now ;        público DateTime CreatedAt => _createdAt ;     cadena pública TempData { obtener ; establecer ; } }      // La clase Pool controla el acceso a los objetos agrupados. Mantiene una lista de objetos disponibles y una // colección de objetos que se han obtenido del pool y están en uso. El pool garantiza que los objetos liberados // se devuelvan a un estado adecuado, listos para su reutilización. public static class Pool { private static List < PooledObject > _available = new List < PooledObject > (); private static List < PooledObject > _inUse = new List < PooledObject > ();                  público estático PooledObject GetObject () { lock ( _available ) { if ( _available . Count != 0 ) { PooledObject po = _available [ 0 ]; _inUse . Add ( po ); _available . RemoveAt ( 0 ); return po ; } else { PooledObject po = new PooledObject (); _inUse . Add ( po ); return po ; } } }                                   público estático void ReleaseObject ( PooledObject po ) { CleanUp ( po );       bloquear ( _disponible ) { _disponible . Agregar ( po ); _enUso . Eliminar ( po ); } }       void estático privado CleanUp ( PooledObject po ) { po.TempData = null ; } }         

En el código anterior, PooledObject tiene propiedades para el momento en que se creó y otra, que puede ser modificada por el cliente, que se restablece cuando PooledObject se libera nuevamente en el pool. Se muestra el proceso de limpieza, al liberar un objeto, para garantizar que esté en un estado válido antes de que se pueda solicitar nuevamente desde el pool.

Java

Java admite la agrupación de subprocesos a través de java.util.concurrent.ExecutorServicey otras clases relacionadas. El servicio de ejecución tiene una cierta cantidad de subprocesos "básicos" que nunca se descartan. Si todos los subprocesos están ocupados, el servicio asigna la cantidad permitida de subprocesos adicionales que luego se descartan si no se utilizan durante el tiempo de expiración determinado. Si no se permiten más subprocesos, las tareas se pueden colocar en la cola. Finalmente, si esta cola puede llegar a ser demasiado larga, se puede configurar para suspender el subproceso solicitante.

clase pública PooledObject { pública String temp1 ; pública String temp2 ; pública String temp3 ; pública String getTemp1 () { devolver temp1 ; } pública void setTemp1 ( String temp1 ) { this . temp1 = temp1 ; } pública String getTemp2 () { devolver temp2 ; } pública void setTemp2 ( String temp2 ) { this . temp2 = temp2 ; } pública String getTemp3 () { devolver temp3 ; } pública void setTemp3 ( String temp3 ) { this . temp3 = temp3 ; } }                                       
clase pública PooledObjectPool { privada estática larga expTime = 6000 ; //6 segundos pública estática HashMap < PooledObject , Long > disponible = nuevo HashMap < PooledObject , Long > (); pública estática HashMap < PooledObject , Long > enUso = nuevo HashMap < PooledObject , Long > (); pública sincronizada estática PooledObject getObject () { larga ahora = System . currentTimeMillis (); si ( ! disponible . isEmpty ()) { para ( Map . Entry < PooledObject , Long > entrada : disponible . entrySet ()) { si ( ahora - entrada . getValue () > expTime ) { //el objeto ha expirado popElement ( disponible ); } de lo contrario { PooledObject po = popElement ( disponible , entrada . getKey ()); push ( enUso , po , ahora ); devolver po ; } } }                                                         // o no hay ningún PooledObject disponible o cada uno ha expirado, por lo que se devuelve uno nuevo return createPooledObject ( now ); } privatesynchronous static PooledObject createPooledObject ( long now ) { PooledObject po = new PooledObject (); push ( inUse , po , now ); return po ; }               privado sincronizado estático void push ( HashMap < PooledObject , Long > map , PooledObject po , long now ) { map . put ( po , now ); }           public static void releaseObject ( PooledObject po ) { cleanUp ( po ); available.put ( po , System.currentTimeMillis ( )); inUse.remove ( po ) ; } private static PooledObject popElement ( HashMap < PooledObject , Long > map ) { Map.Entry < PooledObject , Long > entry = map.entrySet ( ) . iterator ( ) . next ( ) ; PooledObject key = entry.getKey ( ) ; // Long value = entry.getValue ( ) ; map.remove ( entrada.getKey ( ) ) ; return key ; } private static PooledObject popElement ( HashMap < PooledObject , Long > map , PooledObject key ) { map.remove ( key ) ; return key ; } public static void cleanUp ( PooledObject po ) { po.setTemp1 ( null ) ; po.setTemp2 ( null ) ; po.setTemp3 ( null ) ; } }                                      

Véase también

Notas

  1. ^ ab Goetz, Brian (27 de septiembre de 2005). "Teoría y práctica de Java: leyendas del rendimiento urbano, revisitadas". IBM . IBM developerWorks. Archivado desde el original el 14 de febrero de 2012 . Consultado el 15 de marzo de 2021 .

Referencias

Enlaces externos