Modèle de pool d'objets - Object pool pattern

Le modèle de pool d'objets est un modèle de conception de création de logiciel qui utilise un ensemble d' objets initialisés prêts à être utilisés - un " pool " - plutôt que de les allouer et de les détruire à la demande. Un client du pool demandera un objet au pool et effectuera des opérations sur l'objet renvoyé. Lorsque le client a terminé, il renvoie l'objet au pool plutôt que de le détruire ; cela peut être fait manuellement ou automatiquement.

Les pools d'objets sont principalement utilisés pour les performances : dans certaines circonstances, les pools d'objets améliorent considérablement les performances. Les pools d'objets compliquent la durée de vie des objets , car les objets obtenus à partir d'un pool et renvoyés vers un pool ne sont pas réellement créés ou détruits à ce stade, et nécessitent donc une mise en œuvre prudente.

La description

Lorsqu'il est nécessaire de travailler avec de nombreux objets particulièrement coûteux à instancier et que chaque objet n'est nécessaire que pendant une courte période de temps, les performances d'une application entière peuvent être affectées négativement. Un modèle de conception de pool d'objets peut être jugé souhaitable dans de tels cas.

Le modèle de conception de pool d'objets crée un ensemble d'objets qui peuvent être réutilisés. Lorsqu'un nouvel objet est nécessaire, il est demandé au pool. Si un objet préalablement préparé est disponible, il est renvoyé immédiatement, évitant le coût d'instanciation. Si aucun objet n'est présent dans le pool, un nouvel élément est créé et renvoyé. Lorsque l'objet a été utilisé et n'est plus nécessaire, il est renvoyé au pool, ce qui lui permet d'être réutilisé à l'avenir sans répéter le processus d'instanciation coûteux en calcul. Il est important de noter qu'une fois qu'un objet a été utilisé et renvoyé, les références existantes deviendront invalides.

Dans certains pools d'objets, les ressources sont limitées, donc un nombre maximum d'objets est spécifié. Si ce nombre est atteint et qu'un nouvel élément est demandé, une exception peut être levée, ou le thread sera bloqué jusqu'à ce qu'un objet soit libéré dans le pool.

Le modèle de conception de pool d'objets est utilisé à plusieurs endroits dans les classes standard du .NET Framework. Un exemple est le fournisseur de données .NET Framework pour SQL Server. Comme les connexions à la base de données SQL Server peuvent être lentes à créer, un pool de connexions est maintenu. La fermeture d'une connexion ne renonce pas réellement au lien vers SQL Server. Au lieu de cela, la connexion est conservée dans un pool, à partir duquel elle peut être récupérée lors de la demande d'une nouvelle connexion. Cela augmente considérablement la vitesse d'établissement des connexions.

Avantages

Le regroupement d'objets peut offrir une amélioration significative des performances dans les situations où le coût d'initialisation d'une instance de classe est élevé et le taux d'instanciation et de destruction d'une classe est élevé - dans ce cas, les objets peuvent fréquemment être réutilisés, et chaque réutilisation permet d'économiser une quantité importante de temps. La mise en commun d'objets nécessite des ressources - de la mémoire et éventuellement d'autres ressources, telles que des sockets réseau, et il est donc préférable que le nombre d'instances utilisées à un moment donné soit faible, mais ce n'est pas obligatoire.

L'objet mis en commun est obtenu en un temps prévisible lorsque la création des nouveaux objets (surtout sur le réseau) peut prendre un temps variable. Ces avantages sont principalement vrais pour les objets coûteux en temps, tels que les connexions de base de données, les connexions de socket, les threads et les objets graphiques volumineux tels que les polices ou les bitmaps.

