domingo, 27 de enero de 2008

Parte 1

Threading in C# - Joseph Albahari


Traducción: Leonardo Micheloni



Introducción y conceptos



C#  soporta la ejecución paralela de código a través del multithreading. Un thread es un camino de ejecución de código independiente capaz de ejecutarse simultáneamente junto a otros threads. Una aplicación C# comienza con un único thread creado automáticamente por el CLR y el sistema operativo (el thread principal), y se convierte en multi-threaded al agregar threads adicionales. Aquí hay un ejemplo simple y su resultado:



Nota: Todos los ejemplos asumen que los siguiente namespaces han sido importados a menos que se especifique.




class ThreadTest 
{
static void Main()
{
Thread t = new Thread (WriteY);
t.Start(); // Ejecuta WriteY en un nuevo thread
while (true) Console.Write ("x"); // Escribe'x' por siempre }
static void WriteY()
{
while (true) Console.Write ("y"); // Escribe 'y' por siempre
}
}



xxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyy

yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxx

xxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy

yyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

...



El thread principal crea un nuevo thread  t en el cual corre un método que imprime el carácter y repetidas veces. Al mismo tiempo el thread main imprime el carácter x repetidas veces.

El CLR asigna a cada thread su propio memory stack por lo cual las variable locales se mantienen separadas. En el siguiente ejemplo definimos un método con una variable local y luego llamamos a dicho método desde el thread principal y desde un nuevo thread simultáneamente:




static void Main() 
{
new Thread (Go).Start(); // Llama a Go() en un nuevo thread
Go(); // Llama a Go() en el thread principal
}
static void Go()
{
// Declara y utiliza la variable local 'cycles'
for (int cycles = 0; cycles < 5; cycles++)
Console.Write ('?');
}



??????????





Una copia separada de la variable cycles es creada para el memory stack de cada thread, el resultado, como es predecible, son diez signos de pregunta.

Los threads comparten datos en caso de referenciar la misma instancia de un objeto. Aquí hay un ejemplo:




class ThreadTest 
{
bool done;
static void Main()
{
ThreadTest tt = new ThreadTest(); // Se crea una instancia en común
new Thread (tt.Go).Start();
tt.Go();
}
// Notar que Go ahora es un método de instacia
void Go()
{
if (!done)
{
done = true;
Console.WriteLine ("Done");
}
}
}



Ambos threads comparte el campo done porque llaman al método Go() en la misma instancia de ThreadTest. Por lo tanto se imprime "Done" una vez en lugar de dos:



Done



Los campos estáticos ofrecen otra manera de compartir datos entre threads. Aquí el mismo ejemplo con el campo done definido como estático:




class ThreadTest 
{
static bool done; // Los campos estáticos son compartidos entre los
// diferentes threads
static void Main()
{
new Thread (Go).Start();
Go();
}
static void Go()
{
if (!done)
{
done = true;
Console.WriteLine ("Done");
}
}
}



Estos dos ejemplos muestran un concepto clave – thread safety (mejor dicho lo contrario!). El resultado es indeterminado: es posible (si bien no deseado) que "Done" se imprima dos veces. En caso de invertir el orden en que se llama al método Go, entonces las probabilidades de que "Done" sea impreso dos veces aumentan dramáticamente:




static void Go() 
{
if (!done)
{
Console.WriteLine ("Done"); done = true;
}
}



Done

Done   (usually!)



El problema es que un thread puede encontrarse evaluando el if justo en el instante en que el otro thread se encuentra ejecutando el método WriteLine – antes de que éste tenga la oportunidad de poner done en true.

El remedio es obtener un exclusive lock mientras se lee y/o escribe el campo en común, para esto C# provee la instrucción lock:




class ThreadSafe 
{
static bool done;static object locker = new object();
static void Main()
{
new Thread (Go).Start();
Go();
}
static void Go()
{
lock (locker)
{
if (!done)
{
Console.WriteLine ("Done"); done = true;
}
}
}
}



Cuando dos threads simultáneamente compiten por un lock (en este caso locker), uno de los thread espera, o se bloquea, hasta que el lock esté disponible. En este caso, éste se asegura que sólo un thread pueda acceder a la sección de código crítica simultáneamente, y "Done" se imprimirá sólo una vez. El código que se encuentra protegido de algún modo, del comportamiento indeterminado en un contexto multithreading, es llamado thread-safe.



