Ottobre 2015

Volume 30 Numero 10

Il presente articolo è stato tradotto automaticamente.

Windows con C++ - Coroutine in Visual C++ 2015

Da Kenny Kerr | Ottobre 2015

Innanzitutto imparato coroutine in C++ nel 2012 e parlato idee in una serie di articoli in MSDN Magazine. Esame di un semplice form di multitasking cooperativo che emulata coroutine riproducendo trucchi intelligenti con istruzioni switch. Quindi, ho illustrato alcune attività per migliorare l'efficienza e componibilità di sistemi asincroni con le estensioni proposte per le promesse e future. Infine, ho illustrato alcune problematiche che esistono anche con una visione futuristica di ritardo, nonché una proposta di un elemento denominato funzioni ripristinabili. È consigliabile leggere questi se siete interessati ad alcune delle sfide e cronologia correlati alla concorrenza elegante in C++:

Gran parte di tale articolo è stato teorica poiché avevo un compilatore che implementato uno qualsiasi di tali idee e al fine di emulare in vari modi. E quindi Visual Studio 2015 fornito all'inizio dell'anno. Questa edizione di Visual C++ include un'opzione del compilatore sperimentale denominata / await che sblocca un'implementazione di coroutine supportate direttamente dal compilatore. Non è più HACK, le macro o altri magia. Si tratta di realistica, sia essa sperimentale e ancora unsanctioned dal comitato di C++. E non si tratta di zucchero sintattico solo nel compilatore front-end, come gli argomenti con i c# yield (parola chiave) e asincrono. L'implementazione di C++ include un investimento nella progettazione completo del compilatore back-end che offre un'implementazione estremamente scalabile. Effettivamente, va ben oltre ciò che è possibile se il front-end del compilatore fornito semplicemente una sintassi più utile per l'utilizzo di promesse e ritardo o anche la classe di attività di Runtime di concorrenza. Vedere questo aspetto oggi Iniziamo riprendo l'argomento. Molto è cambiato rispetto 2012, quindi inizierò con una breve panoramica per illustrare in cui sono stati fatti da e ci si trovi prima di esaminare alcuni esempi più specifici e pratiche.

Conclude la serie menzionati in precedenza con un esempio interessante per le funzioni può essere ripristinate, quindi Inizierò presenti. Si immagini una coppia di risorse per la lettura da un file e la scrittura in una connessione di rete:

struct File
{
  unsigned Read(void * buffer, unsigned size);
};
struct Network
{
  void Write(void const * buffer, unsigned size);
};

È possibile utilizzare la vostra immaginazione per compilare il resto, ma è piuttosto rappresentativo dell'aspetto tradizionale sincrone. Metodo di lettura del file tenta di leggere dati dalla posizione corrente del file nel buffer fino a una dimensione massima e restituisce il numero effettivo di byte copiati. Se il valore restituito è inferiore alla dimensione richiesta, in genere significa che è stata raggiunta la fine del file. La classe di rete consente di modellare un protocollo orientato alla connessione tipico, ad esempio TCP o named pipe di Windows. Il metodo Write copia un numero specifico di byte allo stack di rete. Un'operazione di copia sincrono tipica è facile immaginare, ma ti aiuterò con Figura 1 in modo da disporre di un frame di riferimento.

Figura 1 operazione di copia sincrono

File file = // Open file
Network network = // Open connection
uint8_t buffer[4096];
while (unsigned const actual = file.Read(buffer, sizeof(buffer)))
{
  network.Write(buffer, actual);
}

Fino a quando il metodo Read restituisce un valore maggiore di zero, i byte risultanti vengono copiati dal buffer intermedio alla rete utilizzando il metodo di scrittura. Si tratta di codice che tutti i programmatori ragionevoli non avrebbe alcuna conoscenza di problemi, indipendentemente dalla loro sfondo. Naturalmente, Windows fornisce i servizi che supportano l'offload questo tipo di operazione interamente nel kernel per evitare tutte le transizioni, ma tali servizi sono limitati a scenari specifici e questo è rappresentativo dei tipi di applicazioni sono spesso legate backup con operazioni di blocco.

