lunes, 18 de noviembre de 2013

Procesos asíncronos con Task (TPL)

Para ejecutar procesos largos de forma asíncrona, la recomendación actual es que nos apoyemos en las TPL (Tasks Process Library).
Estas librerías aparecieron en el Framework 4 y nos van a facilitar la tarea de ejecutar código en paralelo u multiproceso, aunque en determinados casos nos puede complicar algo la existencia

Veamos su uso con varios ejemplos.

TAREAS


Tenemos este proceso laaaargo que queremos ejecutar asíncronamente.
Para ejecutarlo con las TPL, podemos hacerlo de esta forma:

Console.WriteLine("Empezamos task con código");
string frase="Hello world!!!";
// Creamos una tarea que contiene una función y devuelve un string
var task = new Task<string>(() => {
// Generamos un aleatorio entre 1000 y 4000
int tiempo = new Random(DateTime.Now.Millisecond).Next(1000, 4000);
Thread.Sleep(tiempo);
string texto2 = frase.ToUpper();
return texto2;
});
// Ejecutamos la tarea
task.Start();
// cuando acabe la tarea, que imprima el resultado en consola
task.ContinueWith(tareaAnterior => Console.WriteLine(tareaAnterior.Result));
Console.WriteLine("Hilo principal acabado");
Console.ReadKey();


Si el proceso largo lo tenemos en una función
  
private static string ProcesoLargo(string texto)
{
int tiempo = new Random().Next(1000, 6000);
Thread.Sleep(tiempo);
string texto2 = texto.ToUpper();
return texto2;
}


, lo llamaremos de esta otra forma.

Console.WriteLine("Empezamos task con lambda y start");
var task = new Task<string>(() => ProcesoLargo("Hello world!!!"));
task.Start();
task.ContinueWith(tareaAnterior => Console.WriteLine(tareaAnterior.Result));
Console.WriteLine("Hilo principal acabado");
Console.ReadKey();

En vez de preparar la tarea y empezarla con task.start, podemos empezarla directamente usando Task.Factory.StartNew

Console.WriteLine("Empezamos task con lambda y startNew");
var task = Task.Factory.StartNew(() => ProcesoLargo("Hello world!!!"));
task.ContinueWith(tareaAnterior => Console.WriteLine(tareaAnterior.Result));
Console.WriteLine("Hilo principal acabado");
Console.ReadKey();


LISTAS DE TAREAS Y SEMAFOROS 

Si tenemos varias tareas, podemos lanzarlas en paralelo mediante un array de tasks, y luego parar el proceso principal hasta que acabe una o todas las tareas que habían en el array.

Console.WriteLine("Empezamos array de task usando WaitAny y WaitAll");
Task<string>[] lista = new Task<string>[]
{
    Task.Factory.StartNew(() => ProcesoLargo("Hello, world 1 !!!")),
    Task.Factory.StartNew(() => ProcesoLargo("Hello, world 2 !!!")),
    Task.Factory.StartNew(() => ProcesoLargo("Hello, world 3 !!!")),
    Task.Factory.StartNew(() => ProcesoLargo("Hello, world 4 !!!"))
};

// El flujo se para hasta que alguna tarea se acabe.
// WaitAny nos devuelve el índice de la tarea que ha acabado.
var i= Task.WaitAny(lista);
Console.WriteLine("Ha acabado la tarea {0} con el resultado {1}", i, lista[i].Result);


// El flujo se para hasta que todas las tareas se acaben
Task.WaitAll(lista);
Console.WriteLine("Han acabado las {0} tareas", lista.Count());

Console.WriteLine("Hilo principal acabado");
Console.ReadKey();
Si no queremos esperarnos a que acaben las tareas, podemos también lanzar otra función asíncrona cuando acaben las tareas.

Console.WriteLine("Empezamos array de task usando ContinueWhenAny y ContinueWhenAll");
Task<string>[] lista = new Task<string>[]
{
    Task.Factory.StartNew(() => ProcesoLargo("Hello, world 1 !!!")),
    Task.Factory.StartNew(() => ProcesoLargo("Hello, world 2 !!!")),
    Task.Factory.StartNew(() => ProcesoLargo("Hello, world 3 !!!")),
    Task.Factory.StartNew(() => ProcesoLargo("Hello, world 4 !!!"))
};

// El flujo sigue, y ejecuta la función cuando acabe alguna tarea
Task.Factory.ContinueWhenAny(lista, (tareaAcabada) => { Console.WriteLine("Se ha acabado la tarea {0}",tareaAcabada.Result); });

