Вы находитесь на странице: 1из 21

1 de 21

La clase Thread
Ejecucin asncrona de
Hebras en aplicaciones
Hebras en aplicaciones
Control de la ejecucin
Paralelismo real
Referencias

delegados
Windows
web
de una hebra

Todas las aplicaciones .NET son en realidad aplicaciones multihebra.

La clase Thread
La clase System.Thread representa una hebra del sistema. Las hebras se lanzan cuando se invoca
un delegado sin argumentos que sirve de punto de entrada a la hebra. Al no tener argumentos este
delegado, el estado inicial de la hebra hay que establecerlo previamente en el objeto que aloje el
delegado.

// Creacin del objeto que aloja nuestra hebra


Tarea tarea = new Tarea();
// Inicializacin de parmetros
tarea.parameter= 1234;

2 de 21

// Establecimiento del punto de entrada de la hebra (delegado)


ThreadStart threadStart = new ThreadStart(tarea.Run);
// Creacin de la hebra
Thread thread = new Thread(threadStart);
// Ejecucin de la hebra
thread.Start();
// ...
// Espera a la finalizacin de la hebra
thread.Join();

Clculo de PI
Aunque no sea algo especialmente til en el trabajo cotidiano de un
programador, supongamos que, por algn extrao motivo, hemos de calcular
con precisin el valor del nmero PI y no nos vale la constante definida en
System.Math.PI, que slo incluye veinte dgitos de precisin. No obstante, el
proceso que seguiremos para desarrollar una aplicacin multihebra ser el
mismo que el que seguiramos en cualquier otro caso real (p.ej. realizacin de
copias de seguridad mientras nuestra aplicacin sigue funcionando,
implementacin de servidores de aplicaciones, acceso a recursos a travs de la
red sin detener la ejecucin de nuestra aplicacin, etc.). Recordemos que, en
general, la utilizacin de hebras es siempre til cuando hemos de realizar
cualquier tarea que requiera su tiempo...
Comenzamos creando una aplicacin Windows con un formulario principal como
el siguiente:

3 de 21

Formulario FormPI (Text="Clculo de PI").


NumericUpDown
Value=1000).

editDigits