La libreria Standard C++ offre future e promette nel tentativo di supportare operazioni asincrone, ma è stata molto funzionalità a causa di progettazione naive. Ho parlato di tali problemi nel 2012. Trascurando anche tali problemi, riscrivere l'esempio di copia di file di rete in Figura 1 non trascurabile. La traduzione delle sincrono (semplice) durante il ciclo più diretta richiede un algoritmo di iterazione attentamente "artigianali" che può scorrere una catena di futures:

template <typename F>
future<void> do_while(F body)
{
  shared_ptr<promise<void>> done = make_shared<promise<void>>();
  iteration(body, done);
  return done->get_future();
}

L'algoritmo può rivelarsi in realtà all'interno della funzione di iterazione:

template <typename F>
void iteration(F body, shared_ptr<promise<void>> const & done)
{
  body().then([=](future<bool> const & previous)
  {
    if (previous.get()) { iteration(body, done); }
    else { done->set_value(); }
  });
}

L'espressione lambda deve acquisire la promessa condivisa per valore, poiché si tratta davvero iterativo ricorsiva. Ma ciò risulta problematica, significa che una coppia di operazioni interlock per ogni iterazione. Inoltre, ritardo non è ancora un metodo "then" continuazioni di catena, anche se è possibile simulare questa situazione oggi con la classe di attività di Runtime di concorrenza. Comunque, supponendo che tali algoritmi futuristico e continuazioni esistano, potrei riscrivere l'operazione di copia sincrono da Figura 1 in modo asincrono. Verrà innanzitutto necessario aggiungere gli overload asincroni per le classi File e rete. Ad esempio simile al seguente:

struct File
{
  unsigned Read(void * buffer, unsigned const size);
  future<unsigned> ReadAsync(void * buffer, unsigned const size);
};
struct Network
{
  void Write(void const * buffer, unsigned const size);
  future<unsigned> WriteAsync(void const * buffer, unsigned const size)
};

Futuro WriteAsync del metodo deve restituire il numero di byte copiati, poiché si tratta di tutto una continuazione potrebbe essere per stabilire se terminare l'iterazione. Potrebbe essere un'altra opzione per la classe di File fornire un metodo EndOfFile. In ogni caso, dato queste nuove primitive, l'operazione di copia può essere espresso in modo che è comprensibile se è stata imbibed quantità sufficiente di zuccheri. Figura 2 illustra questo approccio.

Figura 2 operazione di copia con ritardo

File file = // Open file
Network network = // Open connection
uint8_t buffer[4096];
future<void> operation = do_while([&]
{
  return file.ReadAsync(buffer, sizeof(buffer))
    .then([&](task<unsigned> const & read)
    {
      return network.WriteAsync(buffer, read.get());
    })
    .then([&](task<unsigned> const & write)
    {
      return write.get() == sizeof(buffer);
    });
});
operation.get();

L'algoritmo do_while facilita il concatenamento delle continuazioni come "corpo" del ciclo restituisce true. ReadAsync viene pertanto chiamato, il cui risultato viene utilizzato dalla WriteAsync, il cui risultato viene testato come condizione del ciclo. Questo non è scienza missilistica, ma non si dispone di alcun desiderio di scrivere codice simile. Si è complicato e diventa rapidamente troppo complessa per prendere in considerazione. Immettere funzioni ripristinabili.

Aggiunta di / await compilatore opzione Abilita il supporto del compilatore per le funzioni ripristinabili, un'implementazione di coroutine per C++. Vengono chiamate funzioni ripristinabili anziché semplicemente coroutine poiché si sta serve un comportamento molto simile C++ tradizionale funzioni possibili. In effetti, a differenza di quanto è stato descritto nel 2012, un consumer di una funzione non è necessario conoscere se viene, infatti, implementato come un coroutine affatto.