 La pausa temporal o bloqueo, es una característica esencial en la coordinación o sincronización de las actividades de los threads. Esperar por un lock exclusivo es una razón por la cual un thread puede ser bloqueado. Otra es si un thread necesita ser pausado o pasado a Sleep por un periodo de tiempo:




Thread.Sleep (TimeSpan.FromSeconds (30));         // Detiene el thread por 30 segundos


Un thread puede también esperar a que otro thread termine llamando a su método Join:




Thread t = new Thread (Go);           // Asume que Go es un método estático
t.Start();
t.Join(); // Espera (block) hasta que t finalice



Los threads no consumen recursos del CPU mientras se encuentran bloqueados.




¿Cómo funciona el Threading?





El multithreading es manejado internamente por un programador de threads, una función que el CLR típicamente delega el sistema operativo. Un programador de threads se asegura que todos los threads activos se encuentren apropiadamente dispuestos en tiempo de ejecución, y que los threads que se encuentran bloqueados - por ejemplo por un bloqueo exclusivo, o esperando una entrada del usuario - no consuman recursos de tiempo del CPU.



En una computadora con un único procesador el thread scheduler realiza time-slicing –  alternando la ejecución entre cada uno de los threads activos. El resultado es un comportamiento "de a pedazos", al igual que en el primer ejemplo en el cual cada bloque de repeticiones de X o Y correspondían a un time-slice reservado para cada thread. Bajo Windows XP, un time-slice se encuentra típicamente en el orden de las decenas de milisegundos – valor elegido para que sea mucho más grande que el tiempo de más que agrega el CPU para cambiar entre el contexto de un thread y otro (el cual se encuentra típicamente en el orden de un par de milisegundos).



En un computador con múltiples procesadores, multithreading es implementado con una mezcla de time-slicing y concurrencia real – en donde diferentes threads ejecutan código simulatáneamente en direrentes CPUs. Existirá siempre algo de time-slicing, ya que el sistema operativo necesita servir a sus propios threads – así como a los de otras áplicaciones.



Se dice que un thread es bloqueado cuando es interrumpido por un factor externo como el time-slicing. En muchas situaciones un thread no tiene control de cuándo o dónde sera bloqueado.




Threads vs. Procesos




Todos los threads de una aplicación se encuentran contenidos en proceso – la unidad del sistema operativo en la cual una aplicación corre.

 Los threads tiene ciertas similitudes con los procesos – por ejemplo los procesos corren bajo time-sliced con otros procesos en la computadora de un modo similar a los threads en una aplicación C#. La principal diferencia es que los procesos se encuentran totalmente aislados de otros procesos, mientras que los threads comparten el montículo de memoria (heap)  con otros threads de la misma aplicación. Esto hace que los threads sean muy útiles: un thread puede recuperar datos en segundo plano, mientras que otro thread va mostrando los datos mientras llegan.



Cuándo utilizar Threads?



Una aplicación común para el multithreading es la ejecución en segundo plano de tareas que consumen mucho tiempo. El thread principal se mantiene corriendo mientras que el thread que realiza la operación funciona en segundo plano. En aplicaciones Windows Forms, si el thread principal realiza operaciones largas, los mensajes del teclado y el mouse no pueden ser procesados, como resultado la aplicación deja de responder. Por esta razón es bueno hacer que las tareas que insumen mucho tiempo funcionen en threads de trabajo y en todo caso el thread principal puede mostrar el diálogo modal “Procesando… espere por favor” en caso que el programa no pueda continuar hasta que la tarea termine. Esto asegura que la aplicación no sea tomada por el sistema operativo como "No responde" incitando al usuario a forzar la finalización del proceso! El diálogo modal permite implementar un botón "Cancelar" ya que el diálogo continúa recibiendo eventos mientras que la tarea es realizada por el hilo trabajador. La clase BackgroundWorker ayuda a implementar este patrón de uso.



En el caso de las aplicaciones in interfaz gráfica, como los servicios de Windows, el multithreding toma particular importancia cuando una tarea es potencialmente consumidora de tiempo porque se encuentra a la espera de la respuesta de otra computadora (como un servidor de aplicaciones, un servidor de base de datos, o un cliente). Tener un thread trabajador realizando dicha tarea significa que el thread que lo instancia se encontrará inmediatamente libre para hacer otras cosas.