// El flujo sigue, y ejecuta la función cuando acaben todas las tareas.
Task.Factory.ContinueWhenAll(lista, (listaTareas) => { Console.WriteLine("Se han acabado las {0} tareas", listaTareas.Count()); });

Console.WriteLine("Hilo principal acabado");
Console.ReadKey();
CANCELACIONES 

Para realizar cancelaciones, modificaremos el proceso largo para que acepte una Cancellation Token por parámetro. En el ejemplo, lo que haremos es que cuando acabe la primera tarea, cancelamos el resto de tareas. Para ello creamos un nuevo proceso largo con Cancellation.

private static string ProcesoLargoConCancel(string texto, CancellationToken cancel)
{
    // Generamos un aleatorio
    int tiempo = new Random(DateTime.Now.Millisecond).Next(4000, 6000);
    DateTime horaExit = DateTime.Now.AddMilliseconds(tiempo);
    try
    {

        cancel.ThrowIfCancellationRequested();
        while (horaExit < DateTime.Now); 
        return texto.ToUpper();
    }
    
    catch (OperationCanceledException ex)
    {
        Console.WriteLine("Tarea cancelada");
    }

    return null;
}

 Console.WriteLine("Empezamos array con cancelación");
 CancellationTokenSource cancel = new CancellationTokenSource();

 Task<string>[] lista = new Task<string>[]
 {
     new Task<string>(()=>ProcesoLargoConCancel("Hello, world 1 !!!", cancel.Token)),
     new Task<string>(()=>ProcesoLargoConCancel("Hello, world 2 !!!", cancel.Token)),
     new Task<string>(()=>ProcesoLargoConCancel("Hello, world 3 !!!", cancel.Token)),
     new Task<string>(()=>ProcesoLargoConCancel("Hello, world 4 !!!", cancel.Token))
 };

 Task.Factory.ContinueWhenAny(lista, (tareaAcabada) => {
     cancel.Cancel();
     Console.WriteLine("Se ha acabado la tarea {0}", tareaAcabada.Result); 
 });

 lista.ToList().ForEach(x => x.Start());

 Console.WriteLine("Hilo principal acabado");
 Console.ReadKey();

PROGRESO 

Si queremos controlar el progreso de un proceso, nos suscribimos al evento ProgressChanged de IProgress con una función, y en medio de la tarea vamos llamando al método Report para ir notificando.

Console.WriteLine("Empezamos hilo con porcentaje");
Progress<int> p = new Progress<int>();

p.ProgressChanged += (sender, porcentaje) =>
{
    Console.Write("{0}%.......", porcentaje);
};

Task tarea = Task.Factory.StartNew(() =>
{
    for (int i = 0; i <= 100; i++)
    {
        Thread.Sleep(60);
        ((IProgress<int>)p).Report(i);
    }
});

Console.WriteLine("Hilo principal acabado");
Console.ReadKey();
Sincronización de procesos. 

Ahora vamos con asincronía para winforms. Tenemos un botón y un label. Hacemos un proceso de esperar 2 segundos, y luego cambiar el label. Escribimos en el evento click del ratón el siguiente código:

private void button1_Click(object sender, EventArgs e)
{
    var t= Task.Factory.StartNew(()=>Thread.Sleep(2000));
    t.ContinueWith(tarea => label1.Text = "Hello, world!!!");
}
Esto nos va a dar un bonito System.InvalidOperationException.
  Operación no válida a través de subprocesos: Se tuvo acceso al control 'label1' desde un subproceso distinto a aquel en que lo creó.

 ¿Por qué?
Porque el código que se llama asíncronamente no está en el mismo proceso y no puede acceder al proceso donde está el formulario.
 Si cambiamos el label con un lambda y la función Invoke del formulario, el proceso lo ejecutará el formulario cuando acabe la tarea.

private void button1_Click(object sender, EventArgs e)
{
    var t= Task.Factory.StartNew(()=>Thread.Sleep(2000));
    t.ContinueWith((taskPrevia)=>
        this.Invoke((MethodInvoker)(() =>
        {
            label1.Text = "Hello, world!!!";
        })));
}
Otra aproximación podría ser crear una función para cambiar el label de forma síncrona o asíncrona, y que sea llamada desde el método anterior.

private void button1_Click(object sender, EventArgs e)
{
    var t = Task.Factory.StartNew(() => Thread.Sleep(2000));
    t.ContinueWith((taskprevia)=>CambiarLabel("Hello, world!!!"));
}
private void CambiarLabel(string texto)
{
    if (InvokeRequired)
        this.Invoke((MethodInvoker)(()=>label1.Text=texto));
    else
        label1.Text=texto;
}
Como dije al principio, todos estos ejemplos se aplican a partir del Framework 4. En el Framework 4.5 se incorporan las instrucciones Async, Await, whenAll, whenAny, ...  que simplifican un poco la llamada a funciones asíncronas.