(Align=Right;

Maximum=10000;

Button buttonCalc (Text="Calcular").


TextBox textPI (Text=""; Multiline=true).
ProgressBar progress.
Para calcular PI hasta la precisin deseada slo tenemos que implementar un
bucle y utilizar una funcin que nos va devolviendo los dgitos de PI de 9 en 9
(Pi.cs):
private void CalcularPi (int precision)
{
int
digitos, calculados, nuevos;
string
ds;
StringBuilder pi = new StringBuilder("3.", precision + 2);
ShowProgress(pi.ToString(), 0, precision);
calculados = 0;
while (calculados<precision) {
digitos = NineDigitsOfPi.StartingAt(calculados+1);

4 de 21

nuevos = Math.Min(precision - calculados, 9);


ds = string.Format("{0:D9}", digitos);
pi.Append(ds.Substring(0, nuevos));
ShowProgress(pi.ToString(), calculados+nuevos, precision);
calculados += 9;
}
}
Los avances en el clculo de PI los mostramos peridicamente en el TextBox y,
para que el usuario tenga una idea de cunto queda, tambin en la barra de
progreso de nuestro formulario:
void ShowProgress (string pi, int actual, int total)
{
textPI.Text
= pi;
progress.Maximum = total;
progress.Value
= actual;
}
La forma ingenua de implementar nuestro aplicacin sera hacer que, al pulsar
el botn, se calculase el valor de PI con la precisin solicitada. No obstante,
aparte de que nuestra aplicacin se queda bloqueada mientras se realiza el
clculo, al cambiar a otra aplicacin y luego volver a la nuestra, nos podramos
encontrar una desagradable sorpresa (muy comn, por otro lado, en
demasiadas aplicaciones comerciales):

5 de 21

La imagen anterior se debe a que nuestra aplicacin Windows, ocupada en


realizar el clculo de PI como respuesta a la pulsacin del botn, no atiende
ningn otro evento de los que se producen (como, por ejemplo, el que le pide
refrescar su imagen en pantalla: el evento Paint del formulario). La solucin a
nuestro problema pasa, pues, por crear una hebra independiente que se ejecute
en paralelo y no interfiera en el comportamiento habitual del interfaz de nuestra
aplicacin:

private void buttonCalc_Click(object sender, System.EventArgs e)


{
ThreadStart piThreadStart = new ThreadStart(PiStart);
Thread
piThread
= new Thread(piThreadStart);
piThread.Start();
}
private void PiStart ()
{
CalcularPi ( (int) editDigits.Value );
}
Con esta pequea modificacin conseguimos que el interfaz grfico de nuestra
aplicacin funcione correctamente:

6 de 21

Ejecucin asncrona de delegados


Para no tener que crear una funcin sin parmetros especialmente escrita para poder lanzar la hebra
(y tener que implementar en ella la inicializacin de los parmetros reales de la hebra), podemos
utilizar delegados:

// Declaracin
delegate void PiDelegate (int precision);
// Creacin
PiDelegate delegado = new PiDelegate(CalcularPi);
// Uso
delegado((int)editDigits.Value);

El delegado declarado deriva de la clase MultiCastDelegate, que implementa tres funciones:

7 de 21

class PiDelegate : MulticastDelegate


{
public void Invoke(int precision);
public void BeginInvoke(int precision, AsyncCallback callback, object state);
public void EndInvoke(IAsyncResult result);
}

Cuando un delegado se usa como si fuese una funcin, en realidad se est llamando al mtodo
sncrono Invoke, que es el que se encarga de llamar a la funcin concreta con la que se instancia el
delegado (CalcularPi en este caso). Los otros dos mtodos del delegado, BeginInvoke y
EndInvoke, son los que permiten invocar al delegado de forma asncrona. De forma que podemos
calcular PI en una hebra independiente de la siguiente forma:
Clculo de PI con delegados

delegate void PiDelegate (int precision);


private void buttonCalc_Click(object sender, System.EventArgs e)
{
PiDelegate delegado = new PiDelegate(CalcularPi);
delegado.BeginInvoke((int)editDigits.Value, null, null);
}

En principio, todo parece ir bien, aunque an nos quedan algunos detalles por pulir...

Hebras en aplicaciones Windows


Aunque tuvimos suerte en el ejemplo anterior (posiblemente por la implementacin de nuestro
sistema operativo), en realidad violamos una regla bsica de Windows: manipular una ventana
nicamente desde la hebra que la crea. En general, como veremos en la siguiente seccin del curso,
no es correcto acceder un recurso compartido desde distintas hebras si no utilizamos los

8 de 21

mecanismos de proteccin adecuados.


La documentacin de la plataforma .NET lo deja claro. Slo hay cuatro mtodos de un control que se
pueden llamar de forma segura desde cualquier hebra: Invoke, BeginInvoke, EndInvoke y
CreateGraphics). Cualquier otro mtodo ha de llamarse a travs de uno de los anteriores, como,
por ejemplo, los que modifican las distintas propiedades de los controles de nuestra ventana. En
realidad, slo tenemos que crear un nuevo delegado que se ejecute en la hebra principal
correspondiente a la interfaz grfica.
Si nos preocupase que la hebra que calcula el valor de PI quedase bloqueada (como antes la hebra de la
interfaz grfica), tendramos que utilizar los mtodos asncronos BeginInvoke y EndInvoke. En el
ejemplo que nos ocupa nos basta con el mtodo sncrono Invoke:

public object Invoke(Delegate method);


public object Invoke(Delegate method, object[] args);
La segunda variante del mtodo es la que utilizaremos en nuestra hebra para llamar a ShowProgress
especificando sus parmetros.
Creamos un delegado para la funcin que deseamos llamar:
delegate void ShowProgressDelegate
(string pi, int actual, int total);
Sustituimos las llamadas a ShowProgress por:

ShowProgressDelegate showProgress =
new ShowProgressDelegate(ShowProgress);
...
this.Invoke(showProgress, new object[] { pi.ToString(), calculados, precision});

El uso de Invoke nos garantiza que accedemos de forma segura a los controles de nuestra ventana
en una aplicacin multihebra. La hebra principal crea una hebra encargada de realizar una tarea
computacionalmente costosa [la hebra trabajadora] y sta le pasa el control a la hebra principal cada
vez que necesita actuar sobre la interfaz. La siguiente figura ilustra cmo funciona nuestra aplicacin
en tiempo de ejecucin:

9 de 21

Tener que llamar a Invoke cada vez que queremos garantizar el correcto funcionamiento de nuestra
aplicacin multihebra es realmente incmodo y, adems, resulta bastante fcil que se nos olvide
hacerlo en alguna ocasin. Por tanto, no es mala idea que sea la propia funcin a la que llamamos la
que se encargue de asegurar su correcta ejecucin en una aplicacin multihebra:

10 de 21

En el ejemplo anterior, si implementamos la funcin ShowProgress de la siguiente forma,


