Creación de hilos con parámetros en C#
Muchos lenguajes de programación permiten la creación de hilos o threads en un programa. De forma resumida, los hilos son un mecanismo mediante el cual podemos devidir una aplicación en diferentes partes que se pueden ejecutar de forma paralela, existiendo mecanismos por los que pueden compartir información.
C# ofrece un mecanismo muy sencillo de implementar hilos, basado en la utilización de la clase Thread
. El constructor de esta clase recibe como parámetro el método o función que hay que ejecutar en paralelo. Este parámetro se indica mediante la utilización de un delegado, que es el mecanismo que, entre otras cosas, se utiliza en .NET para utilizar punteros a funciones de forma segura. La firma del delegado no incluye ningún parámetro, por lo que únicamente es posible crear hilos de forma directa sobre métodos y funciones que no requieran parámetros de entrada ni de salida. En los siguientes ejemplos muestro un caso sencillo de creación de un hilo y otro en el que explico una forma de poder crear un hilo con entrada y salida de parámetros.
En el siguiente ejemplo se dispone de una clase con dos métodos que muestran mensajes por pantalla. El objetivo es crear dos hilos, uno para cada uno de los métodos y ejecutarlos de forma paralela, de forma que podamos ver como resultado cómo se van intercalando los mensajes escritos por cada método.
using System; using System.IO; using System.Threading; public class Mensajes{ public void Mostrar1() { for(int i=0;i<10;i++){ Console.WriteLine("Escribiendo desde ==> 1"); Thread.Sleep(1000); } } public void Mostrar2() { for(int i=0;i<10;i++){ Console.WriteLine("Escribiendo desde ==> 2"); Thread.Sleep(1000); } } } public class Ejemplo{ public static void Main() { Mensajes msg = new Mensajes(); Thread th1 = new Thread(new ThreadStart(msg.Mostrar1)); Thread th2 = new Thread(new ThreadStart(msg.Mostrar2)); th1.Start(); th2.Start(); th1.Join(); th2.Join(); } } |
La creación de cada hilo se realiza mediante las líneas Thread th1 = new Thread(new ThreadStart(msg.Mostrar1));
. Esta línea indica que se crea una instancia de la clase Thread, con nombre th1, a partir de un delegado de la clase ThreadStart, que apunta al método Mostrar1 del objeto msg creado anteriormente.
Una vez creados los dos hilos hay que activarlos, para lo que se llama al método Start de cada uno de ellos. Tras este punto cada hilo se ejecuta en paralelo entre si, y con el programa principal, por lo que utilizamos el método Join de ambos hilos para esperar a que terminen los hilos antes de finalizar el programa.
El delegado ThreadStart no acepta parámetros de entrada ni de salida, por lo que si queremos crear un hilo sobre un método que los necesite, hay que utilizar algún mecanismo auxiliar. Una posible forma de conseguir esto es crear una nueva clase con los parámetros necesarios en la entrada y con un nuevo método sin parámetros que llame al método que queremos hacer paralelo, enviándole estos parámetros. A partir de aquí tendríamos que crear una instancia de dicha clase con los parámetros que queremos enviar al método original, y hacer que el hilo se ejecutase sobre el nuevo método de la clase. En el caso de que quisiéramos obtener el resultado de la ejecución, deberíamos crear una función que acepte como parámetro de entrada el tipo del valor devuelto por el método original, y hacer que la nueva clase creada disponga también de un delegado que indique la función a la que llamar tras la ejecución.
Como esto puede parecer un poco lioso, vamos a ver otro ejemplo. En esta ocasión disponemos de una clase de funciones matemáticas y queremos llamar de forma paralela a una de ellas. Este método acepta un valor entero en la entrada y devuelve otro entero.
using System; using System.Threading; using System.IO; public class EjemploMates{ public static int CalculoComplejo(int n) { // sumo uno y espero 5 segundos n = n+1; Thread.Sleep(5000); return n; } } public class HiloParaMates{ protected int n; protected MatesCallback callback = null; public HiloParaMates(int n, MatesCallback callback){ this.n = n; this.callback = callback; } public void CalculoComplejo() { int result = EjemploMates.CalculoComplejo(n); if(callback != null) callback(result); } } // creo un delegado con la firma necesaria para capturar // el valor devuelto por el método CalculoComplejo public delegate void MatesCallback(int n); public class Ejemplo{ public static void Main() { HiloParaMates hpm = new HiloParaMates(1000, new MatesCallback(ResultCallback)); Thread th = new Thread(new ThreadStart(hpm.CalculoComplejo)); th.Start(); th.Join(); } public static void ResultCallback(int n) { Console.WriteLine("Resultado de la operación: "+n); } } |
En el anterior código la clase HiloParaMates
es la que nos permite encapsular la llamada al método EjemploMates.Calcular
. Este método requiere un parámetro de tipo entero, por lo que la clase requiere este parámetro en su constructor. Además se requiere en el constructor otro parámetro más, un delegado MatesCallback, que acepta un entero en la entrada. La idea es que tras realizar el cálculo se llame al método que se indique proporcionándole el resultado.
Para hacer funcionar todo esto, en Main se crea una instancia de la clase HiloParaMates indicándole que queremos utilizar el valor numérico 1000 y que se llame al método (estático) ResultCallback cuando se obtenga el resultado. Para crear el hilo es suficiente con indicar que se quiere hacer sobre el método CalculoComplejo de la instancia hpm.
Conclusiones
Por supuesto todo lo anterior son únicamente ejemplos muy sencillos, ya que la programación de un sistema multihilo suele ser bastante compleja debido al indeterminismo implícito en la ejecución de múltiples hilos de ejecución, que normalmente comparten recursos o dependen unos de otros. En cualquier caso sí que nos permite apreciar la facilidad con la que se pueden crear estas estructuras en C#, olvidándonos de complejas sintaxis y librerías, y centrándonos únicamente en los requisitos de nuestro sistema.
Todo esto es muy bonito, pero muy complejo, por que no creas una nueva clase derivada de Thread, y sobreescribiendo el método Run.
Teniendo en cuenta que Thread es sealed, la opción de crear una clase derivada no parece muy viable.
Es cierto, pensaba que funcionaba como en Java o en Delphi, donde si se pude derivar de la clase de thread, e incluso en Java esta mejor ya que solo hay que implementar una interfaz.
En Net.2 se pude hacer un hilo al que se le pasa un parametro del tipo Object.
En vez de hacer todo eso se puede iniciar con una función que recibe un parametro del tipo Object, lo curioso es que para pasar un parametro al hilo en Net 1 tengas que hacer todas esas cosas cuando desde el API de windows a la función que crea hilos siempre se pude pasar un parámetro.
Yo también pensé que funcionaba como en el delphi, y alguien me habló que en java era mucho más fácil según Ricardo Zuares. Lo más complicado de esto es el delegado que para cuand es un solo hilo no tiene muchas deficultades pero cuando se trata de varios se puede complicar la cosa…
Es verdad que no es muy sencilo pero ya de una vez implementado por otro es más fácil pero mi duda no es al implementar el hilo sino como y cuando la aplicación principal puede poner a trabajar o correr al hilo implementado, es decir ya yo hice mi aplicación (que correra como un hilo) pero ¿cómo le digo al hilo que debe empezar a ejecutarse cuando yo lo necesito?
El hilo comienza a ejecutarse cuando llamas al método «Start», hasta entonces únicamente está creado.
en java tambien se puede hace de esta forma,
la opcion de heredar de la clase thread es valida en java y aparentemente no en c#, pero esta opcion tiene limitaciones, ya que como tenes que herdar y no se permite la herencia multiple, no podes hacer un thread que herede de otra clase.
En cambio en c# no hace falta ni heredar una interfaz, igualmente hay que respetar el prototipo del delegate, que es mas o menos lo mismo
podrias poner un ejemplo usando windowsforms; ya q en consola si me corre pero en la interfaz grafica no.
Gracias
Hola. En el Framework 2.0 un hilo puede inicializarse con un delegado del tipo ParameterizedThreadStart. Con esto podemos usar hilos para métodos que reciben valores (pero que no regresan valores… es decir que !!NO SIRVE!! pa el ejemplo que usted presenta). De todos modos hago un medio ejemplo pa aportar algo…..
public static void Main()
{
Thread th = new Thread(new
ThreadStart(EjemploMates.CalculoComplejo));
th.Start(1000); //
Hola Daniel, parece que se te ha cortado el código. De todas formas, tal como dices en el Framework 2.0 existe la opción de crear hilos mediante ParametrizedThreadStart y pasar parámetros al hacer el start. De hecho, esta creación se reconoce de forma automática y no hace falta utilizar la clase concreta, es suficiente con Thread. En el ejemplo que pongo podría haber sido de la siguiente forma (tal como tú dices):
Thread th = new Thread(new ThreadStart(EjemploMates.CalculoComplejo));
th.Start(1000)
Sin embargo, para la devolución de parámetros desde el hilo hay que seguir haciendo lo mismo que en el ejemplo, es decir, pasar al método un delegado como parámetro, para que lo invoque cuando termine el proceso. Para ello habría que modificar EjemploMates.CalculoComplejo para que aceptase un parámetro de tipo delegado, y hacer que al terminar el proceso lo invocase:
public class EjemploMates{
public static int CalculoComplejo(int n, MatesCallback callback)
{
// sumo uno y espero 5 segundos
n = n+1;
Thread.Sleep(5000);
if(callback != null)
callback(n);
return n;
}
}
Con esto la creación del hilo quedaría:
Thread th = new Thread(new ThreadStart(EjemploMates.CalculoComplejo));
th.Start(1000, new MatesCallback(ResultCallback));
Espero que sirva 😉
HOla,
si se hace:
t1.Join();
t2.Join();
primero esperaría a que acabara el primer hilo (t1.Join();) y luego cuando haya acabado de ejecutarse el primer hilo, se ejecutaría la instrucción t2.Join(); para esperar que acabara el segundo hilo.
Es esto correcto ???
Saludos.
Sip, así es. De todas formas ten en cuenta que el método Join tiene otras sobrecargas que pueden utilizarse [http://msdn2.microsoft.com/en-us/library/system.threading.thread.join.aspx].
En el ejemplo descrito el programa no terminaría nunca si alguno de los hilos no terminara. Para remediar esta situación se podrían utilizar llamadas a Join con un determinado tiempo para obtener de nuevo el control tras ese lapso de tiempo, obteniendo si el hilo ha terminado o no, y poder actuar como proceda en esa situación, terminar el hilo o dando otro lapso de tiempo, por ejemplo.
Hola,
Tambien me gustaria ver un ejemplo de esto, pero usando Windows.Forms…
Si en mi programa principal tengo un string (string msg) y un TextBox (Textbox Console).
Podria desde un hilo obtener el string msg y luego escribir en Console el valor de este string??
Por favor si alguien me puede ayudar, lo agradecería demasiado!
Saludos.
Hola tavo, la respuesta es que sí, aunque al querer modificar un control del interfaz (el TextBox), tendrías que hacerlo de una forma especial, ya que esto sólo puede hacerse desde el hilo principal de la aplicación, en caso contrario puedes obtener efectos inesperados.
En .Net 2.0 podrías hacerlo de forma sencilla por ejemplo con:
textbox.Invoke((MethodInvoker) delegate {
textbox.Text = msg;
});
Si quieres tenerlo estructurado en métodos, o tienes versiones anteriores, tendrías que crear un delegado y utilizarlo con invoke.
Tienes un buen ejemplo en:
http://www.yoda.arachsys.com/csharp/threads/winforms.shtml
Hola Patxi.
Muchas gracias por la respuesta. Habia encontrado la solución (similar) justo antes, lamento no haber revisado tu respuesta antes.
De todas maneras, muchas gracias. Saludos desde Chile!
y como declaro el tiempo el en .join(?)
Hola, necesito crear un hilo para abrir un documento y no me quede la aplicación congelada. Ahora, lo que hago es ejecutar un proceso y mientras este esta abierto, la aplicación queda congelada por debajo, hasta que el proceso no finaliza el programa no vuelve a estar activo. He estado mirando decenas de manuales pero no logro comprender como se hace. Ayuda por favor!
Arreglado!! El componente backgroundworker, genial!!!
cuando creo un hilo de esta forma
th1 = new Thread(new ThreadStart(this.hacer));
y en la funcion hacer llamo a una funcion ubicar,
si llamo a la funcion ubicar desde otra parte del programa no hay problema, pero si la llamo desde la funcion hacer me sale un InvalidOperationExeption, Operación no válida a través de subprocesos: Se tuvo acceso al control ‘dgv_espera’ desde un subproceso distinto a aquel en que lo creó.
que hacer??
ayuda
psd dgv_espera es un data grid view y la esepcion sale cuando quiero agregar un rowCount
Eso te pasa porque estás intentando modificar el interfaz de la aplicación desde un hilo diferente al principal, desde el que se crearon los componentes que lo crearon. Lee el siguiente post:
http://www.eslomas.com/index.php/archives/2008/01/16/actualizacion-de-pantalla-desde-otro-hilo-de-ejecucion-en-c/
Hay un muy buen libro que enseña muchos patrones para actualizar la interfaz con el usuario desde otro hilo: «C# 2008 and 2005 threaded programming», es en inglés, pero dedica varios capítulos al tema con muy buenos ejemplos, de Gaston hillar.
http://www.packtpub.com/beginners-guide-for-C-sharp-2008-and-2005-threaded-programming/book
En la página Web de Packt http://www.packtpub.com, la editorial que lo ha publicado, se puede descargar el código con ejemplos muy interseantes.
Espero que les sea útil esta información,
Ezquiel O’Hara (C# & .Net developer)
Bien, Gracias por la info men… sigue asi
Gracias, los ejemplos me han ayudado mucho en el manejo de los hilos.