Pooling

L'esempio di pooling illustra come estendere Windows Communication Foundation (WCF) per supportare il pooling di oggetti. L'esempio illustra come creare un attributo sintatticamente e semanticamente simile alla funzionalità dell'attributo ObjectPoolingAttribute di Enterprise Services. Il pool degli oggetti può fornire una spinta notevole alle prestazioni di un'applicazione. Tuttavia, può avere l'effetto contrario se non utilizzato correttamente. Il pool degli oggetti consente di ridurre il sovraccarico dovuto alla creazione continua di oggetti frequentemente utilizzati che richiedono un'inizializzazione estesa. Tuttavia, se una chiamata a un metodo su un oggetto del pool richiede una quantità considerevole di tempo, il pool degli oggetti mette in coda richieste aggiuntive appena viene raggiunta la dimensione del pool massima. Pertanto può non riuscire a soddisfare richieste di creazione di oggetti generando un'eccezione di timeout.

Nota

La procedura di installazione e le istruzioni di compilazione per questo esempio si trovano alla fine di questo argomento.

Il primo passaggio per la creazione di un'estensione WCF consiste nel decidere il punto di estendibilità da usare.

In WCF il termine dispatcher fa riferimento a un componente di runtime responsabile della conversione dei messaggi in arrivo in chiamate al metodo nel servizio dell'utente e per la conversione di valori restituiti da tale metodo a un messaggio in uscita. Un servizio WCF crea un dispatcher per ciascun endpoint. Un client WCF deve usare un dispatcher se il contratto associato a tale client è un contratto duplex.

I dispatcher del canale e dell'endpoint offrono estensibilità sul canale e sul contratto esponendo le varie proprietà che controllano il comportamento del dispatcher. La proprietà DispatchRuntime consente inoltre di ispezionare, modificare e personalizzare la modalità di autenticazione dei certificati. Questo esempio si concentra sulla proprietà InstanceProvider che punta all'oggetto che fornisce le istanze della classe del servizio.

Provider di istanze

In WCF il dispatcher crea istanze della classe del servizio usando un InstanceProvider, che implementa l'interfaccia IInstanceProvider. Questa interfaccia ha tre metodi:

Pool di oggetti

Un'implementazione personalizzata della classe IInstanceProvider fornisce la semantica del pool di oggetti necessaria per un servizio. Pertanto, questo esempio ha un tipo ObjectPoolingInstanceProvider che fornisce un'implementazione personalizzata della classe IInstanceProvider per il pool. Quando Dispatcher chiama il metodo GetInstance(InstanceContext, Message), anziché creare una nuova istanza, l'implementazione personalizzata cerca un oggetto esistente in un pool in memoria che viene restituito se disponibile. In caso contrario, viene creato un nuovo oggetto. Nell'esempio di codice seguente viene illustrata l'implementazione di GetInstance.

object IInstanceProvider.GetInstance(InstanceContext instanceContext, Message message)
{
    object obj = null;

    lock (poolLock)
    {
        if (pool.Count > 0)
        {
            obj = pool.Pop();
        }
        else
        {
            obj = CreateNewPoolObject();
        }
        activeObjectsCount++;
    }

    WritePoolMessage(ResourceHelper.GetString("MsgNewObject"));

    idleTimer.Stop();

    return obj;
}

L'implementazione personalizzata di ReleaseInstance aggiunge l'istanza rilasciata nuovamente al pool e decrementa il valore di ActiveObjectsCount. Il Dispatcher può chiamare questi metodi da thread diversi e pertanto sincronizzare l'accesso ai membri del livello della classe, nella classe ObjectPoolingInstanceProvider obbligatoria.

void IInstanceProvider.ReleaseInstance(InstanceContext instanceContext, object instance)
{
    lock (poolLock)
    {
        pool.Push(instance);
        activeObjectsCount--;

        WritePoolMessage(
        ResourceHelper.GetString("MsgObjectPooled"));

        // When the service goes completely idle (no requests
        // are being processed), the idle timer is started
        if (activeObjectsCount == 0)
            idleTimer.Start();
    }
}