 Otra aplicación del multithreading es utilizarlo en métodos que realizan cálculos intensivos.  Tales métodos pueden ejecutarse más rápido en computadoras con múltiples procesadores si la carga de trabajo es dividida en múltiples threads. (La cantidad de procesadores puede obtenerse por medio de la propiedad Environment.ProcessorCount).



Una aplicación C# puede convertirse en multi thread de dos maneras: creando explícitamente threads adicionales, o utilizando características del framework .NET que implícitamente crean threads – como BackgroundWorker, thread pooling, un threading timer, un Remoting server, o un Web Services o una aplicación ASP.NET. En estos últimos casos no se tiene control del multithreading.




Cuándo no utilizar Threads?




 El multithreading posee desventajas. La más grande es que implica mayor complejidad en los programas. La creación de una aplicación multiples threads no es compleja, la complejidad se encuentra en la interacción entre los threads. Esto es así cuando la interacción es válida o no, y puede derivar en largos procesos de desarrollo y la consecuente susceptibilidad de intermitentes y difícilmente reproducibles bugs. Por esta razón es recomendable mantener el diseño de la interacción entre los múltiples threads simple – o no utilizar multithreadingl – a menos que tangas una peculiar inclinación por re-ecribir y debuggear!

 El multithreading también implica consumo de recursos y costo de CPU en crear y cambiar entre threads si es utilzado n exceso. En particular, cuando implica pesados procesos de lectura/escritura a disco, puede ser más rápido tener uno o dos threads trabajadores realizando las tareas en secuencia en lugar de tener múltiples threads trabajando en simultáneo. Más adelante describiremos cómo implementar una cola Productor/Consumidor, que provee dicha funcionalidad.




Creando e iniciando Threads




Los threads utilizando el constructor de la clase Thread, pasándole un delegado del tipo ThreadStart– indicando el método desde donde debe iniciar la ejecución. Aquí vemos la definición del delegado ThreadStart:



public delegate void ThreadStart();



Llamando al método Start logramos que la ejecución comience. El thread continúa hasta que su método retorne, en este punto el thread finaliza.  Aquí vemos un ejemplo utilizando la sintaxis larga de C# para crear el delegado TheadStart:




class ThreadTest 
{
static void Main()
{
Thread t = new Thread (new ThreadStart (Go));
t.Start();
new thread.Go(); // Corre simultaneamente Go() en el thread principal.
}
static void Go()
{
Console.WriteLine ("hello!");
}

En este ejemlo, el thread t ejecuta Go() – e inmediatamente después el thread main llama a Go(). El resultado son dos hello:



hello!

hello!



Se puede crear un thread de un modo más conveniente utilizando la sintáxis corta de C# para instanciar delegados:




static void Main() 
{
Thread t = new Thread (Go); // No es necesario utilizar explícitamente ThreadStart
t.Start();
...
}
static void Go()
{
...
}



En este caso el delegado ThreadStart es inferido por el compilador. Otro método es utilizar un método anónimo para iniciar un thread:




static void Main() 
{
Thread t = new Thread (delegate()
{
Console.WriteLine ("Hello!");
}
);
t.Start();
}



Un thread tiene su propiedad IsAlive en valor true después de llamar a su método Start(), hasta que el thread finaliza. Una vez finalizado un thread no puede ser re-iniciado.




Pasando datos a ThreadStart




Digamos que en ejemplo de arriba queremos distinguir mejor la salida de cada thread, por ejemplo haciendo que uno de ellos imprima en mayúscula.