podemos llamar a la funcin de la forma tradicional sin preocuparnos de la hebra desde la
que utilizamos nuestra funcin:
void ShowProgress(string pi, int actual, int total)
{
if ( textPI.InvokeRequired == false ) {
// Hebra correcta
textPI.Text
= pi;
progress.Maximum = total;
progress.Value
= actual;
} else {
// Llamar a la funcin de forma asncrona
ShowProgressDelegate showProgress = new ShowProgressDelegate(ShowProgress);
this.Invoke(showProgress, new object[] { pi, actual, total});
}
}
Si tenemos en cuenta que la funcin ShowProgress no devuelve ningn valor (esto es, en
realidad se trata de un procedimiento), podemos sustituir la llamada sncrona Invoke por
una llamada asncrona con BeginInvoke:

BeginInvoke(showProgress, new object[] { pi, actual, total});

BeginInvoke es siempre preferible cuando la funcin no devuelve ningn valor ya que evita
que se puedan producir bloqueos.

Hebras en aplicaciones web


Las peculiaridades de las interfaces web ocasionan la aparicin de problemas de los cuales no
tendramos que preocuparnos en otros contextos. ste es el caso cuando nuestra aplicacin web
debe realizar una tarea relativamente larga. Lo que est claro es que no queda demasiado bien de
cara al usuario dejar su ventana en blanco de forma indefinida mientras nuestra aplicacin realiza los
clculos que sean necesarios.
Una solucin a este problema involucra la utilizacin de hebras. El problema de realizar un clculo
largo lo volvemos a descomponer en dos hebras:
La hebra principal se encargar de mostrarle al usuario el estado actual de la aplicacin,

11 de 21

estado que se refrescar en su navegador automticamente gracias al uso de la cabecera


Refresh, definida en el estndar para las respuestas HTTP.
Una hebra auxiliar ser la encargada de ejecutar el cdigo correspondiente a efectuar
todos los clculos que sean necesarios para satisfacer la solicitud del usuario.
Hebras en aplicaciones web
Una pgina ASP.NET lanza la tarea:

...
using System.Threading;
public class Payment : System.Web.UI.Page
{
protected Guid ID; // Identificador de la solicitud
private void Page_Load(object sender, System.EventArgs e)
{
if (Page.IsPostBack) {
// 1. Crear un ID para la solicitud
ID = Guid.NewGuid();
// 2. Lanzar la hebra
ThreadStart ts = new ThreadStart(RealizarTarea);
Thread thread = new Thread(ts);
thread.Start();
// 3. Redirigir a la pgina de resultados
Response.Redirect("Result.aspx?ID=" + ID.ToString());
}
}
private void RealizarTarea ()
{

12 de 21

// Tarea larga !!!


System.Threading.Thread.Sleep(new TimeSpan(0, 0, 0, 7, 0));
object resultado = new Random(0).Next(100);
Results.Add(ID, resultado);
}
...
}
Una clase auxiliar Results mantiene los resultados de las distintas
hebras para que la pgina de resultados pueda acceder a ellos:

using System;
using System.Collections;
public sealed class Results
{
private static Hashtable results = new Hashtable();
public static object Get(Guid ID)
{
return results[ID];
}
public static void Add (Guid ID, object result)
{
results[ID] = result;
}
public static void Remove(Guid ID)
{
results.Remove(ID);
}
public static bool Contains(Guid ID)
{
return results.Contains(ID);
}
}

13 de 21

Finalmente, la pgina encargada de mostrar los resultados se refresca


automticamente hasta que la ejecucin de la hebra auxiliar haya
terminado y sus resultados estn disponibles:

public class Result : System.Web.UI.Page


{
protected System.Web.UI.WebControls.Label lblMessage;
private void Page_Load(object sender, System.EventArgs e)
{
Guid ID = new Guid(Page.Request.QueryString["ID"]);
if (Results.Contains(ID)) {
// La tarea ha terminado: Mostrar el resultado
lblMessage.Text = Results.Get(ID).ToString();
Results.Remove(ID);
} else {
// An no tenemos el resultado: Esperar otros 2 segundos
Response.AddHeader("Refresh", "2");
}
}
...
}

La solucin aqu propuesta es extremadamente til en la prctica. Imagine, por ejemplo, una
aplicacin de comercio electrnico que ha de contactar con un banco para comprobar la validez de
una tarjeta de crdito. No resulta demasiado difcil imaginar la impresin del usuario final cuando la
implementacin utiliza hebras y cuando no lo hace.

Control de la ejecucin de una hebra


An nos faltaba por hacer una cosa ms con nuestra aplicacin Windows. Una vez que el usuario
ordena la ejecucin de una tarea computacionalmente costosa, sera deseable que siempre
mantuviese el control sobre lo que hace el ordenador (un principio bsico en el diseo de interfaces
de usuario).

