2009-04-29 11 views
33

Contesto: ho un sacco di stringhe che sto ricevendo da un database e voglio restituirle. Tradizionalmente, sarebbe qualcosa di simile:C# IEnumerator/struttura di rendimento potenzialmente negativa?

public List<string> GetStuff(string connectionString) 
{ 
    List<string> categoryList = new List<string>(); 
    using (SqlConnection sqlConnection = new SqlConnection(connectionString)) 
    { 
     string commandText = "GetStuff"; 
     using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection)) 
     { 
      sqlCommand.CommandType = CommandType.StoredProcedure; 

      sqlConnection.Open(); 
      SqlDataReader sqlDataReader = sqlCommand.ExecuteReader(); 
      while (sqlDataReader.Read()) 
      { 
       categoryList.Add(sqlDataReader["myImportantColumn"].ToString()); 
      } 
     } 
    } 
    return categoryList; 
} 

Ma poi ho capire il consumatore sta andando a voler scorrere gli elementi e non si preoccupa molto altro, e mi piacerebbe non me stesso in scatola ad una lista, di per sé, quindi se restituisco un IEnumerable tutto è buono/flessibile. Stavo pensando che potrei usare un "ritorno rendimento" tipo di progetto per gestire questa situazione ... qualcosa di simile:

public IEnumerable<string> GetStuff(string connectionString) 
{ 
    using (SqlConnection sqlConnection = new SqlConnection(connectionString)) 
    { 
     string commandText = "GetStuff"; 
     using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection)) 
     { 
      sqlCommand.CommandType = CommandType.StoredProcedure; 

      sqlConnection.Open(); 
      SqlDataReader sqlDataReader = sqlCommand.ExecuteReader(); 
      while (sqlDataReader.Read()) 
      { 
       yield return sqlDataReader["myImportantColumn"].ToString(); 
      } 
     } 
    } 
} 

Ma ora che sto leggendo un po 'di più su rendimento (su siti come questo .. .msdn non sembra menzionarlo), è apparentemente un pigro valutatore, che mantiene lo stato del populatore in giro, in previsione di qualcuno che chiede il valore successivo, e quindi lo esegue solo finché non restituisce il valore successivo.

Questo sembra buono nella maggior parte dei casi, ma con una chiamata DB, questo suona un po 'rischioso. Come un esempio un po 'forzato, se qualcuno chiede un IEnumerable da quello che sto compilando da una chiamata DB, ne riceve metà e poi si blocca in un loop ... per quanto posso vedere la mia connessione DB sta andando rimanere aperti per sempre.

Suona come chiedere guai in alcuni casi se l'iteratore non finisce ... mi manca qualcosa?

+0

Grazie per la modifica, Jon ... questo è quello che ho per la digitazione al volo. – Beska

+1

Finché il consumatore chiama 'Dispose' su IEnumerator, sei al sicuro. Vedi il mio post qui sotto. – tofi9

risposta

44

È un atto di bilanciamento: si desidera forzare immediatamente tutti i dati nella memoria in modo da poter liberare la connessione, o si desidera trarre vantaggio dallo streaming dei dati, al costo di legare la connessione per tutto ciò tempo?

Il modo in cui lo guardo, quella decisione dovrebbe potenzialmente essere all'altezza del chiamante, che sa di più su ciò che vogliono fare. Se si scrive il codice utilizzando un blocco iteratore, il chiamante può molto facilmente trasformato quella forma in streaming in una forma completamente tamponata:

List<string> stuff = new List<string>(GetStuff(connectionString)); 

Se, d'altra parte, si fa il buffer da soli, non c'è in modo che il chiamante possa tornare a un modello di streaming.

Quindi probabilmente utilizzerei il modello di streaming e dico esplicitamente nella documentazione che cosa fa e consiglio al chiamante di decidere in modo appropriato. Potresti anche voler fornire un metodo di supporto per chiamare fondamentalmente la versione in streaming e convertirla in un elenco.

Ovviamente, se non ti fidi dei tuoi chiamanti a prendere la decisione appropriata, e hai buone ragioni per credere che non vorranno mai veramente trasmettere i dati (ad esempio, non restituirà comunque molto) quindi andare per l'approccio elenco. In ogni caso, documentalo: potrebbe davvero influire sul modo in cui viene utilizzato il valore di ritorno.

Un'altra opzione per gestire grandi quantità di dati consiste nell'utilizzare i batch, ovviamente - questo sta pensando in qualche modo lontano dalla domanda originale, ma è un approccio diverso da considerare nella situazione in cui lo streaming sarebbe normalmente attraente.

+0

La scelta delineata è vera, ma penso che si debba dare più peso al default della decisione di NON streaming. Lasciare connessioni o risorse impegnate porterà a problemi di scalabilità. Il comportamento predefinito dovrebbe essere sano e non causare problemi. –