  Podemos lograr esto pasando un flag al método Go: pero no podemos usar el delegado ThreadStart porque no acepta argumentos. Afortunadamente en framework .NET define otra versión del delegado llamada ParameterizedThreadStart, la cual acepta un objeto como parámetro del siguiente modo:



public delegate void ParameterizedThreadStart (object obj);



Entonces el ejemplo previo se ve así:




class ThreadTest 
{
static void Main()
{
Thread t = new Thread (Go);
t.Start (true);
Go (true)
Go (false);
}
static void Go (object upperCase)
{
bool upper = (bool) upperCase;
Console.WriteLine (upper ? "HELLO!" : "hello!");
}
}



hello!

HELLO!



En este ejemplo el compilador infiere automáticamente el delegado ParameterizedThreadStart porque el método Go acepta un objeto como argumento.



Podríamos haberlo escrito:



Thread t = new Thread (new ParameterizedThreadStart (Go));
t.Start (true);



Una característica de utilizar ParameterizedThreadStart es que debemos hacer cast del argumento object al tipo específico (en este caso bool) antes de usarlo. Si bien hay una sola versión del delegado que acepte un único argumento.

Una alternativa al uso del método anónimo es llamar a un método ordinario del siguiente modo:



static void Main() 
{
Thread t = new Thread (delegate()
{
WriteText ("Hello");
});
t.Start();
}
static void WriteText (string text)
{
Console.WriteLine (text);
}



La ventaja es que el método de destino (en este caso WriteText) puede aceptar cualquier número de argumentos y hace cast según se requiera. Sin embargo debemos tener en cuenta la semántica outer-variable de los métodos anónimos como muestra en siguiente ejemplo:



static void Main() 
{
string text = "Before";
Thread t = new Thread (delegate()
{
WriteText (text);
});
text = "After";
t.Start();
}

static void WriteText (string text)
{
Console.WriteLine (text);
}



 Los métodos anónimos abren la grotesca posibilidad de la interacción no deseada a través de "outer-variables" (variables externas) si es que son modificadas por cada thread que se inicia.  Otro sistema común de pasar datos a un thread es dar al thread un método de instancia en lugar de un estático. Las propiedades de la instacia del objeto le dirán al thread qué tiene que hacer como en el siguiente ejemplo:



class ThreadTest 
{
bool upper;
static void Main()
{
ThreadTest instance1 = new ThreadTest();
instance1.upper = true;
Thread t = new Thread (instance1.Go);
t.Start();
ThreadTest instance2 = new ThreadTest();
instance2.Go();
// Thread main, corriendo con upper=false }

void Go()
{
Console.WriteLine (upper ? "HELLO!" : "hello!");
}
}




Nombrando Threads




Se puede asignar nombre a un thread a través de su propiedad Name. Esto es de gran beneficio en la depuración: y también permite hacer Console.WriteLine nombre del thread, Microsoft Visual Studio recoge el nombre del thread y lo muestra en la barra de herramientas Debug Location. Podemos nombrar un thread en cualquier momento, pero sólo una vez, en caso de intentar cambiar el nombre de un thread obtendremos como resultado una excepción.

También podemos asignarle nombre al thread principal de la aplicación, en el siguiente ejemplo hacemos esto a través de la propiedad estática CurrentThread:



class ThreadNaming 
{
static void Main()
{
Thread.CurrentThread.Name = "main";
Thread worker = new Thread (Go);
worker.Name = "worker";
worker.Start(); Go();
}
static void Go()
{
Console.WriteLine ("Hello from " + Thread.CurrentThread.Name);
}
}



Hello from main

Hello from worker




Threads en primer y segundo plano




Por defecto los threads corren en primer plano, esto quiere decir que la aplicación siguirá corriendo mientras que alguno de sus threas se encuentre activo. C# soporta threads en segundo plano (background threads) los cuales no mantiene la aplicación corriendo por si mismo, sino que terminan inmediatamente cuando la aplicación finaliza.

Cambiar un thread de primer a segundo plano no cambiar su estado o prioridad en la planificación del CPU de ninguna manera.

La propiedad IsBackground del thread controla el modo de ejecución como en el siguiente ejemplo:




class PriorityTest 
{
static void Main (string[] args)
{
Thread worker = new Thread (delegate() { Console.ReadLine();
});
if (args.Length > 0) worker.IsBackground = true;
worker.Start();
}
}



Si llamamos al programa sin pasarle argumentos el thread de trabajo corre en su modo por defecto de primer plano y esperará en la instrucción ReadLine hasta que el usuario presione Enter. Mientras tanto el thread principal deja de existir pero la aplicación sigue corriendo porque existe un thread en primer plano todabía vivo.

Por otro lado, si un argumento es pasado al thread principal (Main()) el thread de trabajo es ejecutado en segundo plano y el programa deja de existir cuando el thread principal termina, finalizando la instrucción ReadLine.



