Blocchi dei messaggi asincroni

La libreria di agenti fornisce diversi tipi di blocchi di messaggi che consentono di propagare i messaggi tra i componenti dell'applicazione in modo thread-safe. Questi tipi di blocchi di messaggi vengono spesso utilizzati con le varie routine di passaggio dei messaggi, come Concurrency::send, Concurrency::asend, Concurrency::receive e Concurrency::try_receive. Per ulteriori informazioni sulle routine di passaggio dei messaggi definite nella libreria di agenti, vedere Funzioni di passaggio dei messaggi.

Sezioni

Di seguito sono elencate le sezioni di questo argomento:

  • Origini e destinazioni

  • Propagazione dei messaggi

  • Cenni preliminari sui tipi di blocchi dei messaggi

  • Classe unbounded_buffer

  • Classe overwrite_buffer

  • Classe single_assignment

  • Classe call

  • Classe transformer

  • Classe choice

  • Classi join e multitype_join

  • Classe timer

  • Filtro dei messaggi

  • Prenotazione dei messaggi

Origini e destinazioni

Le origini e le destinazioni rappresentano due elementi partecipanti importanti nel passaggio dei messaggi. Con origine si intende un endpoint di comunicazione che invia messaggi, mentre con destinazione si intende un endpoint di comunicazione che riceve messaggi. L'origine può essere considerata come un endpoint da cui si legge, mentre la destinazione come un endpoint in cui si scrive. Le origini e le destinazioni vengono connesse insieme dalle applicazioni per formare le reti di messaggistica.

La libreria di agenti utilizza due classi astratte per rappresentare le origini e le destinazioni: Concurrency::ISource e Concurrency::ITarget. I tipi di blocchi dei messaggi che fungono da origini derivano da ISource, mentre i tipi di blocchi dei messaggi che fungono da destinazioni derivano da ITarget. I tipi di blocchi di messaggi che fungono da origine e destinazione derivano sia da ISource che da ITarget.

