Esiste un modo per utilizzare la Task Parallel Library (TPL) con SQLDataReader?

Mi piace la semplicità dei metodi di estensione Parallel.For e Parallel.ForEach in TPL. Mi stavo chiedendo se ci fosse un modo per sfruttare qualcosa di simile o anche con i Task leggermente più avanzati.

Di seguito è riportato un utilizzo tipico di SqlDataReader e mi chiedevo se fosse ansible e in tal caso come sostituire il ciclo while in basso con qualcosa nel TPL. Poiché il lettore non è in grado di fornire un numero fisso di iterazioni, non è ansible il metodo di estensione For, che si occupa delle attività che raccoglierei. Speravo che qualcuno potesse averlo già affrontato e aver elaborato alcune cose da fare e da non fare con ADO.net.

using (SqlConnection conn = new SqlConnection("myConnString")) using (SqlCommand comm = new SqlCommand("myQuery", conn)) { conn.Open(); SqlDataReader reader = comm.ExecuteReader(); if (reader.HasRows) { while (reader.Read()) { // Do something with Reader } } } 

Ci sei quasi. Avvolgi il codice inserito in una funzione con questa firma:

 IEnumerable MyQuery() 

e quindi sostituisci // Do something with Reader codice // Do something with Reader con questo:

 yield return reader; 

Ora hai qualcosa che funziona in un singolo thread. Sfortunatamente, mentre si leggono i risultati della query, viene restituito ogni volta un riferimento allo stesso object e l’object si muta per ogni iterazione. Ciò significa che se si tenta di eseguirlo in parallelo si ottengono risultati davvero strani poiché le letture parallele mutano l’object utilizzato in thread diversi. Hai bisogno del codice per prendere una copia del record da inviare al tuo loop parallelo.

A questo punto, però, quello che mi piace fare è saltare la copia extra del record e andare direttamente a una class fortemente tipizzata. Inoltre, mi piace usare un metodo generico per farlo:

 IEnumerable GetData(Func factory, string sql, Action addParameters) { using (var cn = new SqlConnection("My connection string")) using (var cmd = new SqlCommand(sql, cn)) { addParameters(cmd.Parameters); cn.Open(); using (var rdr = cmd.ExecuteReader()) { while (rdr.Read()) { yield return factory(rdr); } } } } 

Supponendo che i metodi factory creino una copia come previsto, questo codice dovrebbe essere sicuro da utilizzare in un ciclo Parallel.ForEach. La chiamata del metodo sarebbe simile a questa (presupponendo una class Employee con un metodo factory statico denominato “Create”):

 var UnderPaid = GetData(Employee.Create, "SELECT * FROM Employee WHERE AnnualSalary <= @MinSalary", p => { p.Add("@MinSalary", SqlDbType.Int).Value = 50000; }); Parallel.ForEach(UnderPaid, e => e.GiveRaise()); 

Aggiornamento importante:
Non sono così sicuro di questo codice come lo ero una volta. Un thread separato potrebbe ancora mutare il lettore mentre un altro thread sta eseguendo la sua copia. Potrei mettere un blocco su questo, ma sono anche preoccupato che un altro thread possa chiamare aggiornare il lettore dopo che l’originale si è chiamato Read () ma prima che inizi a fare la copia. Pertanto, la sezione critica qui consiste dell’intero ciclo while … e a questo punto, si torna di nuovo a thread singolo. Mi aspetto che ci sia un modo per modificare questo codice per funzionare come previsto per gli scenari multi-thread, ma avrà bisogno di ulteriori studi.

Avrai difficoltà a sostituirlo direttamente con il ciclo. SqlDataReader non è una class thread-safe, quindi non è ansible utilizzarla direttamente da più thread.

Detto questo, potresti potenzialmente elaborare i dati che hai letto usando il TPL. Ci sono alcune opzioni, qui. Il più semplice potrebbe essere quello di rendere la tua implementazione IEnumerable che funziona sul lettore e restituisce una class o una struttura contenente i tuoi dati. È quindi ansible utilizzare PLINQ o un’istruzione Parallel.ForEach per elaborare i dati in parallelo:

 public IEnumerable ReadData() { using (SqlConnection conn = new SqlConnection("myConnString")) using (SqlCommand comm = new SqlCommand("myQuery", conn)) { conn.Open(); SqlDataReader reader = comm.ExecuteReader(); if (reader.HasRows) { while (reader.Read()) { yield return new MyDataClass(... data from reader ...); } } } } 

Una volta che hai questo metodo, puoi elaborarlo direttamente, tramite PLINQ o TPL:

 Parallel.ForEach(this.ReadData(), data => { // Use the data here... }); 

O:

 this.ReadData().AsParallel().ForAll(data => { // Use the data here... });