2013-02-14 21 views
45

Sono di fronte al problema della progettazione di metodi che eseguono l'I/O di rete (per una libreria riutilizzabile). Ho letto questa domandaScrivere un'API asincrona/non asincrona ben progettata

c# 5 await/async pattern in API design

e anche altri quelli più vicino al mio problema.

Quindi, la domanda è, se voglio fornire asincrono e non asincrono metodo come devo progettare questi?

Ad esempio, per esporre una versione non asincrona di un metodo, ho bisogno di fare qualcosa di simile

public void DoSomething() { 
    DoSomethingAsync(CancellationToken.None).Wait(); 
} 

e mi sento che non è un grande disegno. Vorrei un suggerimento (ad esempio) su come definire metodi privati ​​che possono essere inclusi in quelli pubblici per fornire entrambe le versioni.

+3

Domanda importante: come è implementata la parte asincrona di 'DoSomethingAsync'? Questo è importante perché se questo sta creando un 'Task' e un thread di lavoro, la risposta potrebbe essere molto diversa, ad esempio, se si sta utilizzando un'API eventing esterna. Nello specifico, non ha senso avere il metodo "sync" chiama il metodo "async" e "wait", se il metodo "async" sta prendendo una discussione: sarebbe meglio fare il lavoro direttamente. Tuttavia, questo cambia per altri significati di "async" (non tutti "async" significa "thread") –

+0

@MarcGravell, sto cercando di spiegare meglio (e non è semplice a causa dell'inglese non è la mia lingua principale). Ho "operazione XYZ" che esegue intensi I/O. Voglio esporlo in una libreria con due metodi: uno che ritorna quasi immediatamente usando un pattern asincrono (come descritto in _MS Pattern asincrono basato su Task doc_) e uno che esegue synchronous. Qual è il più ** efficiente **/** mantenibile **/** unit-test abilitato ** in questo modo. Un metodo privato esposto con un wrapper async pubblico e un altro wrapper di sincronizzazione, ad esempio? Se sì, come sono fatti questi metodi? – jay

+2

"efficiente" e "gestibile" non sono la stessa cosa e spesso non sono d'accordo l'uno con l'altro. E ancora: dipende tutto da come sta funzionando il tuo codice "asincrono". Se questo è un codice procedurale/lineare che stai semplicemente eseguendo su un thread separato, francamente non ha senso esporrlo tramite "async". Se quel codice è completamente asincrono, allora potrebbe essere meglio avere 2 implementazioni completamente separate, se si vuole "efficiente" come obiettivo. Ci dispiace, ma questa domanda è estremamente limitata al contesto e non hai dato molto contesto. –

risposta

47

Se si desidera l'opzione più gestibile, fornire solo un'API async, che viene implementata senza effettuare alcuna chiamata di blocco o utilizzando alcun thread di thread.

Se si desidera avere entrambe le API async e sincrone, si verificherà un problema di manutenibilità. Hai davvero bisogno di implementarlo due volte: una volta async e una volta sincrono. Entrambi i metodi sembreranno quasi identici, quindi l'implementazione iniziale è semplice, ma si finirà con due metodi quasi identici separati, quindi la manutenzione è problematica.

In particolare, non esiste un modo semplice e buono per creare semplicemente un "wrapper" async o sincrono. Stephen Toub ha le migliori informazioni sul tema:

  1. Should I expose asynchronous wrappers for synchronous methods?
  2. Should I expose synchronous wrappers for asynchronous methods?

(breve risposta ad entrambe le domande è "no")

+2

{+1} Questa era la cosa che avevo bisogno di sentire! Nel frattempo ho finito per riscrivere i metodi per essere veramente asincrona e senza preoccuparmi più della versione di sincronizzazione. Dopo che tutti i consumatori potrebbero chiamare DoSomethingAsync.Wait() ** dal loro codice ** se lo vogliono fare. Mi interessava la compatibilità Mono, ma il profilo 3.0 supporta le parole chiave 'async' /' await', quindi il problema non esiste. A proposito, ottimi articoli. – jay

+2

@jay andrò oltre il +1; Sono attualmente seduto in una sessione con Stephen Toub e questa risposta è la risposta ideale a una domanda molto complessa. Quindi sto andando fuori con un +500. –

+0

@MarcGravell: Grazie! Non sarebbe successo a registrare la sessione di Stephen, vero? :) Non mi avvicino quasi mai a quel lato del paese ... –

2