 Cuando un hilo en segundo plano termina de este modo, todos los bloques finally son ignorados. Ya que no es deseable ignorar los bloques finally es una buena práctica esperar a que todos los threads de trabajo terminen antes de finalizar la aplicación – posiblemente con un timeout (esto puede lograrse utilizando Thread.Join). Si por alguna razón un thread renegado nunca termina, podemos intentar abortarlo, y si esto falla, abandonar el thread permitiendo que muera con el proceso.



Tener threads en segundo plano puede ser beneficioso, por la razón que es posible que el último diga cuando la aplicación debe terminar. Consideremos la alternativa - un thread en primer plano que no muere - evitando que la aplicación termine. Un thread en primer plano olvidado es particularmente dañino en aplicaciones Windows Form, porque la aplicación aparentará terminar (el menos para el usuario) pero el proceso continuará corriendo. La aplicación habrá desaparecido de la solapa de aplicaciones en el administrador de tareas de Windows, sin embargo su nombre de archivo ejecutable será visible en la solapa de procesos. Al menos que el usuario localice y termine la tarea explicitamente, la misma continuará consumiendo recursos, y tal vez impidiendo que una nueva instancia sea ejecutada o funcione apropiadamente.



Una causa común de que las aplicaciones no terminen correctamente es la presencia de threads en primer plano olvidados.

Prioridad de los Thread La prioridad de un thread determina cuanto tiempo de ejecución tomará en relación a los otros threads en el mismo proceso según la siguiente escala:




enum ThreadPriority 
{
Lowest,
BelowNormal,
Normal,
AboveNormal,
Highest
}



Esto es sólo relevante cuando hay varios threads activos simultáneamente.

Asignar a un thread a la prioridad más alta no significa que pueda realizar operaciones en tiempo real, ya que se encuentra limitado a la prioridad del proceso. Para ejecutar trabajo en tiempo real la clase Process en System.Diagnostics debe además elevar su prioridad del siguiente modo(no digan que yo les dije esto):



Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;



ProcessPriorityClass.High se encuentra justo antes de la prioridad más alta: Realtime. Configurar un proceso en  Realtime le indica al sistema operativo que no se desea que el proceso sea demorado nunca. En caso que nuestro programa entre accidentalmente en un bucle infinito podemos esperar que el sistema operativo se bloquee. ¡Nada además de apagar la computadora va a salvarnos! Por esta razón,  High es considerada la mayor prioridad realmente usable para un proceso.



Si una aplicación de tiempo real posee una interfaz gráfica no es deseable que posea un tiempo excesivo de CPU - haciendo lenta toda la computadora, particularmente si la interfaz es compleja. (si bien al momento de escribir esto, la aplicación de telefonía por Internet Skype lo hace ya que su interfaz es bastante simple). Bajar la prioridad de thread principal -  en conjunto con elevar la prioridad del proceso - asegura que el thread de tiempo real no será interrumpido por el refresco de la interfaz, pero no evitará que la computadora se ralentice ya que el sistema operativo aún asignará excesivo tiempo de CPU al proceso. La solución ideal es tener el thread de trabajo en tiempo real y la interfaz del usuario corriendo en procesos separados (con diferentes prioridades), comunicándose vía Remoting o memoria compartida. La memoria compartida requiere P/Invoking sobre la API de Win32.




Manejo de excepciones




Cualquier bloque try/catch/finally en el ámbito cuando un thread es creado no tiene relevancia cuando el thread es ejecutado. Consideremos el siguiente ejemplo:



public static void Main() 
{
try
{
new Thread (Go).Start();
}
catch (Exception ex)
{
// Nunca deberíamos llegar aquí!
Console.WriteLine ("Exception!");
}
static void Go()
{
throw null;
}
}



El bloque try/catch de este ejemplo es efectivamente inútil, y el thread recientemente creado se interrumpirá por una excepción no controlada del tipo NullReferenceException. Este comportamiento tiene sentido si consideramos un thread como un camino independiente de ejecución. El remedio es que los métodos de entrada de los threads tengan sus propios controles de excepción:



public static void Main() 
{
new Thread (Go).Start();
}
static void Go()
{
try
{
... throw null; // esta excepción debe ser atrapada después
}catch (Exception ex)
{
//Típicamente loger la excepción, y/o avisar a otro thread ...
}
}



Para .NET 2.0 una excepción no controlada en cualquier thread hará caer la aplicación, esto significa que ignorar las excepciones generalmente no es una opción. Por lo tanto es deseable un bloque try/catch en cada método de entrada de un thread - al menos en producción - para evitar la caída de la aplicación no desea a causa de una excepción no controlada. Esto puede ser un tanto engorroso - particularmente para los programadores Windows Form, quienes utilizan comúnmente el handler de excepciones "global" como se ve:



using System;
using System.Threading;
using System.Windows.Forms;

static class Program
{
static void Main()
{
Application.ThreadException += HandleError;
Application.Run (
new MainForm());
}
static void HandleError (object sender, ThreadExceptionEventArgs e)
{
Log exception, then either exit the app or continue...
}
}



El evento Application.ThreadException es disparado cuando una excepción es lanzada como resultado de un mensaje de windows(por ejemplo, el teclado, el mouse, un mensaje "paint") en resumen todo el código es una aplicación Windows Form típica.



Si bien esto funciona perfectamente, esto puede hacernos caer en una falsa sensación de seguridad - que todas las excepciones serán atrapadas por controlador central de excepciones. Las excepciones que se producen en threads de trabajo son un buen ejemplo de excepciones que no serán atrapadas por Application.ThreadException (el código dentro del método Main es otro - incluido el constructor del formulario principal, el cual es ejecutado antes que el bucle de mensajes de Windows comience).