In questo modo, il / await opzione del compilatore richiede inoltre l'opzione /Zi anziché l'opzione /ZI predefinita per disabilitare la funzionalità Modifica e continuazione del debugger. Inoltre, è necessario disabilitare i controlli SDL con la /sdl-option ed evitare le opzioni /RTC come controlli di runtime del compilatore non sono compatibili con coroutine. Tutte queste limitazioni sono temporanei e la natura sperimentale dell'implementazione e quello previsto per essere eseguito il lift nei prossimi aggiornamenti al compilatore. È tuttavia tutti importante, come può vedere nella Figura 3. È ovvio e univoco molto più semplice scrivere e più facile da comprendere di quello richiesto per l'operazione di copia implementata con ritardo. Infatti, sembra molto simile a quello dell'esempio originale sincrono in Figura 1. Non è inoltre necessario in questo caso per il WriteAsync futuri per restituire un valore specifico.

Operazione di copia nella figura 3 all'interno di funzione può essere ripristinato

future<void> Copy()
{
  File file = // Open file
  Network network = // Open connection
  uint8_t buffer[4096];
  while (unsigned copied = await file.ReadAsync(buffer, sizeof(buffer)))
  {
    await network.WriteAsync(buffer, copied);
  }
}

La parola chiave await utilizzata in Figura 3, nonché le altre nuove parole chiave fornite dall'opzione del compilatore /await può trovarsi solo all'interno di una funzione può essere ripristinata, pertanto la funzione copia circostante che restituisce un futuro. Utilizza gli stessi metodi ReadAsync e WriteAsync dell'esempio precedente di ritardo, ma è importante tenere presente che il compilatore non conosce future. In effetti, che possono non essere futures affatto. Come funziona? Beh, non funzionerà a meno che non vengono scritte alcune funzioni di adattatore per fornire al compilatore con le associazioni necessarie. Questo è analogo alla modalità di figure compilatore su come collegare un intervallo di istruzione for basata cercando adatto iniziano e terminano le funzioni. Nel caso di un await espressione, anziché alla ricerca di inizio e fine, il compilatore cerca adatte funzioni denominate await_ready, await_suspend e await_resume. Come iniziare e terminare, queste nuove funzioni possono essere funzioni membro o le funzioni disponibili. La possibilità di scrivere funzioni non membro è estremamente utile come è quindi possibile scrivere gli adattatori per i tipi esistenti che forniscono la semantica necessaria, come avviene con la versione futures futuristica esplorato finora. Figura 4 fornisce una serie di schede che è in grado di soddisfare interpretazione del compilatore di una funzione può essere ripristinata Figura 3.

Figura 4 Await schede per un ipotetico futuro

namespace std
{
  template <typename T>
  bool await_ready(future<T> const & t)
  {
    return t.is_done();
  }
  template <typename T, typename F>
  void await_suspend(future<T> const & t, F resume)
  {
    t.then([=](future<T> const &)
    {
      resume();
    });
  }
  template <typename T>
  T await_resume(future<T> const & t)
  {
    return t.get();
  }
}

Anche in questo caso, tenere presente che il modello di classe future della libreria Standard C++ non fornisce ancora un metodo "then" per aggiungere una continuazione, ma questo è tutto ciò che sarebbero necessari per l'utilizzo in questo esempio con il compilatore di oggi. La parola chiave await all'interno di una funzione può essere ripristinata in modo efficace viene impostato un potenziale punto di sospensione in esecuzione potrebbe lasciare la funzione se l'operazione non è ancora completa. Se await_ready restituisce true, non viene sospesa l'esecuzione e viene chiamato immediatamente await_resume per ottenere il risultato. Se, d'altra parte, await_ready restituisce false, viene chiamato await_suspend, consentendo l'operazione registrare una funzione di ripristino fornito dal compilatore da chiamare al completamento di un'eventuale. Non appena viene chiamato tale funzione di ripristino, le coroutine riprendere al punto precedente la sospensione e l'esecuzione continua a quella successiva await espressione o la chiusura della funzione.