8

Non ti manca nulla. Il tuo esempio mostra come NON usare il rendimento restituito. Aggiungere gli articoli a un elenco, chiudere la connessione e restituire l'elenco. La firma del metodo può ancora restituire IEnumerable.

Modifica: Detto questo, Jon ha un punto (così sorpreso!): Ci sono rare occasioni in cui lo streaming è in realtà la cosa migliore da fare dal punto di vista delle prestazioni. Dopo tutto, se si tratta di 100.000 (1.000.000? 10.000.000?) Righe di cui stiamo parlando qui, non si vuole caricare prima tutto in memoria.

+1

Sì ... Stavo solo mettendo in risalto l'aspetto IEnumerable perché è quello che mi ha fatto pensare di usare yield in primo luogo. E grazie per la risposta ... felice di vedere che non sto abbaiando completamente dall'albero sbagliato. – Beska

+0

Nessun problema amico, felice di aver aiutato. Se questo ha risposto alla tua domanda, non dimenticare di contrassegnarla come risposta in modo che rimuova l'elenco delle domande senza risposta. –

+0

Oh, quasi sempre segna le mie domande come risposta ... ma mi piacerebbe conservare questo per un po ', dato che Jon ha valutato con un punto di vista leggermente diverso, e mi piacerebbe vedere come funziona su. – Beska

1

No, siete sulla strada giusta ... la resa si blocca il lettore ... potete provarlo a fare un'altra chiamata database mentre chiamando il IEnumerable

+0

Abilita MARS nella stringa di connessione per consentire più SqlDataReader aperti a un risultato di prestazioni. Ma ancora, questo schema ha problemi. – spoulson

-2

Non rendimento uso qui. il tuo campione va bene.

+0

Eh? Cosa c'era di sbagliato in questa risposta? –

0

Mi sono imbattuto in questo muro alcune volte. Le query del database SQL non sono facilmente eseguibili come i file. Invece, interrogare solo quanto si ritiene necessario e restituirlo come qualsiasi contenitore desiderato (IList<>, DataTable, ecc.). IEnumerable non ti aiuterà qui.

-1

Quello che puoi fare è utilizzare un SqlDataAdapter e compilare un DataTable. Qualcosa di simile a questo:

public IEnumerable<string> GetStuff(string connectionString) 
{ 
    DataTable table = new DataTable(); 
    using (SqlConnection sqlConnection = new SqlConnection(connectionString)) 
    { 
     string commandText = "GetStuff"; 
     using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection)) 
     { 
      sqlCommand.CommandType = CommandType.StoredProcedure; 
      SqlDataAdapter dataAdapter = new SqlDataAdapter(sqlCommand); 
      dataAdapter.Fill(table); 
     } 

    } 
    foreach(DataRow row in table.Rows) 
    { 
     yield return row["myImportantColumn"].ToString(); 
    } 
} 

In questo modo, si sta interrogando tutto in un solo colpo, e chiudendo immediatamente la connessione, ma si sta ancora pigramente iterare il risultato. Inoltre, il chiamante di questo metodo non può trasmettere il risultato a un elenco e fare qualcosa che non dovrebbe fare.

+3

Non capisco quale sia lo scopo di "pigramente ripetendo il risultato" in questo esempio. – mquander

+0

Penso che il punto sia che l'OP non sarà legato a Lista <> (che è il motivo per cui è andato con l'approccio di rendimento in primo luogo), ma allo stesso tempo non manterrà aperta la connessione al database. – Andy

+0

Bene, con entrambi gli approcci non ho bisogno di essere legato alla lista <>; Potrei restituire IEnumerable <> in entrambi i casi. Stavo solo pensando di andare verso qualcosa di più generico di List <>, e questo è ciò che mi ha fatto pensare al rendimento e alle potenziali conseguenze di esso. – Beska

10

Non sei sempre insicuro con IEnumerable. Se lasci il framework call GetEnumerator (che è quello che farà la maggior parte delle persone), allora sei al sicuro. In sostanza, sei sicuro come la professionalità e cura del codice utilizzando il metodo:

class Program 
{ 
    static void Main(string[] args) 
    { 
     // safe 
     var firstOnly = GetList().First(); 

     // safe 
     foreach (var item in GetList()) 
     { 
      if(item == "2") 
       break; 
     } 

     // safe 
     using (var enumerator = GetList().GetEnumerator()) 
     { 
      for (int i = 0; i < 2; i++) 
      { 
       enumerator.MoveNext(); 
      } 
     } 

     // unsafe 
     var enumerator2 = GetList().GetEnumerator(); 

     for (int i = 0; i < 2; i++) 
     { 
      enumerator2.MoveNext(); 
     } 
    } 