 El framework .NET provee un evento para manipular excepciones de bajo nivel: AppDomain.UnhandledException. Este evento es disparado cuando ocurre una excepción no controlada en cualquier thread, y en cualquier tipo de aplicación (con o sin interfase). Sin embargo, si bien esto ofrece un buen mecanismo para logear excepciones no atrapadas, no significa que evite que la aplicación caiga - y no significa que elimine el mensaje de excepción no controlada de .NET.

 En aplicaciones de producción son necesarios manejos de excepciones en todos las métodos de entrada de los threads. Se puede ahorrar trabajo utilizando una clase wrapper  helper class que haga el trabajo, como BackgroundWorker (discutido en la Parte 3).



© 2006-2007, O'Reilly Media, Inc. All rights reserved


7 comentarios:

Claudio Gonzalez Vera dijo...

Interesante, me fue de mucha utilidad el post. Gracias

Willy dijo...

se puede usar tire hilos con ado.net

Claudio Gonzalez Vera dijo...

Willy:
El uso de thread en .NET me parece que es independiente segun esquema de clases que uses, en este caso ADO.NET para el acceso a datos. Mientras respeteas las formas standar de sincronizacion entre hilos deberia funcionar todo bien. Pero te puedo dar una sugerencia: trata de que los hilos sean lo maximo posible independientes unos de otros, esto es, autocontenidos en cuanto el proceso en si que manejan, incluido la gestion de errores. De otra manera el comportamiento de la aplicacion multithread puede llegar a ser caotico y nada feliz para depurar.
Saludos!

Swinet dijo...
Este comentario ha sido eliminado por el autor.
Swinet dijo...

Buenas noches colega tengo una la siguiente duda a ver si pueden orientar.

La problematica que tengo es la siguiente. Estoy utilizando la libreria de VLC Player en C# para transmitir streaming de video a diferentes computadoras hacia una direccion IP y puerto diferente.

Por lo que en mi aplicacion local que actuara como un servidor conectado a una base de datos. Donde tengo que brindar servicios de peticiones entrantes por ejemplo : Pedro quiere ver : "Matrix" y juan: "Senor De los anillos" queria saber si puedo implententar un thread donde estare instanciando la libreria del VLC. Para separat el streaming segun los parametros entrantes en su constructor. Quiero saber si es posisble despues de haber lanzado el hilo si puedo volver a llamar cada thred independiente y Modificar al algun para volver a transmitir otro streaming diferente. Cada ves que llegue un cambio de solititud entrante por cada usuario a la base de datos. Por ejemplo los mismo usuarios dichos que mencione en el ejemplo anterior. Gracias de antemano.

CostWar dijo...

Tal parece que esto no me sirve para operar matrices grandes.
igual muchas gracias por el aporte.

Este metodo es poco practico, a lo mejor deba seguir usando el antiguo metodo del for para hacer esto.

Unknown dijo...

Muy buen aporte. Muchas gracias.