Tenere presente che si verifica la ripresa in qualsiasi thread ha chiamato la funzione di ripristino del compilatore. Ciò significa che è possibile che una funzione può essere ripristinata può essere creato su un singolo thread e poi riprenderla in seguito e continuare l'esecuzione in un altro thread. Si tratta in realtà consigliabile dal punto di vista delle prestazioni, come l'alternativa comporterebbe l'invio alla ripresa di un altro thread, è spesso costosi e non necessarie. D'altra parte, potrebbe essere casi sarebbero auspicabile e necessaria anche qualsiasi codice successivo necessario affinità dei thread, come nel caso di gran parte del codice di grafica. Sfortunatamente, la parola chiave await non dispone ancora di un modo per consentire l'autore di un'espressione await fornire tali un suggerimento al compilatore. Non è senza precedenti. Il Runtime di concorrenza dispone di un'opzione di questo tipo, ma è interessante notare che lo stesso linguaggio C++ fornisce un modello che è possibile utilizzare:

int * p = new int(1);
// Or
int * p = new (nothrow) int(1);

Nello stesso modo, l'espressione await richiede un meccanismo per fornire un suggerimento per la funzione await_suspend per influire sul contesto del thread in cui si verifica il ripristino:

await network.WriteAsync(buffer, copied);
// Or
await (same_thread) network.WriteAsync(buffer, copied);

Per impostazione predefinita, il ripristino si verifica nel modo più efficiente possibile per l'operazione. La costante same_thread di qualche tipo ipotetico std::same_thread_t sarebbe ambiguità tra gli overload della funzione await_suspend. Await_suspend in Figura 3 sarebbe il predefinito e l'opzione più efficiente, poiché verrebbe presumibilmente riprendere in un thread di lavoro e completare senza un ulteriore cambio di contesto. L'overload same_thread illustrato nella Figura 5 potrebbe essere richiesta quando il consumer richiede l'affinità di thread.

Figura 5 ipotetico await_suspend Overload

template <typename T, typename F>
void await_suspend(future<T> const & t, F resume, same_thread_t const &)
{
  ComPtr<IContextCallback> context;
  check(CoGetObjectContext(__uuidof(context),
    reinterpret_cast<void **>(set(context))));
  t.then([=](future<T> const &)
  {
    ComCallData data = {};
    data.pUserDefined = resume.to_address();
    check(context->ContextCallback([](ComCallData * data)
    {
      F::from_address(data->pUserDefined)();
      return S_OK;
    },
    &data,
    IID_ICallbackWithNoReentrancyToApplicationSTA,
    5,
    nullptr));
  });
}

Questo overload recupera l'interfaccia IContextCallback per il thread chiamante (o apartment). La continuazione quindi chiama la funzione di ripristino del compilatore da questo stesso contesto. In tal caso essere STA dell'applicazione, l'applicazione potrebbe continuare Fortunatamente interazione con altri servizi con affinità di thread. ComPtr classe modello e controllo funzione di supporto sono parte della libreria di moderni, è possibile scaricare da github.com/kennykerr/modern, ma è anche possibile utilizzare qualsiasi possibile avere a disposizione.

Ho trattato molti argomenti, alcuni dei quali continua a essere abbastanza teorica, ma il compilatore Visual C++ fornisce già tutto il lavoro sporco a tale scopo. È un momento interessante per gli sviluppatori C++ interessati a concorrenza e spero che si sarà andiamo a nuovamente il mese prossimo approfondimenti di funzioni può essere ripristinate con Visual C++.


Kenny Kerrè un programmatore di computer basato su in Canada, nonché un autore per Pluralsight e Microsoft MVP. Partecipa al blog kennykerr.ca e seguirlo su Twitter @kennykerr.

Grazie all'esperto tecnico Microsoft seguente per la revisione di questo articolo: Gor Nishanov