Il metodo ReleaseInstance fornisce una funzionalità di "inizializzazione di pulizia". Normalmente il pool gestisce un numero minimo di oggetti per la durata del pool. Ci possono essere, tuttavia, periodi di utilizzo eccessivo che richiedono la creazione di oggetti aggiuntivi nel pool per raggiungere il limite massimo specificato nella configurazione. Successivamente, quando il pool diviene meno attivo, quegli oggetti in surplus possono divenire un sovraccarico aggiuntivo. Pertanto, quando il conteggio activeObjectsCount si azzera, viene avviato un timer, precedentemente inattivo, che inizia ed esegue un ciclo di pulizia.

Aggiunta del comportamento.

Le estensioni del livello del dispatcher vengono collegate utilizzando i comportamenti seguenti:

  • Comportamenti del servizio. Essi consentono la personalizzazione del runtime dell'intero servizio.

  • Comportamenti dell'endpoint. Essi consentono la personalizzazione degli endpoint del servizio, in particolare un canale e un dispatcher dell'endpoint.

  • Comportamenti del contratto. Essi consentono la personalizzazione di entrambe le classi ClientRuntime e DispatchRuntime rispettivamente sul client e sui servizi.

Allo scopo di un'estensione del pool di oggetti deve essere creato un comportamento del servizio. I comportamenti del servizio vengono creati implementando l'interfaccia IServiceBehavior. Ci sono molti modi per rendere consapevole il modello dei servizi dei comportamenti personalizzati:

  • Utilizzo di un attributo personalizzato.

  • Aggiunto imperativamente alla raccolta di comportamenti della descrizione del servizio.

  • Estensione dei file di configurazione

Questo esempio utilizza un attributo personalizzato. Quando la classe ServiceHost viene costruita, essa esamina gli attributi utilizzati nella definizione del tipo del servizio e aggiunge i comportamenti disponibili alla raccolta di comportamenti della descrizione del servizio.

L'interfaccia IServiceBehavior contiene tre metodi: Validate, AddBindingParameters e ApplyDispatchBehavior. Il metodo Validate viene utilizzato per assicurare che il comportamento possa essere applicato al servizio. In questo esempio, l'implementazione assicura che il servizio non sia configurato con Single. Il metodo AddBindingParameters viene utilizzato per configurare le associazioni del servizio. Non è obbligatorio in questo scenario. Il metodo ApplyDispatchBehavior viene utilizzato per configurare i dispatcher del servizio. Questo metodo viene chiamato da WCF quando l'oggetto ServiceHost viene inizializzato. I parametri seguenti vengono passati in questo metodo:

  • Description: questo argomento fornisce la descrizione del servizio per l'intero servizio. Può essere utilizzato per controllare dati della descrizione sugli endpoint del servizio, contratti, associazioni e altri dati.

  • ServiceHostBase: questo argomento fornisce la classe ServiceHostBase attualmente in fase di inizializzazione.

Nell'implementazione personalizzata della classe IServiceBehavior viene creata una nuova istanza di ObjectPoolingInstanceProvider e viene assegnata alla proprietà InstanceProvider in ogni DispatchRuntime in ServiceHostBase.

void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase serviceHostBase)
{
    // Create an instance of the ObjectPoolInstanceProvider.
    ObjectPoolingInstanceProvider instanceProvider = new
           ObjectPoolingInstanceProvider(description.ServiceType,
                                                    minPoolSize);

    // Forward the call if we created a ServiceThrottlingBehavior.
    if (this.throttlingBehavior != null)
    {
        ((IServiceBehavior)this.throttlingBehavior).ApplyDispatchBehavior(description, serviceHostBase);
    }

    // In case there was already a ServiceThrottlingBehavior
    // (this.throttlingBehavior==null), it should have initialized
    // a single ServiceThrottle on all ChannelDispatchers.
    // As we loop through the ChannelDispatchers, we verify that
    // and modify the ServiceThrottle to guard MaxPoolSize.
    ServiceThrottle throttle = null;

    foreach (ChannelDispatcherBase cdb in
            serviceHostBase.ChannelDispatchers)
    {
        ChannelDispatcher cd = cdb as ChannelDispatcher;
        if (cd != null)
        {
            // Make sure there is exactly one throttle used by all
            // endpoints. If there were others, we could not enforce
            // MaxPoolSize.
            if ((this.throttlingBehavior == null) &&
                        (this.maxPoolSize != Int32.MaxValue))
            {
                throttle ??= cd.ServiceThrottle;
                if (cd.ServiceThrottle == null)
                {
                    throw new
InvalidOperationException(ResourceHelper.GetString("ExNullThrottle"));
                }
                if (throttle != cd.ServiceThrottle)
                {
                    throw new InvalidOperationException(ResourceHelper.GetString("ExDifferentThrottle"));
                }
             }

             foreach (EndpointDispatcher ed in cd.Endpoints)
             {
                 // Assign it to DispatchBehavior in each endpoint.
                 ed.DispatchRuntime.InstanceProvider =
                                      instanceProvider;
             }
         }
     }

     // Set the MaxConcurrentInstances to limit the number of items
     // that will ever be requested from the pool.
     if ((throttle != null) && (throttle.MaxConcurrentInstances >
                                      this.maxPoolSize))
     {
         throttle.MaxConcurrentInstances = this.maxPoolSize;
     }
}