14 de 21

Para que el usuario mantenga el control absoluto sobre la ejecucin de nuestra aplicacin,
podemos hacer que el botn mediante el cual se inici el clculo de PI sirva tambin para
detenerlo (o crear una ventana de dilogo independiente en la cual se incluya algn botn con la
misma funcionalidad). Si el usuario decide cancelar la operacin en curso, la hebra que controla la
interfaz de usuario debe comunicarle a la otra hebra que detenga su ejecucin y, mientras sta no
se detenga, tenemos que deshabilitar el botn (con el fin de evitar que, por error, se cree una
nueva hebra en el intervalo de tiempo que abarca desde que el usuario pulsa el botn de cancelar
hasta que la hebra realmente se detiene).
Comenzamos definiendo una variable de estado:
enum Estado { Inactivo, Calculando, Cancelando };
Estado estado = Estado.Inactivo;
Implementamos la respuesta de nuestro botn en funcin del estado actual de la
operacin:
private void buttonCalc_Click(object sender, System.EventArgs e)
{
switch (estado) {
case Estado.Inactivo:
// Comenzar el clculo
estado = Estado.Calculando;
buttonCalc.Text = "Cancelar";
PiDelegate delegado = new PiDelegate(CalcularPi);
delegado.BeginInvoke((int)editDigits.Value, null, null);
break;

}
}

case Estado.Calculando:
estado = Estado.Cancelando;
buttonCalc.Enabled = false;
break;

// Cancelar operacin

case Estado.Cancelando:
Debug.Assert(false);
break;

// No debera suceder nunca...

15 de 21

Para que la hebra detenga su ejecucin, podemos hacer que las hebras se comuniquen a
travs de paso de parmetros (por ejemplo, haciendo que la funcin ShowProgress
devuelva un valor a travs de un parmetro adicional, que ser comprobado en cada
iteracin de la hebra para determinar si el usuario ha pedido cancelar la operacin en
curso). Tambin podemos emplear datos compartidos por las hebras, aunque, en ese
caso, deberemos tener especial cuidado con los problemas de sincronizacin y posibles
bloqueos que puedan ocurrir. Si utilizamos esta ltima opcin, podemos escribir lo
siguiente:
delegate void EndProgressDelegate ();

private void CalcularPi (int precision)


{
EndProgressDelegate endProgress = new EndProgressDelegate(EndProgress);
...
while ( (estado!=Estado.Cancelando) && ...) {
...
}
this.Invoke(endProgress);
}
private void EndProgress ()
{
estado = Estado.Inactivo;
buttonCalc.Text = "Calcular";
buttonCalc.Enabled = true;
}
OJO! Cuando varias hebras acceden simultneamente a algn recurso compartido es
necesario implementar algn tipo de mecanismo de exclusin mutua: en el desarrollo de
aplicaciones multihebra siempre debemos garantizar la exclusin mutua en el acceso a
recursos compartidos. En el caso anterior, la aplicacin funciona porque slo una de las
hebras modifica el valor de la variable compartida y, adems, lo restablece slo despus
de que la otra hebra haya comprobado el valor de la variable compartida y haya decidido
finalizar su ejecucin (EndProgress). En general, no obstante, deberamos estudiar las
posibles condiciones de carrera y la posibilidad de bloqueos si empleamos tcnicas de
exclusin mutua...

16 de 21

Paralelismo real
Podemos aprovechar el paralelismo real de nuestra mquina para reducir el tiempo de reloj
necesario para realizar un clculo costoso si disponemos de un multiprocesador, de un
microprocesador de doble (o cudruple) ncleo o simplemente tenemos un procesador SMT
(Simultaneous Multithreading, lo que Intel hace comercializa bajo el nombre HyperThreading).

Igual que antes, para que el usuario mantenga el control absoluto sobre la ejecucin de
nuestra aplicacin, hacemos que el botn mediante el cual se inici el clculo de PI sirva
tambin para detenerlo:

// Variable de estado
enum Estado { Inactivo, Calculando, Cancelando };
Estado estado = Estado.Inactivo;

// Evento asociado al ratn


private void buttonCalc_Click(object sender, System.EventArgs e)
{
switch (estado) {
case Estado.Inactivo:
// Comenzar el clculo
estado = Estado.Calculando;
// con varias hebras
buttonCalc.Text = "Cancelar";
textPI.Text = "";
setPrecision((int)editDigits.Value);
startThread();
startThread();
break;
case Estado.Calculando:

// Cancelar operacin

17 de 21

estado = Estado.Cancelando;
buttonCalc.Enabled = false;
break;
}
}
La actualizacin de la interfaz de usuario la hacemos de forma similar a como la hacamos
antes, si bien hemos simplificado algo la signatura de los mtodos utilizados ya que, ahora, la
precisin deseada de pi ser una variable de instancia de nuestra clase:

delegate void ShowProgressDelegate(int actual);


delegate void EndProgressDelegate();

private void ShowProgress (int completed)


{
Debug.Assert(textPI.InvokeRequired == false);
progress.Maximum = precision;
progress.Value
= Math.Min(precision, 9*completed);
}

private void EndProgress ()


{
if (estado != Estado.Cancelando)
textPI.Text = Pi();
else
textPI.Text = "Operacin cancelada";
estado = Estado.Inactivo;
buttonCalc.Text = "Calcular";
buttonCalc.Enabled = true;
}
Una vez ms, utilizaremos un delegado para nuestras hebras:

18 de 21

delegate void PiDelegate();


A continuacin, implementamos las operaciones que nos permitirn lanzar la ejecucin de las
hebras (startThread) y terminar su ejecucin de manera ordenada (endThread).
Usaremos, de forma segura, un contador auxiliar (hebras) para que slo se actualice la
interfaz de usuario cuando termine la ejecucin de la ltima hebra:

int hebras = 0;

private void startThread()


{
PiDelegate delegado = new PiDelegate(CalcularPi);
delegado.BeginInvoke(null, null);
lock (this) {
hebras++;
}
}
private void endThread ()
{
lock (this) {
hebras--;
}
if (hebras==0) {
EndProgressDelegate endProgress = new EndProgressDelegate(EndProgress);
this.Invoke(endProgress);
}
}
El clculo del valor exacto de pi lo iremos haciendo por segmentos independientes y slo
reconstruiremos su valor al terminar (para evitar posibles cuellos de botella que se
produciran si continuamente tuvisemos que estar sincronizando la ejecucin de las distintas
hebras):

19 de 21

private int
precision;
private int[] segments;
private int
current;

private void setPrecision (int precision)


{
this.precision = precision;
this.current
= 0;
this.segments = new int[precision/9+1];
}
A partir de esos "segmentos" (conjuntos de dgitos que estarn incluidos en la parte decimal
del nmero pi), la reconstruccin de pi ser inmediata:

private string Pi ()
{
StringBuilder pi = new StringBuilder("3.", precision + 2);
for (int i=0; i<segments.Length; i++) {
string ds = string.Format("{0:D9}", segments[i]);
int nuevos = Math.Min(precision - 9*i, 9);
pi.Append(ds.Substring(0, nuevos));
}
return pi.ToString();
}
Para ir repartiendo el trabajo entre las distintas hebras, definimos un mtodo auxiliar que le
indica a cada hebra qu segmento del nmero pi ha de calcular a continuacin:

private int nextSegment()


{

20 de 21

int segment = current;


lock (this) {
current++;
}
return segment;
}
Por ltimo, ya slo nos queda implementar el cuerpo de las hebras, que se encargar de ir
calculando el valor exacto de pi por segmentos:

private void CalcularPi ()


{
int segment;
ShowProgressDelegate showProgress = new ShowProgressDelegate(ShowProgress);
this.Invoke(showProgress, new object[] { 0 });
segment = nextSegment();
while ((estado!=Estado.Cancelando) && (segment<segments.Length)) {
segments[segment] = NineDigitsOfPi.StartingAt(9*segment+1);
this.Invoke(showProgress, new object[] { segment });
segment = nextSegment();
}
endThread();
}
Con estas modificaciones, podemos reducir prcticamente un 50% el tiempo de ejecucin
necesario para obtener el valor de PI con la precisin deseada si disponemos de un
procesador de doble ncleo o de un multiprocesador con dos procesadores (el ahorro sera
mayor incluso si disponemos de mayor paralelismo en el hardware y lanzamos ms hebras en
paralelo).

21 de 21

Referencias
Shawn Cicoria: Proper Threading in Winforms .NET. CodeProject, May 2003.
Chris Sells: Safe, Simple Multithreading in Windows Forms, Part 1. MSDN, June 28, 2002.
Chris Sells: Safe, Simple Multithreading in Windows Forms, Part 2. MSDN, September 2,
2002.
Chris Sells: Safe, Simple Multithreading in Windows Forms, Part 3. MSDN, January 23,
2003.
David Carmona: Programming the Thread Pool in the .NET Framework. MSDN, June 2002.

10/12/2015 20:01

Вам также может понравиться