    static IEnumerable<string> GetList() 
    { 
     using (new Test()) 
     { 
      yield return "1"; 
      yield return "2"; 
      yield return "3"; 
     } 
    } 

} 

class Test : IDisposable 
{ 
    public void Dispose() 
    { 
     Console.WriteLine("dispose called"); 
    } 
} 

Sia che si può affort di lasciare la connessione al database aperto o non dipende dall'architettura pure. Se il chiamante partecipa a una transazione (e la connessione viene automaticamente inserita), la connessione verrà comunque mantenuta aperta dal framework.

Un altro vantaggio di yield è (quando si utilizza un cursore sul lato server), il codice non deve leggere tutti i dati (esempio: 1.000 elementi) dal database, se il consumatore desidera uscire dal ciclo in precedenza (esempio: dopo il decimo elemento). Questo può velocizzare i dati di query. Soprattutto in un ambiente Oracle, dove i cursori sul lato server sono il modo comune per recuperare i dati.

+3

+1 per i dettagli sullo smaltimento, ma non credo che fosse la preoccupazione - I * credo * Beska è preoccupato per alcune iterazioni del ciclo del chiamante che impiegano molto tempo per elaborare, lasciando aperta la connessione al database quando non lo fa davvero bisogno di –

+0

Grazie, aggiornato con la mia visione su come mantenere la connessione aperta. – tofi9

1

L'unico modo in cui ciò potrebbe causare problemi è se il chiamante abusa del protocollo di IEnumerable<T>. Il modo corretto per usarlo è chiamare Dispose su di esso quando non è più necessario.

L'implementazione generato dal yield return prende la chiamata Dispose come un segnale di eseguire qualsiasi finally blocchi aperti, che nel tuo esempio chiamerà Dispose sugli oggetti che hai creato nei using dichiarazioni.

Esistono numerose funzionalità linguistiche (in particolare foreach) che rendono molto semplice l'uso di IEnumerable<T> in modo corretto.

+0

Se si potesse inserire qualche documentazione su come Dispose viene usato da/all'interno di enumeratori implementati tramite le parole chiave return return, sarebbe utile. – jpierson

6

Per inciso - si noti che l'approccio IEnumerable<T> è essenzialmente ciò che i fornitori di LINQ (LINQ to SQL, LINQ to Entities) fanno per vivere. L'approccio ha vantaggi, come dice Jon. Tuttavia, ci sono anche problemi definiti - in particolare (per me) in termini di (la combinazione di) separazione | astrazione.

Quello che voglio dire è che:

  • in uno scenario MVC (per esempio) si desidera che il "ottenere i dati" passo per realmente ottenere dati, in modo che è possibile verificare che funziona al Controller, non il vista (senza dover ricordare di chiamare .ToList() ecc)
  • non è possibile garantire che un altro implementazione DAL sarà in grado per lo streaming di dati (ad esempio, una chiamata POX/WSE/sapone può siamo noi ually stream records); e non necessariamente vuole fare il comportamento confusamente differente (vale a dire il collegamento ancora aperta durante l'iterazione con una sola applicazione, e chiuso per un altro)

Questo si lega un po 'con i miei pensieri qui: Pragmatic LINQ.

Ma dovrei sottolineare - ci sono sicuramente momenti in cui lo streaming è altamente desiderabile. Non è una semplice cosa "sempre contro mai" ...

0

Si può sempre usare un thread separato per bufferizzare i dati (magari in coda) mentre si fa anche un yeild per restituire i dati. Quando l'utente richiede dati (restituiti tramite uno yeild), un oggetto viene rimosso dalla coda. I dati vengono aggiunti continuamente alla coda tramite il thread separato. In questo modo, se l'utente richiede i dati abbastanza velocemente, la coda non è mai piena e non ti devi preoccupare dei problemi di memoria. In caso contrario, la coda si riempirà, il che potrebbe non essere così grave. Se esiste una sorta di limitazione che si desidera imporre sulla memoria, è possibile imporre una dimensione massima della coda (a quel punto l'altro thread attenderà che gli elementi vengano rimossi prima di aggiungerne altri alla coda). Naturalmente, vorrai assicurarti di gestire le risorse (vale a dire la coda) correttamente tra i due thread.

In alternativa, è possibile forzare l'utente a passare in un valore booleano per indicare se i dati devono essere memorizzati o meno. Se true, i dati vengono memorizzati nel buffer e la connessione viene chiusa il prima possibile. Se false, i dati non vengono memorizzati nel buffer e la connessione al database rimane aperta finché l'utente ne ha bisogno. Avere un parametro booleano costringe l'utente a fare la scelta, il che garantisce di conoscere il problema.

3

modo leggermente più conciso per forzare la valutazione di iteratore:

using System.Linq; 

//... 

var stuff = GetStuff(connectionString).ToList(); 
Problemi correlati