Dans d'autres situations, la mise en commun d'objets simples (qui ne contiennent aucune ressource externe, mais n'occupent que de la mémoire) peut ne pas être efficace et réduire les performances. Dans le cas d'un simple pooling mémoire, la technique de gestion d' allocation mémoire par dalle est plus adaptée, car le seul but est de minimiser le coût d'allocation et de désallocation mémoire en réduisant la fragmentation.

Mise en œuvre

Les pools d'objets peuvent être implémentés de manière automatisée dans des langages comme C++ via des pointeurs intelligents . Dans le constructeur du pointeur intelligent, un objet peut être demandé au pool, et dans le destructeur du pointeur intelligent, l'objet peut être renvoyé dans le pool. Dans les langages ramassés, où il n'y a pas de destructeurs (qui sont garantis pour être appelés dans le cadre d'un déroulement de pile), les pools d'objets doivent être implémentés manuellement, en demandant explicitement un objet à la fabrique et en retournant l'objet en appelant une méthode dispose (comme dans le modèle de disposition ). L'utilisation d'un finaliseur pour ce faire n'est pas une bonne idée, car il n'y a généralement aucune garantie de quand (ou si) le finaliseur sera exécuté. Au lieu de cela, "essayer ... finalement" doit être utilisé pour s'assurer que l'obtention et la libération de l'objet sont neutres vis-à-vis des exceptions.

Les pools d'objets manuels sont simples à mettre en œuvre, mais plus difficiles à utiliser, car ils nécessitent une gestion manuelle de la mémoire des objets de pool.

Gestion des piscines vides

Les pools d'objets utilisent l'une des trois stratégies pour traiter une demande lorsqu'il n'y a pas d'objets de rechange dans le pool.

  1. Échec de la fourniture d'un objet (et renvoie une erreur au client).
  2. Allouez un nouvel objet, augmentant ainsi la taille du pool. Les piscines qui font cela vous permettent généralement de définir la limite des hautes eaux (le nombre maximum d'objets jamais utilisés).
  3. Dans un environnement multithread , un pool peut bloquer le client jusqu'à ce qu'un autre thread renvoie un objet au pool.

Pièges

Des précautions doivent être prises pour s'assurer que l'état des objets renvoyés au pool est réinitialisé à un état raisonnable pour la prochaine utilisation de l'objet, sinon l'objet peut être dans un état inattendu par le client, ce qui peut provoquer son échec. Le pool est responsable de la réinitialisation des objets, pas des clients. Les pools d'objets remplis d'objets avec un état dangereusement périmé sont parfois appelés puisards d'objets et considérés comme un anti-modèle .

L'état périmé peut ne pas toujours être un problème ; il devient dangereux lorsqu'il provoque un comportement inattendu de l'objet. Par exemple, un objet représentant les détails d'authentification peut échouer si l'indicateur « authentifié avec succès » n'est pas réinitialisé avant d'être réutilisé, car il indique qu'un utilisateur est authentifié (éventuellement en tant que quelqu'un d'autre) alors qu'il ne l'est pas. Cependant, ne pas réinitialiser une valeur utilisée uniquement pour le débogage, telle que l'identité du dernier serveur d'authentification utilisé, peut ne poser aucun problème.

Une réinitialisation inadéquate des objets peut provoquer des fuites d'informations. Les objets contenant des données confidentielles (par exemple les numéros de carte de crédit d'un utilisateur) doivent être effacés avant d'être transmis à de nouveaux clients, sinon les données peuvent être divulguées à une partie non autorisée.

Si le pool est utilisé par plusieurs threads, il peut avoir besoin de moyens pour empêcher les threads parallèles d'essayer de réutiliser le même objet en parallèle. Cela n'est pas nécessaire si les objets regroupés sont immuables ou sinon thread-safe.

Critique

