Modello pool di oggetti - Object pool pattern

Il modello del pool di oggetti è un modello di progettazione di creazione del software che utilizza un insieme di oggetti inizializzati tenuti pronti per l'uso - un " pool " - anziché allocarli e distruggerli su richiesta. Un client del pool richiederà un oggetto dal pool ed eseguirà operazioni sull'oggetto restituito. Quando il client ha terminato, restituisce l'oggetto al pool invece di distruggerlo ; questo può essere fatto manualmente o automaticamente.

I pool di oggetti vengono utilizzati principalmente per le prestazioni: in alcune circostanze, i pool di oggetti migliorano significativamente le prestazioni. I pool di oggetti complicano la durata degli oggetti , poiché gli oggetti ottenuti da e restituiti a un pool non vengono effettivamente creati o distrutti in questo momento e quindi richiedono attenzione nell'implementazione.

Descrizione

Quando è necessario lavorare con numerosi oggetti particolarmente costosi da istanziare e ciascun oggetto è necessario solo per un breve periodo di tempo, le prestazioni di un'intera applicazione potrebbero essere compromesse. Un modello di progettazione di un pool di oggetti può essere ritenuto desiderabile in casi come questi.

Il modello di progettazione del pool di oggetti crea un insieme di oggetti che possono essere riutilizzati. Quando è necessario un nuovo oggetto, viene richiesto dal pool. Se è disponibile un oggetto precedentemente preparato, viene restituito immediatamente, evitando il costo di istanziazione. Se non sono presenti oggetti nel pool, viene creato e restituito un nuovo elemento. Quando l'oggetto è stato utilizzato e non è più necessario, viene restituito al pool, consentendogli di essere riutilizzato in futuro senza ripetere il processo di istanziazione dispendioso dal punto di vista computazionale. È importante notare che una volta che un oggetto è stato utilizzato e restituito, i riferimenti esistenti non saranno più validi.

In alcuni pool di oggetti le risorse sono limitate, quindi viene specificato un numero massimo di oggetti. Se viene raggiunto questo numero e viene richiesto un nuovo elemento, potrebbe essere generata un'eccezione o il thread verrà bloccato fino a quando un oggetto non verrà rilasciato nuovamente nel pool.

Il modello di progettazione del pool di oggetti viene utilizzato in diversi punti nelle classi standard di .NET Framework. Un esempio è il provider di dati .NET Framework per SQL Server. Poiché le connessioni al database di SQL Server possono essere lente da creare, viene mantenuto un pool di connessioni. La chiusura di una connessione non annulla effettivamente il collegamento a SQL Server. La connessione viene invece conservata in un pool, dal quale può essere recuperata quando si richiede una nuova connessione. Ciò aumenta sostanzialmente la velocità di creazione delle connessioni.

Benefici

Il pool di oggetti può offrire un significativo aumento delle prestazioni in situazioni in cui il costo di inizializzazione di un'istanza di classe è elevato e il tasso di istanziazione e distruzione di una classe è elevato: in questo caso gli oggetti possono essere riutilizzati frequentemente e ogni riutilizzo consente di risparmiare una quantità significativa di tempo. Il pool di oggetti richiede risorse: memoria e possibilmente altre risorse, come i socket di rete, e quindi è preferibile che il numero di istanze in uso in qualsiasi momento sia basso, ma ciò non è necessario.

L'oggetto in pool viene ottenuto in un tempo prevedibile in cui la creazione dei nuovi oggetti (soprattutto sulla rete) può richiedere tempi variabili. Questi vantaggi sono per lo più veri per oggetti che sono costosi rispetto al tempo, come connessioni a database, connessioni socket, thread e oggetti grafici di grandi dimensioni come caratteri o bitmap.

In altre situazioni, il semplice pool di oggetti (che non contiene risorse esterne, ma occupa solo memoria) potrebbe non essere efficiente e ridurre le prestazioni. In caso di pool di memoria semplice, la tecnica di gestione della memoria con allocazione lastra è più adatta, poiché l'unico obiettivo è ridurre al minimo il costo di allocazione e deallocazione della memoria riducendo la frammentazione.

Implementazione

I pool di oggetti possono essere implementati in modo automatizzato in linguaggi come C++ tramite puntatori intelligenti . Nel costruttore del puntatore intelligente è possibile richiedere un oggetto dal pool e nel distruttore del puntatore intelligente l'oggetto può essere rilasciato nuovamente al pool. Nei linguaggi di raccolta dei rifiuti, dove non ci sono distruttori (che sono garantiti per essere chiamati come parte di uno stack unwind), i pool di oggetti devono essere implementati manualmente, richiedendo esplicitamente un oggetto dalla fabbrica e restituendo l'oggetto chiamando un metodo Dispose (come nel modello di smaltimento ). L'utilizzo di un finalizzatore per eseguire questa operazione non è una buona idea, poiché di solito non ci sono garanzie su quando (o se) verrà eseguito il finalizzatore. Invece, "prova...finalmente" dovrebbe essere usato per garantire che ottenere e rilasciare l'oggetto sia neutrale rispetto alle eccezioni.

I pool di oggetti manuali sono semplici da implementare, ma più difficili da utilizzare, poiché richiedono la gestione manuale della memoria degli oggetti del pool.

Movimentazione di piscine vuote