Sono d'accordo sia con Marc e Stephen (Cleary).

(A proposito, ho iniziato a scrivere questo come commento alla risposta di Stephen, ma si è rivelato troppo lungo: fatemi sapere se è OK scrivere come risposta o no, e sentitevi liberi di prendere le cose da esso e aggiungilo alla risposta di Stephen, nello spirito di "fornire la migliore risposta").

In realtà "dipende": come ha detto Marc, è importante sapere come DoSomethingAsync è asincrono. Siamo tutti d'accordo che non ha senso avere il metodo "sync" chiama il metodo "async" e "wait": questo può essere fatto nel codice utente. L'unico vantaggio di avere un metodo separato è quello di ottenere guadagni di prestazioni effettive, di avere un'implementazione che è, sotto la cappa, diversa e adattata allo scenario sincrono. Questo è particolarmente vero se il metodo "asincrono" sta creando un thread (o lo prende da un threadpool): si finisce con qualcosa che sotto utilizza due "control flow", mentre "promising" con i suoi look sincroni da eseguire nel contesto dei chiamanti. Questo potrebbe anche avere problemi di concorrenza, a seconda dell'implementazione.

Anche in altri casi, come l'I/O intensivo menzionato dall'OP, può valere la pena di avere due diverse implementazioni.La maggior parte dei sistemi operativi (Windows di sicuro) hanno meccanismi I/O diversi su misura per i due scenari: ad esempio, l'esecuzione asincrona e l'operazione I/O trae grandi vantaggi dai meccanismi del livello OS come le porte di completamento I/O, che aggiungono un po ' overhead (non significativo, ma non nullo) nel kernel (dopotutto, devono fare contabilità, distribuzione, ecc.) e un'implementazione più diretta per le operazioni sincrone. La complessità del codice varia molto, specialmente nelle funzioni in cui vengono eseguite/coordinate più operazioni.

Cosa farei è:

  • hanno alcuni esempi/prova per utilizzo tipico e scenari
  • vedere quale variante API utilizzata, dove e misura. Misura anche la differenza di prestazioni tra una variante "sincro puro" e "sincronizzazione". (non per l'intera API, ma per alcuni casi tipici selezionati)
  • in base alla misurazione, decidere se vale il costo aggiunto.

Questo principalmente perché due obiettivi sono in qualche modo in contrasto l'uno con l'altro. Se si desidera un codice gestibile, la scelta più ovvia è implementare la sincronizzazione in termini di asincrono/attesa (o viceversa) (o, meglio ancora, fornire solo la variante asincrona e lasciare che l'utente "attenda"); se vuoi prestazioni devi implementare le due funzioni in modo diverso, per sfruttare diversi meccanismi sottostanti (dal framework o dal sistema operativo). Penso che non dovrebbe fare la differenza da un punto di vista del test unitario su come implementare effettivamente la tua API.

+0

C'è un leggero sovraccarico per la programmazione asincrona, ma è tutto nel lato modalità utente. Il kernel di Windows è al 100% asincrono; non ci sono chiamate sincrone a livello di driver di periferica. –

+0

{+1} @ dema80 le tue risposte ad interessanti considerazioni sull'OS. Mi piacerebbe anche sentire cosa succede a basso livello in * nix/Mono (per I/O intensivi). Un'altra cosa che potrebbe essere aggiunta è relativa al mio ultimo commento. Il codice può essere fattorizzato e quindi riutilizzato per avere due implementazioni davvero diverse? Senza avvolgere e come hai detto affrontando entrambe le versioni. Questa non è una programmazione che dura tutto il giorno, ma piuttosto una domanda di progetto di struttura. Dopo tutto BCL ha un sacco di API con sincronizzazione/versione asincrona fianco a fianco. – jay

+0

@StephenCleary ovviamente, nessun kernel moderno bloccherà in modo sincrono: e dopo, a basso livello, I/O è un lavoro asincrono (interrupt, IPC, ...). Questo non è quello che intendevo, ho appena sottolineato che l'implementazione per le funzioni sincrone è probabile che sia più diretta: non sono sicuro di accadere tutto in modalità utente: penso che con I/O sincrono il thread costruisce l'IRP, memorizzarlo nello stack del dispositivo e attende nel kernel che l'IRP sia completato. Con I/O asincrono, è necessario APC o accodamento aggiuntivo. Ma poi, ho lavorato su questa roba nell'era di Windows 2000, le cose potrebbero essere cambiate! –