Certaines publications ne recommandent pas l'utilisation du pool d'objets avec certains langages, tels que Java , en particulier pour les objets qui n'utilisent que de la mémoire et ne contiennent aucune ressource externe (telles que les connexions à la base de données). Les opposants disent généralement que l'allocation d'objets est relativement rapide dans les langages modernes avec les ramasse-miettes ; alors que l'opérateur newn'a besoin que de dix instructions, la paire classique new- deletetrouvée dans les conceptions de mise en commun en nécessite des centaines car elle effectue un travail plus complexe. De plus, la plupart des récupérateurs de mémoire analysent les références d'objets "en direct", et non la mémoire que ces objets utilisent pour leur contenu. Cela signifie que n'importe quel nombre d'objets "morts" sans références peuvent être éliminés à peu de frais. En revanche, le fait de conserver un grand nombre d'objets « vivants » mais inutilisés augmente la durée du ramasse-miettes.

Exemples

Aller

Le code Go suivant initialise un pool de ressources d'une taille spécifiée (initialisation simultanée) pour éviter les problèmes de course aux ressources via les canaux et, dans le cas d'un pool vide, définit le traitement du délai d'attente pour empêcher les clients d'attendre trop longtemps.

// package pool
package pool

import (
	"errors"
	"log"
	"math/rand"
	"sync"
	"time"
)

const getResMaxTime = 3 * time.Second

var (
	ErrPoolNotExist  = errors.New("pool not exist")
	ErrGetResTimeout = errors.New("get resource time out")
)

//Resource
type Resource struct {
	resId int
}

//NewResource Simulate slow resource initialization creation
// (e.g., TCP connection, SSL symmetric key acquisition, auth authentication are time-consuming)
func NewResource(id int) *Resource {
	time.Sleep(500 * time.Millisecond)
	return &Resource{resId: id}
}

//Do Simulation resources are time consuming and random consumption is 0~400ms
func (r *Resource) Do(workId int) {
	time.Sleep(time.Duration(rand.Intn(5)) * 100 * time.Millisecond)
	log.Printf("using resource #%d finished work %d finish\n", r.resId, workId)
}

//Pool based on Go channel implementation, to avoid resource race state problem
type Pool chan *Resource

//New a resource pool of the specified size
// Resources are created concurrently to save resource initialization time
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 based on channel, resource race state is avoided and resource acquisition timeout is set for empty pool
func (p Pool) GetResource() (r *Resource, err error) {
	select {
	case r := <-p:
		return r, nil
	case <-time.After(getResMaxTime):
		return nil, ErrGetResTimeout
	}
}

//GiveBackResource returns resources to the resource pool
func (p Pool) GiveBackResource(r *Resource) error {
	if p == nil {
		return ErrPoolNotExist
	}
	p <- r
	return nil
}

// package main
package main

import (
	"github.com/tkstorm/go-design/creational/object-pool/pool"
	"log"
	"sync"
)

func main() {
	// Initialize a pool of five resources,
	// which can be adjusted to 1 or 10 to see the difference
	size := 5
	p := pool.New(size)

	// Invokes a resource to do the id job
	doWork := func(workId int, wg *sync.WaitGroup) {
		defer wg.Done()
		// Get the resource from the resource pool
		res, err := p.GetResource()
		if err != nil {
			log.Println(err)
			return
		}
		// Resources to return
		defer p.GiveBackResource(res)
		// Use resources to handle work
		res.Do(workId)
	}

	// Simulate 100 concurrent processes to get resources from the asset pool
	num := 100
	wg := new(sync.WaitGroup)
	wg.Add(num)
	for i := 0; i < num; i++ {
		go doWork(i, wg)
	}
	wg.Wait()
}

C#

Dans la bibliothèque de classes de base .NET , quelques objets implémentent ce modèle. System.Threading.ThreadPoolest configuré pour avoir un nombre prédéfini de threads à allouer. Lorsque les threads sont renvoyés, ils sont disponibles pour un autre calcul. Ainsi, on peut utiliser des threads sans payer le coût de création et d'élimination des threads.