I pool di oggetti utilizzano una delle tre strategie per gestire una richiesta quando non sono presenti oggetti di riserva nel pool.

  1. Impossibile fornire un oggetto (e restituire un errore al client).
  2. Assegna un nuovo oggetto, aumentando così la dimensione del pool. Le piscine che fanno questo di solito consentono di impostare il marchio di acqua alta (il numero massimo di oggetti mai usato).
  3. In un ambiente multithread , un pool può bloccare il client finché un altro thread non restituisce un oggetto al pool.

insidie

È necessario prestare attenzione per garantire che lo stato degli oggetti restituiti al pool venga ripristinato a uno stato ragionevole per il successivo utilizzo dell'oggetto, altrimenti l'oggetto potrebbe trovarsi in uno stato imprevisto dal client, il che potrebbe causare un errore. Il pool è responsabile del ripristino degli oggetti, non dei client. I pool di oggetti pieni di oggetti con uno stato pericolosamente obsoleto sono talvolta chiamati pozzi neri di oggetti e considerati anti-pattern .

Lo stato stantio potrebbe non essere sempre un problema; diventa pericoloso quando provoca un comportamento imprevisto dell'oggetto. Ad esempio, un oggetto che rappresenta i dettagli di autenticazione potrebbe non riuscire se il flag "autenticato con successo" non viene reimpostato prima di essere riutilizzato, poiché indica che un utente è autenticato (possibilmente come qualcun altro) quando non lo sono. Tuttavia, la mancata reimpostazione di un valore utilizzato solo per il debug, ad esempio l'identità dell'ultimo server di autenticazione utilizzato, potrebbe non creare problemi.

Il ripristino inadeguato degli oggetti può causare perdite di informazioni. Gli oggetti contenenti dati riservati (es. numeri di carta di credito di un utente) devono essere cancellati prima di essere passati a nuovi clienti, altrimenti i dati potrebbero essere divulgati a una parte non autorizzata.

Se il pool viene utilizzato da più thread, potrebbe essere necessario il mezzo per impedire ai thread paralleli di tentare di riutilizzare lo stesso oggetto in parallelo. Questo non è necessario se gli oggetti in pool sono immutabili o comunque thread-safe.

Critica

Alcune pubblicazioni sconsigliano l'utilizzo del pool di oggetti con determinati linguaggi, come Java , in particolare per gli oggetti che utilizzano solo memoria e non contengono risorse esterne (come le connessioni al database). Gli oppositori di solito dicono che l'allocazione degli oggetti è relativamente veloce nei linguaggi moderni con i garbage collector ; mentre l'operatore ha newbisogno solo di dieci istruzioni, la classica coppia new- deletetrovata nei progetti di pooling ne richiede centinaia in quanto svolge un lavoro più complesso. Inoltre, la maggior parte dei Garbage Collector esegue la scansione dei riferimenti a oggetti "live" e non della memoria utilizzata da questi oggetti per il contenuto. Ciò significa che qualsiasi numero di oggetti "morti" senza riferimenti può essere scartato con poco costo. Al contrario, mantenere un numero elevato di oggetti "vivi" ma non utilizzati aumenta la durata della raccolta dei rifiuti.

Esempi

andare

Il codice Go seguente inizializza un pool di risorse di una dimensione specificata (inizializzazione simultanea) per evitare problemi di competizione delle risorse attraverso i canali e, nel caso di un pool vuoto, imposta l'elaborazione del timeout per impedire ai client di attendere troppo a lungo.

// 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#

Nella libreria di classi base .NET sono presenti alcuni oggetti che implementano questo modello. System.Threading.ThreadPoolè configurato per avere un numero predefinito di thread da allocare. Quando i thread vengono restituiti, sono disponibili per un altro calcolo. Pertanto, è possibile utilizzare i thread senza pagare il costo di creazione e smaltimento dei thread.

Di seguito viene mostrato il codice di base del modello di progettazione del pool di oggetti implementato utilizzando C#. Per brevità le proprietà delle classi sono dichiarate utilizzando la sintassi delle proprietà implementate automaticamente in C# 3.0. Questi potrebbero essere sostituiti con definizioni di proprietà complete per le versioni precedenti del linguaggio. Il pool viene mostrato come una classe statica, poiché è insolito che siano richiesti più pool. Tuttavia, è ugualmente accettabile utilizzare le classi di istanza per i pool di oggetti.

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;
        }
    }
}

Nel codice sopra, il PooledObject ha proprietà per il momento in cui è stato creato e un altro, che può essere modificato dal client, che viene ripristinato quando il PooledObject viene rilasciato di nuovo nel pool. Viene mostrato il processo di pulizia, al rilascio di un oggetto, che assicura che sia in uno stato valido prima che possa essere richiesto nuovamente dal pool.

Giava

Java supporta il pool di thread tramite java.util.concurrent.ExecutorServicee altre classi correlate. Il servizio executor ha un certo numero di thread "di base" che non vengono mai scartati. Se tutti i thread sono occupati, il servizio alloca il numero consentito di thread aggiuntivi che vengono successivamente eliminati se non utilizzati per un determinato periodo di scadenza. Se non sono consentiti altri thread, le attività possono essere messe in coda. Infine, se questa coda può diventare troppo lunga, può essere configurata per sospendere il thread richiedente.

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);
	}
}

Guarda anche

Appunti

Riferimenti

link esterno