[vai all'inizio]

Propagazione dei messaggi

La propagazione dei messaggi consiste nell'inviare un messaggio da un componente all'altro. Quando viene offerto un messaggio a un blocco di messaggi, questo può accettare, rifiutare o posticipare il messaggio. Ogni tipo di blocco di messaggi archivia e trasmette i messaggi in modi diversi. Ad esempio, la classe unbounded_buffer archivia un numero illimitato di messaggi, la classe overwrite_buffer archivia un singolo messaggio per volta e la classe transformer archivia una versione modificata di ogni messaggio. Questi tipi di blocchi di messaggi vengono descritti più dettagliatamente più avanti in questo documento.

Quando un blocco di messaggi accetta un messaggio, può facoltativamente eseguire il lavoro e, se il blocco di messaggi è un'origine, passare il messaggio risultante a un altro membro della rete. Un blocco di messaggi può utilizzare una funzione di filtro per rifiutare i messaggi che non desidera ricevere. I filtri vengono descritti in modo più dettagliato più avanti in questo argomento, nella sezione Filtro dei messaggi. Un blocco di messaggi che posticipa un messaggio può prenotare il messaggio e utilizzarlo in un secondo momento. La prenotazione dei messaggi viene descritta in modo più dettagliato più avanti in questo argomento, nella sezione Prenotazione dei messaggi.

La libreria di agenti consente ai blocchi di messaggi di passare i messaggi in modalità sincrona o asincrona. Quando si passa un messaggio a un blocco di messaggi in modo sincrono, ad esempio, utilizzando la funzione send, il runtime blocca il contesto corrente fino a quando il blocco di destinazione non accetta o rifiuta il messaggio. Quando si passa un messaggio a un blocco di messaggi in modo asincrono, ad esempio, utilizzando la funzione asend, il runtime offre il messaggio alla destinazione e, se la destinazione lo accetta, il runtime esegue la pianificazione come attività asincrona che propaga il messaggio al ricevitore. Il runtime utilizza attività leggere per propagare i messaggi in modo cooperativo. Per ulteriori informazioni sulle attività leggere, vedere Utilità di pianificazione (runtime di concorrenza).

Le origini e le destinazioni vengono connesse insieme dalle applicazioni per formare le reti di messaggistica. Generalmente si effettua il collegamento della rete e si chiama il metodo send oppure asend per passare i dati alla rete. Per connettere un blocco di messaggi di origine a una destinazione, chiamare il metodo Concurrency::ISource::link_target. Per disconnettere un blocco di origine da una destinazione, chiamare il metodo Concurrency::ISource::unlink_target. Per disconnettere un blocco di origine da tutte le rispettive destinazioni, chiamare il metodo Concurrency::ISource::unlink_targets. Quando uno dei tipi predefiniti di blocchi di messaggi esce dall'ambito o viene eliminato, viene automaticamente disconnesso da tutti i blocchi di destinazione. Alcuni tipi di blocchi di messaggi prevedono un limite massimo di destinazioni in cui possono scrivere. Nella sezione riportata di seguito vengono descritte le restrizioni applicate ai tipi predefiniti di blocchi di messaggi.

[vai all'inizio]

Cenni preliminari sui tipi di blocchi dei messaggi

Nella tabella seguente viene brevemente descritto il ruolo dei principali tipi di blocchi dei messaggi.

  • unbounded_buffer
    Archivia una coda di messaggi.

  • overwrite_buffer
    Archivia un messaggio in cui è possibile scrivere e da cui è possibile leggere più volte.

  • single_assignment
    Archivia un messaggio in cui è possibile scrivere una volta e da cui è possibile leggere più volte.

  • call
    Esegue un'operazione quando viene ricevuto un messaggio.

  • transformer
    Esegue un'operazione quando vengono ricevuti i dati e viene inviato il risultato dell'operazione a un altro blocco di destinazione. La classe transformer può agire su diversi tipi di input e output.

  • choice
    Seleziona il primo messaggio disponibile da un set di origini.

  • join e multitype join
    Consentono di attendere la ricezione di tutti i messaggi da un set di origini e di combinare quindi i messaggi in un unico messaggio per un altro blocco dei messaggi.

  • timer
    Invia un messaggio a un blocco di destinazione a intervalli regolari.

Questi tipi di blocchi dei messaggi hanno caratteristiche diverse in modo da poter essere utilizzati in situazioni diverse. Di seguito sono riportate alcune di queste caratteristiche:

  • Tipo di propagazione: indica se il blocco dei messaggi funge da origine dei dati, da destinazione dei dati o da entrambe.

  • Ordinamento dei messaggi: indica se il blocco dei messaggi mantiene l'ordine originale in cui i messaggi vengono inviati o ricevuti. Ogni tipo di blocco dei messaggi predefinito mantiene l'ordine originale in cui i messaggi vengono inviati o ricevuti.

  • Numero di origini: indica il numero massimo di origini da cui è possibile leggere il blocco dei messaggi.

  • Numero di destinazioni: indica il numero massimo di destinazioni in cui è possibile scrivere il blocco dei messaggi.

Nella tabella seguente viene illustrato come queste caratteristiche sono correlate ai vari tipi di blocchi dei messaggi.

Tipo di blocco dei messaggi

Tipo di propagazione (origine, destinazione o entrambe)

Ordinamento dei messaggi (ordinati o non ordinati)

Numero di origini

Numero di destinazioni

unbounded_buffer

Entrambi

Ordinati

Non vincolato

Non vincolato

overwrite_buffer

Entrambi

Ordinati

Non vincolato

Non vincolato

single_assignment

Entrambi

Ordinati

Non vincolato

Non vincolato

call

Destinazione

Ordinati

Non vincolato

Non applicabile

transformer

Entrambi

Ordinati

Non vincolato

1

choice

Entrambi

Ordinati

10

1

join

Entrambi

Ordinati

Non vincolato

1

multitype_join

Entrambi

Ordinati

10

1

timer

Origine

Non applicabile

Non applicabile

1

Nelle sezioni seguenti vengono descritti in modo più dettagliato i tipi di blocchi dei messaggi.

[vai all'inizio]

Classe unbounded_buffer

La classe Concurrency::unbounded_buffer rappresenta una struttura di messaggistica asincrona di utilizzo generale. Questa classe archivia una coda di messaggi FIFO (First In, First Out) che possono essere letti da più destinazioni o in cui possono scrivere più origini. Quando una destinazione riceve un messaggio da un oggetto unbounded_buffer, il messaggio viene rimosso dalla coda di messaggi. Pertanto, sebbene un oggetto unbounded_buffer possa avere più destinazioni, ogni messaggio verrà ricevuto da una sola destinazione. La classe unbounded_buffer è utile quando si desidera passare più messaggi a un altro componente e tale componente deve ricevere ogni messaggio.

Esempio

Nell'esempio riportato di seguito viene mostrata la struttura di base relativa alla modalità di utilizzo della classe unbounded_buffer. In questo esempio vengono inviati tre valori a un oggetto unbounded_buffer, quindi i valori vengono nuovamente letti dallo stesso oggetto.

// unbounded_buffer-structure.cpp
// compile with: /EHsc
#include <agents.h>
#include <iostream>

using namespace Concurrency;
using namespace std;

int wmain()
{
   // Create an unbounded_buffer object that works with
   // int data.
   unbounded_buffer<int> items;

   // Send a few items to the unbounded_buffer object.
   send(items, 33);
   send(items, 44);
   send(items, 55);

   // Read the items from the unbounded_buffer object and print
   // them to the console.
   wcout << receive(items) << endl;
   wcout << receive(items) << endl;
   wcout << receive(items) << endl;
}

Questo esempio produce l'output seguente:

33
44
55

Per un esempio completo che illustra come utilizzare la classe unbounded_buffer, vedere Procedura: implementare vari modelli producer-consumer.

[vai all'inizio]

Classe overwrite_buffer

La classe Concurrency::overwrite_buffer è simile alla classe unbounded_buffer, con l'unica differenza che un oggetto overwrite_buffer archivia un solo messaggio. Inoltre, quando una destinazione riceve un messaggio da un oggetto overwrite_buffer, il messaggio non viene rimosso dal buffer. Pertanto, una copia del messaggio viene ricevuta da più destinazioni.

La classe overwrite_buffer è utile quando si desidera passare più messaggi a un altro componente, ma il componente necessita solo del valore più recente. Questa classe è utile anche quando si desidera trasmettere un messaggio a più componenti.

Esempio

Nell'esempio riportato di seguito viene mostrata la struttura di base relativa alla modalità di utilizzo della classe overwrite_buffer. In questo esempio vengono inviati tre valori a un oggetto overwrite _buffer, quindi il valore corrente viene letto dallo stesso oggetto tre volte. L'esempio è analogo a quello relativo alla classe unbounded_buffer, con la differenza che la classe overwrite_buffer archivia un solo messaggio. Inoltre, il runtime non rimuove il messaggio da un oggetto overwrite_buffer dopo che questo viene letto.

// overwrite_buffer-structure.cpp
// compile with: /EHsc
#include <agents.h>
#include <iostream>

using namespace Concurrency;
using namespace std;

int wmain()
{
   // Create an overwrite_buffer object that works with
   // int data.
   overwrite_buffer<int> item;

   // Send a few items to the overwrite_buffer object.
   send(item, 33);
   send(item, 44);
   send(item, 55);

   // Read the current item from the overwrite_buffer object and print
   // it to the console three times.
   wcout << receive(item) << endl;
   wcout << receive(item) << endl;
   wcout << receive(item) << endl;
}

Questo esempio produce l'output seguente:

55
55
55

Per un esempio completo che illustra come utilizzare la classe overwrite_buffer, vedere Procedura: implementare vari modelli producer-consumer.

[vai all'inizio]

Classe single_assignment

La classe Concurrency::single_assignment è simile alla classe overwrite_buffer, con l'unica differenza che in un oggetto single_assignment è possibile scrivere una sola volta. Analogamente alla classe overwrite_buffer, quando una destinazione riceve un messaggio da un oggetto single_assignment, il messaggio non viene rimosso dall'oggetto. Pertanto, una copia del messaggio viene ricevuta da più destinazioni. La classe single_assignment è utile anche quando si desidera trasmettere un solo messaggio a più componenti.

Esempio

Nell'esempio riportato di seguito viene mostrata la struttura di base relativa alla modalità di utilizzo della classe single_assignment. In questo esempio vengono inviati tre valori a un oggetto single_assignment, quindi il valore corrente viene letto dallo stesso oggetto tre volte. L'esempio è analogo a quello relativo alla classe overwrite_buffer. Sebbene sia la classe single_assignment che la classe overwrite_buffer archivino un solo messaggio, nella classe single_assignment è possibile scrivere una sola volta.

// single_assignment-structure.cpp
// compile with: /EHsc
#include <agents.h>
#include <iostream>

using namespace Concurrency;
using namespace std;

int wmain()
{
   // Create an single_assignment object that works with
   // int data.
   single_assignment<int> item;

   // Send a few items to the single_assignment object.
   send(item, 33);
   send(item, 44);
   send(item, 55);

   // Read the current item from the single_assignment object and print
   // it to the console three times.
   wcout << receive(item) << endl;
   wcout << receive(item) << endl;
   wcout << receive(item) << endl;
}

Questo esempio produce l'output seguente:

33
33
33

Per un esempio completo che illustra come utilizzare la classe single_assignment, vedere Procedura dettagliata: implementazione di future.

[vai all'inizio]

Classe call

La classe Concurrency::call funge da ricevitore del messaggio che esegue una funzione lavoro quando riceve i dati. Questa funzione lavoro può essere un'espressione lambda, un oggetto funzione o un puntatore a funzione. Un oggetto call si comporta in modo diverso da una comune chiamata di funzione poiché agisce in parallelo con gli altri componenti da cui riceve i messaggi. Se un oggetto call sta eseguendo un'operazione quando riceve un messaggio, il messaggio viene aggiunto a una coda. Ogni oggetto call elabora i messaggi in coda nell'ordine in cui vengono ricevuti.

Esempio

Nell'esempio riportato di seguito viene mostrata la struttura di base relativa alla modalità di utilizzo della classe call. In questo esempio viene creato un oggetto call che stampa ogni valore che riceve nella console. Vengono quindi inviati tre valori all'oggetto call. Poiché l'oggetto call elabora i messaggi in un thread separato, in questo esempio vengono utilizzati anche una variabile contatore e un oggetto event per garantire che l'oggetto call elabori tutti i messaggi prima che la funzione wmain restituisca un risultato.

// call-structure.cpp
// compile with: /EHsc
#include <agents.h>
#include <iostream>

using namespace Concurrency;
using namespace std;

int wmain()
{
   // An event that is set when the call object receives all values.
   event received_all;

   // Counts the 
   long receive_count = 0L;
   long max_receive_count = 3L;

   // Create an call object that works with int data.
   call<int> target([&received_all,&receive_count,max_receive_count](int n) {
      // Print the value that the call object receives to the console.
      wcout << n << endl;

      // Set the event when all messages have been processed.
      if (++receive_count == max_receive_count)
         received_all.set();
   });

   // Send a few items to the call object.
   send(target, 33);
   send(target, 44);
   send(target, 55);

   // Wait for the call object to process all items.
   received_all.wait();
}

Questo esempio produce l'output seguente:

33
44
55

Per un esempio completo che illustra come utilizzare la classe call, vedere Procedura: fornire funzioni lavoro alle classi call e transformer.

[vai all'inizio]

Classe transformer

La classe Concurrency::transformer funge sia da ricevitore che da mittente del messaggio. La classe transformer è simile alla classe call in quanto esegue una funzione lavoro definita dall'utente quando riceve i dati. La classe transformer invia però anche il risultato della funzione lavoro agli oggetti destinatario. Analogamente all'oggetto call, un oggetto transformer agisce in parallelo con gli altri componenti da cui riceve i messaggi. Se un oggetto transformer sta eseguendo un'operazione quando riceve un messaggio, il messaggio viene aggiunto a una coda. Ogni oggetto transformer elabora i messaggi in coda nell'ordine in cui vengono ricevuti.

La classe transformer invia il messaggio a una sola destinazione. Se si imposta il parametro _PTarget nel costruttore su NULL, è possibile specificare la destinazione in un secondo momento chiamando il metodo Concurrency::link_target.

A differenza di tutti gli altri tipi di blocchi dei messaggi asincroni forniti dalla libreria di agenti, la classe transformer può agire su diversi tipi di input e output. Questa possibilità di trasformare i dati da uno tipo all'altro rende la classe transformer un componente chiave in molte reti simultanee. Inoltre, è possibile aggiungere una funzionalità parallela più accurata nella funzione lavoro di un oggetto transformer.

Esempio

Nell'esempio riportato di seguito viene mostrata la struttura di base relativa alla modalità di utilizzo della classe transformer. In questo esempio viene creato un oggetto transformer che moltiplica ciascun valore int di input per 0,33 per produrre un valore double come output. Nell'esempio vengono quindi ricevuti i valori trasformati dallo stesso oggetto transformer e vengono stampati nella console.

// transformer-structure.cpp
// compile with: /EHsc
#include <agents.h>
#include <iostream>

using namespace Concurrency;
using namespace std;

int wmain()
{
   // Create an transformer object that receives int data and 
   // sends double data.
   transformer<int, double> third([](int n) {
      // Return one-third of the input value.
      return n * 0.33;
   });

   // Send a few items to the transformer object.
   send(third, 33);
   send(third, 44);
   send(third, 55);

   // Read the processed items from the transformer object and print
   // them to the console.
   wcout << receive(third) << endl;
   wcout << receive(third) << endl;
   wcout << receive(third) << endl;
}

Questo esempio produce l'output seguente:

10.89
14.52
18.15

Per un esempio completo che illustra come utilizzare la classe transformer, vedere Procedura: Utilizzare la classe transformer in una pipeline di dati.

[vai all'inizio]

Classe choice

La classe Concurrency::choice seleziona il primo messaggio disponibile da un set di origini. La classe choice rappresenta un meccanismo di flusso di controllo anziché un meccanismo di flusso di dati. Nell'argomento Libreria di agenti asincroni vengono descritte le differenze tra il flusso di dati e il flusso di controllo.

La lettura da un oggetto choice è simile alla chiamata alla funzione API Windows WaitForMultipleObjects quando il rispettivo parametro bWaitAll è impostato su FALSE. Tuttavia, la classe choice associa i dati all'evento stesso anziché a un oggetto di sincronizzazione esterno.

La classe choice viene in genere utilizzata con la funzione Concurrency::receive per gestire il flusso di controllo nell'applicazione. Utilizzare la classe choice quando è necessario effettuare la selezione tra buffer dei messaggi con tipi diversi. Utilizzare la classe single_assignment quando è necessario selezionare tra buffer dei messaggi con lo stesso tipo.

L'ordine in cui si collegano le origini a un oggetto choice è importante poiché può determinare il messaggio che viene selezionato. Si consideri, ad esempio, il caso in cui più buffer dei messaggi che già contengono un messaggio vengano collegati a un oggetto choice. L'oggetto choice seleziona il messaggio dalla prima origine a cui viene collegato. Una volta collegate tutte le origini, l'oggetto choice mantiene l'ordine in cui ogni origine riceve un messaggio.

Esempio

Nell'esempio riportato di seguito viene mostrata la struttura di base relativa alla modalità di utilizzo della classe choice. Nell'esempio viene utilizzata la funzione Concurrency::make_choice per creare un oggetto choice che effettua la selezione tra tre blocchi di messaggi. Nell'esempio vengono quindi calcolati vari numeri Fibonacci e ogni risultato viene archiviato in un blocco di messaggi diverso. Viene quindi stampato nella console un messaggio basato sulla prima operazione completata.

// choice-structure.cpp
// compile with: /EHsc
#include <agents.h>
#include <ppl.h>
#include <iostream>

using namespace Concurrency;
using namespace std;

// Computes the nth Fibonacci number.
// This function illustrates a lengthy operation and is therefore
// not optimized for performance.
int fibonacci(int n)
{
   if (n < 2)
      return n;
   return fibonacci(n-1) + fibonacci(n-2);
}

int wmain()
{
   // Although the following thee message blocks are written to one time only, 
   // this example illustrates the fact that the choice class works with 
   // different message block types.

   // Holds the 35th Fibonacci number.
   single_assignment<int> fib35;
   // Holds the 37th Fibonacci number.
   overwrite_buffer<int> fib37;
   // Holds half of the 42nd Fibonacci number.
   unbounded_buffer<double> half_of_fib42;   

   // Create a choice object that selects the first single_assignment 
   // object that receives a value.
   auto select_one = make_choice(&fib35, &fib37, &half_of_fib42);

   // Execute a few lengthy operations in parallel. Each operation sends its 
   // result to one of the single_assignment objects.
   parallel_invoke(
      [&fib35] { send(fib35, fibonacci(35)); },
      [&fib37] { send(fib37, fibonacci(37)); },
      [&half_of_fib42] { send(half_of_fib42, fibonacci(42) * 0.5); }
   );

   // Print a message that is based on the operation that finished first.
   switch (receive(select_one))
   {
   case 0:
      wcout << L"fib35 received its value first. Result = " 
            << receive(fib35) << endl;
      break;
   case 1:
      wcout << L"fib37 received its value first. Result = " 
            << receive(fib37) << endl;
      break;
   case 2:
      wcout << L"half_of_fib42 received its value first. Result = " 
            << receive(half_of_fib42) << endl;
      break;
   default:
      wcout << L"Unexpected." << endl;
      break;
   }
}

Questo esempio produce l'output seguente:

fib35 received its value first. Result = 9227465

Poiché non è garantito che l'attività che calcola il trentacinquesimo numero Fibonacci venga completata per prima, l'output di questo esempio può variare.

In questo esempio viene utilizzato l'algoritmo Concurrency::parallel_invoke per calcolare i numeri Fibonacci in parallelo. Per ulteriori informazioni su parallel_invoke, vedere Algoritmi paralleli.

Per un esempio completo che illustra come utilizzare la classe choice, vedere Procedura: effettuare una scelta tra le attività completate.

[vai all'inizio]

Classi join e multitype_join

Le classi Concurrency::join e Concurrency::multitype_join consentono di attendere che ogni membro di un set di origini riceva un messaggio. La classe join agisce sugli oggetti di origine con un tipo di messaggio comune. La classe multitype_join agisce sugli oggetti di origine con tipi di messaggio diversi.

La lettura da un oggetto join oppure multitype_join è analoga alla funzione API Windows WaitForMultipleObjects quando il rispettivo parametro bWaitAll è impostato su TRUE. Tuttavia, analogamente a un oggetto choice, gli oggetti join e multitype_join utilizzano un meccanismo degli eventi che associa i dati all'evento stesso anziché a un oggetto di sincronizzazione esterno.

La lettura da un oggetto join produce un oggetto std::vector. La lettura da un oggetto multitype_join produce un oggetto std::tuple. Gli elementi vengono visualizzati in questi oggetti nello stesso ordine in cui i buffer di origine corrispondenti vengono collegati all'oggetto join o multitype_join. Dal momento che l'ordine con cui i buffer di origine vengono collegati a un oggetto multitype_join oppure join è associato all'ordine degli elementi nell'oggetto vector o tuple risultante, si consiglia di non scollegare un buffer di origine esistente da un join. In caso contrario, si potrebbe ottenere un comportamento non specificato.

Join greedy e non-greedy

Le classi join e multitype_join supportano il concetto di join greedy e non-greedy. Un join greedy accetta un messaggio da ognuna delle origini man mano che i messaggi divengono disponibili finché non saranno disponibili tutti i messaggi. Un join non-greedy riceve i messaggi in due fasi. Inizialmente un join non-greedy resta in attesa che gli venga offerto un messaggio da ciascuna delle rispettive origini. Successivamente, una volta che tutti i messaggi di origine sono disponibili, un join non-greedy prova a prenotare ciascuno dei messaggi. Se riesce a prenotarli, utilizza tutti i messaggi e li propaga alla destinazione. In caso contrario, rilascia o annulla, le prenotazioni dei messaggi e attende nuovamente che ogni origine riceva un messaggio.

Le prestazioni dei join greedy sono migliori rispetto a quelle dei join non-greedy poiché accettano i messaggi immediatamente. Tuttavia, in rari casi, i join greedy possono causare deadlock. Utilizzare un join non-greedy quando sono presenti più join che contengono uno o più oggetti di origine condivisi.

Esempio

Nell'esempio riportato di seguito viene mostrata la struttura di base relativa alla modalità di utilizzo della classe join. In questo esempio viene utilizzata la funzione Concurrency::make_join per creare un oggetto join che riceve da tre oggetti single_assignment. Questo esempio calcola diversi numeri Fibonacci, archivia ciascun risultato in un oggetto single_assignment diverso, quindi stampa nella console ogni risultato che l'oggetto join gestisce. Questo esempio è simile all'esempio relativo alla classe choice, con la differenza che la classe join resta in attesa che tutti i blocchi di messaggi di origine ricevano un messaggio.

// join-structure.cpp
// compile with: /EHsc
#include <agents.h>
#include <ppl.h>
#include <iostream>

using namespace Concurrency;
using namespace std;

// Computes the nth Fibonacci number.
// This function illustrates a lengthy operation and is therefore
// not optimized for performance.
int fibonacci(int n)
{
   if (n < 2)
      return n;
   return fibonacci(n-1) + fibonacci(n-2);
}

int wmain()
{
   // Holds the 35th Fibonacci number.
   single_assignment<int> fib35;
   // Holds the 37th Fibonacci number.
   single_assignment<int> fib37;
   // Holds half of the 42nd Fibonacci number.
   single_assignment<double> half_of_fib42;   

   // Create a join object that selects the values from each of the
   // single_assignment objects.
   auto join_all = make_join(&fib35, &fib37, &half_of_fib42);

   // Execute a few lengthy operations in parallel. Each operation sends its 
   // result to one of the single_assignment objects.
   parallel_invoke(
      [&fib35] { send(fib35, fibonacci(35)); },
      [&fib37] { send(fib37, fibonacci(37)); },
      [&half_of_fib42] { send(half_of_fib42, fibonacci(42) * 0.5); }
   );

   auto result = receive(join_all);
   wcout << L"fib35 = " << get<0>(result) << endl;
   wcout << L"fib37 = " << get<1>(result) << endl;
   wcout << L"half_of_fib42 = " << get<2>(result) << endl;
}

Questo esempio produce l'output seguente:

fib35 = 9227465
fib37 = 24157817
half_of_fib42 = 1.33957e+008

In questo esempio viene utilizzato l'algoritmo Concurrency::parallel_invoke per calcolare i numeri Fibonacci in parallelo. Per ulteriori informazioni su parallel_invoke, vedere Algoritmi paralleli.

Per esempi completi che illustrano come utilizzare la classe join, vedere Procedura: effettuare una scelta tra le attività completate e Procedura dettagliata: utilizzo della classe join per impedire un deadlock.

[vai all'inizio]

Classe timer

La classe Concurrency::timer funge da origine del messaggio. Un oggetto timer invia un messaggio a una destinazione dopo la scadenza di un periodo di tempo specificato. La classe timer è utile quando è necessario differire l'invio di un messaggio o si desidera inviare un messaggio a intervalli regolari.

La classe timer invia il messaggio a una sola destinazione. Se si imposta il parametro _PTarget nel costruttore su NULL, è possibile specificare la destinazione in un secondo momento chiamando il metodo Concurrency::ISource::link_target.

Un oggetto timer può essere ripetuto o non ripetuto. Per creare un oggetto timer ripetuto, passare true per il parametro _Repeating quando si chiama il costruttore. In caso contrario, passare false per il parametro _Repeating per creare un oggetto timer non ripetuto. Se l'oggetto timer è ripetuto, lo stesso messaggio viene inviato alla destinazione dopo ogni intervallo.

La libreria di agenti crea gli oggetti timer nello stato non avviato. Per avviare un oggetto timer, chiamare il metodo Concurrency::timer::start. Per arrestare un oggetto timer, eliminare l'oggetto o chiamare il metodo Concurrency::timer::stop. Per sospendere un oggetto timer ripetuto, chiamare il metodo Concurrency::timer::pause.

Esempio

Nell'esempio riportato di seguito viene mostrata la struttura di base relativa alla modalità di utilizzo della classe timer. Nell'esempio vengono utilizzati gli oggetti timer e call per indicare lo stato di avanzamento di un'operazione di lunga durata.

// timer-structure.cpp
// compile with: /EHsc
#include <agents.h>
#include <iostream>

using namespace Concurrency;
using namespace std;

// Computes the nth Fibonacci number.
// This function illustrates a lengthy operation and is therefore
// not optimized for performance.
int fibonacci(int n)
{
   if (n < 2)
      return n;
   return fibonacci(n-1) + fibonacci(n-2);
}

int wmain()
{
   // Create a call object that prints characters that it receives 
   // to the console.
   call<wchar_t> print_character([](wchar_t c) {
      wcout << c;
   });

   // Create a timer object that sends the period (.) character to 
   // the call object every 100 milliseconds.
   timer<wchar_t> progress_timer(100u, L'.', &print_character, true);

   // Start the timer.
   wcout << L"Computing fib(42)";
   progress_timer.start();

   // Compute the 42nd Fibonacci number.
   int fib42 = fibonacci(42);

   // Stop the timer and print the result.
   progress_timer.stop();
   wcout << endl << L"result is " << fib42 << endl;
}

Questo esempio produce l'output seguente:

Computing fib(42)..................................................
result is 267914296

Per un esempio completo che illustra come utilizzare la classe timer, vedere Procedura: inviare un messaggio a intervalli regolari.

[vai all'inizio]

Filtro dei messaggi

Quando si crea un oggetto blocco di messaggi, è possibile fornire una funzione di filtro in base alla quale si stabilisce se il blocco di messaggi deve accettare o rifiutare un messaggio. Le funzioni di filtro garantiscono che un blocco di messaggi riceva solo determinati valori.

Nell'esempio riportato di seguito viene illustrato come creare un oggetto unbounded_buffer che utilizza una funzione di filtro in base alla quale vengono accettati solo i numeri pari. L'oggetto unbounded_buffer rifiuta i numeri dispari, pertanto non li propaga ai rispettivi blocchi di destinazione.

// filter-function.cpp
// compile with: /EHsc
#include <agents.h>
#include <iostream>

using namespace Concurrency;
using namespace std;

int wmain()
{
   // Create an unbounded_buffer object that uses a filter
   // function to accept only even numbers.
   unbounded_buffer<int> accept_evens(
      [](int n) {
         return (n%2) == 0;
      });

   // Send a few values to the unbounded_buffer object.
   unsigned int accept_count = 0;
   for (int i = 0; i < 10; ++i)
   {
      // The asend function returns true only if the target
      // accepts the message. This enables us to determine
      // how many elements are stored in the unbounded_buffer
      // object.
      if (asend(accept_evens, i))
      {
         ++accept_count;
      }
   }

   // Print to the console each value that is stored in the 
   // unbounded_buffer object. The unbounded_buffer object should
   // contain only even numbers.
   while (accept_count > 0)
   {
      wcout << receive(accept_evens) << L' ';
      --accept_count;
   }
}

Questo esempio produce l'output seguente:

0 2 4 6 8

Una funzione di filtro può essere una funzione lambda, un puntatore a funzione o un oggetto funzione. Ogni funzione di filtro assume uno dei formati indicati di seguito.

bool (_Type)
bool (_Type const &)

Per eliminare la copia non necessaria dei dati, utilizzare il secondo formato quando è presente un tipo di aggregato che viene propagato in base al valore.

Il filtro dei messaggi supporta il modello di programmazione dataflow, in cui i componenti eseguono calcoli quando ricevono i dati. Per gli esempi in cui vengono utilizzate le funzioni di filtro per controllare il flusso di dati in una rete di passaggio dei messaggi, vedere Procedura: utilizzare il filtro di blocco dei messaggi, Procedura dettagliata: creazione di un agente del flusso di dati e Procedura dettagliata: creazione di una rete per l'elaborazione di immagini.

[vai all'inizio]

Prenotazione dei messaggi

La prenotazione dei messaggi consente a un blocco di messaggi di prenotare un messaggio per un utilizzo successivo. Questa funzionalità in genere non viene utilizzata direttamente ma può risultare utile comprenderne il funzionamento per conoscere meglio il comportamento di alcuni dei tipi predefiniti di blocchi di messaggi.

Si considerino i join non-greedy e greedy. Entrambi utilizzano la prenotazione dei messaggi per consentirne l'utilizzo successivo. Come descritto in precedenza, un join non-greedy riceve i messaggi in due fasi. Durante la prima fase, un oggetto join non-greedy resta in attesa che ciascuna delle rispettive origini riceva un messaggio. Un join non-greedy quindi prova a prenotare ognuno di tali messaggi. Se riesce a prenotarli, utilizza tutti i messaggi e li propaga alla destinazione. In caso contrario, rilascia o annulla le prenotazioni dei messaggi e attende nuovamente che ogni origine riceva un messaggio.

Un join greedy, che legge anche i messaggi di input da una serie di origini, utilizza la prenotazione dei messaggi per leggere altri messaggi in attesa di ricevere un messaggio da ogni origine. Si consideri, ad esempio, un join greedy che riceve messaggi dai blocchi di messaggi A e B. Se il join greedy riceve due messaggi da B ma non ha ancora ricevuto un messaggio da A, il join greedy salva l'identificatore univoco del messaggio per il secondo messaggio da B. Dopo che il join greedy riceve un messaggio da A e propaga all'esterno questi messaggi, utilizza l'identificatore del messaggio salvato per verificare che il secondo messaggio da B sia ancora disponibile.

È possibile utilizzare la prenotazione dei messaggi quando si implementano tipi personalizzati di blocco dei messaggi. Per un esempio di creazione di un tipo di blocco dei messaggi, vedere Procedura dettagliata: creazione di un blocco dei messaggi personalizzato.

[vai all'inizio]

Vedere anche

Concetti

Libreria di agenti asincroni

Cronologia delle modifiche

Data

Cronologia

Motivo

Agosto 2010

Sono stati aggiunti esempi di base.

Miglioramento delle informazioni.