Ce qui suit montre le code de base du modèle de conception de pool d'objets implémenté à l'aide de C#. Par souci de concision, les propriétés des classes sont déclarées à l'aide de la syntaxe de propriété implémentée automatiquement en C# 3.0. Celles-ci pourraient être remplacées par des définitions de propriétés complètes pour les versions antérieures du langage. Pool est affiché comme une classe statique, car il est inhabituel que plusieurs pools soient requis. Cependant, il est tout aussi acceptable d'utiliser des classes d'instances pour les pools d'objets.

namespace DesignPattern.Objectpool 
{
    // The PooledObject class is the type that is expensive or slow to instantiate,
    // or that has limited availability, so is to be held in the object pool.
    public class PooledObject
    {
        private DateTime _createdAt = DateTime.Now;
 
        public DateTime CreatedAt
        {
            get { return _createdAt; }
        }
 
        public string TempData { get; set; }
    }

    // The Pool class controls access to the pooled objects. It maintains a list of available objects and a 
    // collection of objects that have been obtained from the pool and are in use. The pool ensures that released objects 
    // are returned to a suitable state, ready for reuse. 
    public static class Pool
    {
        private static List<PooledObject> _available = new List<PooledObject>();
        private static List<PooledObject> _inUse = new List<PooledObject>();
 
        public static 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;
                }
            }
        }
 
        public static void ReleaseObject(PooledObject po)
        {
            CleanUp(po);
 
            lock (_available)
            {
                _available.Add(po);
                _inUse.Remove(po);
            }
        }
 
        private static void CleanUp(PooledObject po)
        {
            po.TempData = null;
        }
    }
}

Dans le code ci-dessus, le PooledObject a des propriétés pour l'heure à laquelle il a été créé, et une autre, qui peut être modifiée par le client, qui est réinitialisée lorsque le PooledObject est remis dans le pool. Le processus de nettoyage, lors de la libération d'un objet, est illustré, garantissant qu'il est dans un état valide avant de pouvoir être à nouveau demandé à partir du pool.

Java

Java prend en charge le regroupement de threads via java.util.concurrent.ExecutorServiceet d'autres classes associées. Le service exécuteur dispose d'un certain nombre de threads "de base" qui ne sont jamais ignorés. Si tous les threads sont occupés, le service alloue le nombre autorisé de threads supplémentaires qui sont ensuite supprimés s'ils ne sont pas utilisés pendant un certain délai d'expiration. Si plus aucun thread n'est autorisé, les tâches peuvent être placées dans la file d'attente. Enfin, si cette file d'attente peut devenir trop longue, elle peut être configurée pour suspendre le thread demandeur.

public class PooledObject {
	public String temp1;
	public String temp2;
	public String temp3;
	
	public String getTemp1() {
		return temp1;
	}
	public void setTemp1(String temp1) {
		this.temp1 = temp1;
	}
	public String getTemp2() {
		return temp2;
	}
	public void setTemp2(String temp2) {
		this.temp2 = temp2;
	}
	public String getTemp3() {
		return temp3;
	}
	public void setTemp3(String temp3) {
		this.temp3 = temp3;
	}
}
public class PooledObjectPool {
	private static long expTime = 6000;//6 seconds
	public static HashMap<PooledObject, Long> available = new HashMap<PooledObject, Long>();
	public static HashMap<PooledObject, Long> inUse = new HashMap<PooledObject, Long>();
	
	public synchronized static PooledObject getObject() {
		long now = System.currentTimeMillis();
		if (!available.isEmpty()) {
			for (Map.Entry<PooledObject, Long> entry : available.entrySet()) {
				if (now - entry.getValue() > expTime) { //object has expired
					popElement(available);
				} else {
					PooledObject po = popElement(available, entry.getKey());
					push(inUse, po, now); 
					return po;
				}
			}
		}

		// either no PooledObject is available or each has expired, so return a new one
		return createPooledObject(now);
	}	
	
	private synchronized static PooledObject createPooledObject(long now) {
		PooledObject po = new PooledObject();
		push(inUse, po, now);
		return po;
    }

	private synchronized static 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(entry.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);
	}
}

Voir également

Remarques

Les références

Liens externes