Oltre a un'implementazione della classe IServiceBehavior, la classe ObjectPoolingAttribute ha molti membri per personalizzare il pool di oggetti utilizzando gli argomenti dell'attributo. Questi membri includono MaxPoolSize, MinPoolSize e CreationTimeout, per corrispondere al set di funzionalità del pool di oggetti fornito da Enterprise Services .NET.

Il comportamento del pool di oggetti può ora essere aggiunto a un servizio WCF annotando l'implementazione del servizio con l'attributo personalizzato ObjectPooling appena creato.

[ObjectPooling(MaxPoolSize=1024, MinPoolSize=10, CreationTimeout=30000)]
public class PoolService : IPoolService
{
  // …
}

Esecuzione dell'esempio

L'esempio illustra i vantaggi a livello di prestazioni che possono essere raggiunti utilizzando i pool di oggetti in alcuni scenari.

L'applicazione di servizio implementa due servizi: WorkService e ObjectPooledWorkService. Entrambi i servizi condividono la stessa implementazione. Richiedono entrambi una lunga inizializzazione e quindi espongono un metodo DoWork() che è relativamente conveniente. La sola differenza è che ObjectPooledWorkService ha un pool di oggetti configurato:

[ObjectPooling(MinPoolSize = 0, MaxPoolSize = 5)]
public class ObjectPooledWorkService : IDoWork
{
    public ObjectPooledWorkService()
    {
        Thread.Sleep(5000);
        ColorConsole.WriteLine(ConsoleColor.Blue, "ObjectPooledWorkService instance created.");
    }

    public void DoWork()
    {
        ColorConsole.WriteLine(ConsoleColor.Blue, "ObjectPooledWorkService.GetData() completed.");
    }
}

Quando si esegue il client, effettua la chiamata a WorkService 5 volte. Quindi effettua la chiamata a ObjectPooledWorkService 5 volte. Viene infine visualizzata la differenza:

Press <ENTER> to start the client.

Calling WorkService:
1 - DoWork() Done
2 - DoWork() Done
3 - DoWork() Done
4 - DoWork() Done
5 - DoWork() Done
Calling WorkService took: 26722 ms.
Calling ObjectPooledWorkService:
1 - DoWork() Done
2 - DoWork() Done
3 - DoWork() Done
4 - DoWork() Done
5 - DoWork() Done
Calling ObjectPooledWorkService took: 5323 ms.
Press <ENTER> to exit.

Nota

La prima volta che il client viene eseguito, entrambi i servizi sembrano avere la stessa durata. Se si esegue nuovamente l'esempio, è possibile vedere che ObjectPooledWorkService restituisce molto più rapidamente perché un'istanza di quell'oggetto già esiste nel pool.

Per impostare, compilare ed eseguire l'esempio

  1. Assicurarsi di aver eseguito la Procedura di installazione singola per gli esempi di Windows Communication Foundation.

  2. Per compilare la soluzione, seguire le istruzioni in Compilazione degli esempi di Windows Communication Foundation.

  3. Per eseguire l'esempio in un solo computer o tra computer diversi, seguire le istruzioni in Esecuzione degli esempi di Windows Communication Foundation.

Nota

Se si usa Svcutil.exe per rigenerare la configurazione di questo esempio, assicurarsi di modificare il nome dell'endpoint nella configurazione client in modo che corrisponda al codice client.