Comentaré estas nuevas instrucciones en un segundo post.

Saludos.



lunes, 28 de octubre de 2013

Convertir consultas SQL a listas

En esta entrada voy a mostraros algunas de las formas que tengo para extraer datos de una fuente de datos SQL usando ADO.Net sin usar Entity Framework ni Linq2SQL.

Convertir un Dataset a Lista de objetos

 En este caso, tenemos un antiguo repositorio que nos devuelva un dataset a partir de un SQL. Podemos usar este método para convertirlo a un Enumerable y así poder adaptarlo a nuestras clases.

La clase que devuelve un DataSet

   public class DL_Datos  
   {  
     string connectionString="Persist Security Info=False;Integrated Security=TRUE;database=NorthWind;server=srvdesarrollo;Connect Timeout=60";  
     public DataSet DameDatos(string consulta)  
     {  
       SqlConnection conn = new SqlConnection(connectionString);  
       SqlDataAdapter da = new SqlDataAdapter(consulta, conn);  
       DataSet ds = new DataSet();  
       da.Fill(ds);  
       return ds;  
     }  
   }  

Nuestra clase

    public class Empleado
    {
        public int codigo { get; set; }
        public string nombre { get; set; }
        public string ciudad { get; set; }
        public string pais { get; set; }
    };

La conversión

        DL_Datos Sqldatos = new DL_Datos();  
       DataSet ds = Sqldatos.DameDatos("select EmployeeID, FirstName, LastName, City, Country from employees");  
       List<Empleado> empleados = ds.Tables[0]  
                     .AsEnumerable()  
                     .Select(dr => new Empleado()  
                     {  
                       codigo = dr.Field<int>("EmployeeID"),  
                       nombre = dr.Field<string>("FirstName") + " " + dr.Field<string>("LastName"),  
                       ciudad = dr.Field<string>("City"),  
                       pais = dr.Field<string>("Country")  
                     })  
                     .ToList();  

Convertir una consulta SQL a una lista de objetos

Otra forma de crear una lista directamente es crearnos un método extensor del Command que nos devuelva ya la lista montada

El método extensor

       public static void Fill<T>(this IDbCommand cmd, IList<T> list, Func<IDataReader, T> rowConverter)  
       {  
         using (var reader = cmd.ExecuteReader())  
         {  
           while (reader.Read())  
           {  
             list.Add(rowConverter(reader));  
           }  
         }  
       }  

Lo utilizaremos de esta manera:

       string connectionString = "Persist Security Info=False;Integrated Security=TRUE;database=NorthWind;server=srvdesarrollo;Connect Timeout=60";  
       using (SqlConnection conn = new SqlConnection(connectionString))  
       {  
         conn.Open();  
         SqlCommand comm = new SqlCommand("select EmployeeID, FirstName, LastName, City, Country from employees", conn);  
         List<Empleado> empleados = new List<Empleado>();  
         comm.Fill(empleados, r => new Empleado()  
         {  
           codigo = r.GetInt32(r.GetOrdinal("EmployeeID")),  
           nombre = r.GetString(r.GetOrdinal("FirstName")) + " " + r.GetString(r.GetOrdinal("LastName")),  
           ciudad = r.GetString(r.GetOrdinal("City")),  
           pais = r.GetString(r.GetOrdinal("Country"))  
         });  
         conn.Close();  
       }  

Convertir un DataSet a un Dictionary

       DL_Datos Sqldatos = new DL_Datos();  
       DataSet ds = Sqldatos.DameDatos("select regionID, regionDescription from Region");  
       Dictionary<int, string> dict = ds.Tables[0]  
           .AsEnumerable()  
           .ToDictionary(  
               x => x.Field<int>("regionID"),   
               x => x.Field<string>("regionDescription")  
               );  
Saludos.







lunes, 21 de octubre de 2013

Comparador de datos en Visual Studio 2012

En Visual Studio 2010 teníamos la comparación de esquemas y la comparación de datos.




El Visual Studio 2012 solamente viene con el comparador de esquemas.



El comparador de datos para Visual Studio 2012 viene con las Sql Server Data Tools.

Lo podéis bajar de este enlace.

http://msdn.microsoft.com/en-us/jj650015

Ojo, si tienes el Visual Studio 2012 en Español, como bien se indica en la página, debes bajarte la versión en español y ejecutarla en modo Administrador. Si no se hace de esta forma, dará un error al instalar.

Saludos.