Tutorial: Crear un componente sencillo con múltiples procesos en Visual C#
Aunque el componente BackgroundWorker reemplaza y agrega funcionalidad al espacio de nombres System.Threading, System.Threading se conserva a efectos de compatibilidad con versiones anteriores y uso futuro, si se desea.Para obtener más información, vea Información general sobre el componente BackgroundWorker.
Se pueden escribir aplicaciones que sean capaces de realizar varias tareas simultáneamente.Esta capacidad, denominada multithreading o subprocesamiento libre, es un modo eficaz de diseñar componentes que hagan un uso intensivo del procesador y requieran acción del usuario.Un ejemplo claro de un componente que podría hacer uso de multithreading sería un componente que calculara información de nóminas.Este componente podría procesar los datos que un usuario incluyó en una base de datos en un subproceso mientras, en otro, se llevan a cabo cálculos de nómina con un uso intensivo del procesador.Al ejecutar estos procesos en subprocesos separados, los usuarios no tienen que esperar a que el equipo finalice los cálculos antes de especificar datos adicionales.En este tutorial, creará un sencillo componente multiproceso que lleve a cabo varios cálculos complejos de manera simultánea.
Crear el proyecto
La aplicación constará de un único formulario y un componente.El usuario especificará los valores e indicará al componente que puede comenzar los cálculos.El formulario recibirá entonces los valores del componente y los mostrará en controles Label.El componente realizará los cálculos con un uso intensivo del procesador y le hará una señal al formulario cuando haya terminado.Se crearán variables públicas en el componente para albergar los valores recibidos de la interfaz de usuario.Además, se implementarán métodos en el componente para realizar los cálculos según los valores de estas variables.
[!NOTA]
Aunque suele ser preferible una función para un método que calcule un valor, no se pueden pasar argumentos ni devolver valores entre subprocesos.Existen muchos modos simples de suministrar valores a los subprocesos y de recibir valores de ellos.En esta demostración, se devolverán valores a la interfaz de usuario mediante la actualización de las variables públicas, y se usarán eventos para notificar al programa principal cuándo ha finalizado su ejecución un subproceso.
Los cuadros de diálogo y comandos de menú que se ven pueden diferir de los descritos en la Ayuda, en función de los valores de configuración o de edición activos.Para cambiar la configuración, elija Importar y exportar configuraciones en el menú Herramientas.Para obtener más información, vea Valores de configuración de Visual Studio.
Para crear el formulario
Crear un proyecto nuevo de aplicación para Windows.
Asigne a la aplicación el nombre Calculations y cambie el nombre Form1.cs a frmCalculations.cs.Cuando Visual Studio le pida que cambie el nombre del elemento de código Form1, haga clic en Sí.
Este formulario servirá como interfaz de usuario principal de la aplicación.
Agregue al formulario cinco controles Label, cuatro controles Button y un control TextBox.
Establezca las propiedades de estos controles de la manera siguiente:
Control
Name
Text
label1
lblFactorial1
(en blanco)
label2
lblFactorial2
(en blanco)
label3
lblAddTwo
(en blanco)
label4
lblRunLoops
(en blanco)
label5
lblTotalCalculations
(en blanco)
button1
btnFactorial1
Factorial
button2
btnFactorial2
Factorial - 1
button3
btnAddTwo
Add Two
button4
btnRunLoops
Run a Loop
textBox1
txtValue
(en blanco)
Para crear el componente Calculator
En el menú Proyecto, elija Agregar componente.
Asigne al componente el nombre Calculator.
Para agregar variables públicas al componente Calculator
Abra Calculator en el Editor de código.
Agregue instrucciones para crear las variables públicas que usará para pasar los valores de frmCalculations a cada subproceso.
La variable varTotalCalculations mantendrá un total actualizado del número de cálculos realizados por el componente, y las demás variables recibirán sus valores del formulario.
public int varAddTwo; public int varFact1; public int varFact2; public int varLoopValue; public double varTotalCalculations = 0;
Para agregar métodos y eventos al componente Calculator
Declare los delegados para los eventos que usará el componente para comunicar valores al formulario.
[!NOTA]
Aunque tendrá que declarar cuatro eventos, sólo tendrá que crear tres delegados, ya que dos de los eventos tendrán la misma firma.
Inmediatamente debajo de las declaraciones de las variables realizadas en el paso anterior, escriba el código siguiente:
// This delegate will be invoked with two of your events. public delegate void FactorialCompleteHandler(double Factorial, double TotalCalculations); public delegate void AddTwoCompleteHandler(int Result, double TotalCalculations); public delegate void LoopCompleteHandler(double TotalCalculations, int Counter);
Declare los eventos que usará el componente para comunicarse con la aplicación.Para ello, agregue el código siguiente inmediatamente debajo del código agregado en el paso anterior.
public event FactorialCompleteHandler FactorialComplete; public event FactorialCompleteHandler FactorialMinusOneComplete; public event AddTwoCompleteHandler AddTwoComplete; public event LoopCompleteHandler LoopComplete;
Inmediatamente debajo del código que escribió en el paso anterior, escriba el código siguiente:
// This method will calculate the value of a number minus 1 factorial // (varFact2-1!). public void FactorialMinusOne() { double varTotalAsOfNow = 0; double varResult = 1; // Performs a factorial calculation on varFact2 - 1. for (int varX = 1; varX <= varFact2 - 1; varX++) { varResult *= varX; // Increments varTotalCalculations and keeps track of the current // total as of this instant. varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations; } // Signals that the method has completed, and communicates the // result and a value of total calculations performed up to this // point. FactorialMinusOneComplete(varResult, varTotalAsOfNow); } // This method will calculate the value of a number factorial. // (varFact1!) public void Factorial() { double varResult = 1; double varTotalAsOfNow = 0; for (int varX = 1; varX <= varFact1; varX++) { varResult *= varX; varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations; } FactorialComplete(varResult, varTotalAsOfNow); } // This method will add two to a number (varAddTwo+2). public void AddTwo() { double varTotalAsOfNow = 0; int varResult = varAddTwo + 2; varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations; AddTwoComplete(varResult, varTotalAsOfNow); } // This method will run a loop with a nested loop varLoopValue times. public void RunALoop() { int varX; double varTotalAsOfNow = 0; for (varX = 1; varX <= varLoopValue; varX++) { // This nested loop is added solely for the purpose of slowing down // the program and creating a processor-intensive application. for (int varY = 1; varY <= 500; varY++) { varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations; } } LoopComplete(varTotalAsOfNow, varLoopValue); }
Transferir datos proporcionados por el usuario al componente
El paso siguiente consiste en agregar código a frmCalculations para recibir datos proporcionados por el usuario y para transferir y recibir valores a y desde el componente Calculator.
Para implementar la funcionalidad de front-end en frmCalculations
Abra frmCalculations en el Editor de código.
Busque la instrucción public partial class frmCalculations.Inmediatamente debajo del tipo {:
Calculator Calculator1;
Busque el constructor.Inmediatamente antes del carácter }, agregue la línea siguiente:
// Creates a new instance of Calculator. Calculator1 = new Calculator();
En el diseñador, haga clic en todos los botones para generar el esquema de código para los controladores de eventos Click de cada control y agregue el código necesario para crear los controladores.
Cuando haya finalizado, los controladores de los eventos de Click deberían tener un aspecto similar a éste:
// Passes the value typed in the txtValue to Calculator.varFact1. private void btnFactorial1_Click(object sender, System.EventArgs e) { Calculator1.varFact1 = int.Parse(txtValue.Text); // Disables the btnFactorial1 until this calculation is complete. btnFactorial1.Enabled = false; Calculator1.Factorial(); } private void btnFactorial2_Click(object sender, System.EventArgs e) { Calculator1.varFact2 = int.Parse(txtValue.Text); btnFactorial2.Enabled = false; Calculator1.FactorialMinusOne(); } private void btnAddTwo_Click(object sender, System.EventArgs e) { Calculator1.varAddTwo = int.Parse(txtValue.Text); btnAddTwo.Enabled = false; Calculator1.AddTwo(); } private void btnRunLoops_Click(object sender, System.EventArgs e) { Calculator1.varLoopValue = int.Parse(txtValue.Text); btnRunLoops.Enabled = false; // Lets the user know that a loop is running lblRunLoops.Text = "Looping"; Calculator1.RunALoop(); }
Debajo del código que agregó en el paso anterior, escriba lo siguiente para procesar los eventos que recibirá el formulario desde Calculator1:
private void FactorialHandler(double Value, double Calculations) // Displays the returned value in the appropriate label. { lblFactorial1.Text = Value.ToString(); // Re-enables the button so it can be used again. btnFactorial1.Enabled = true; // Updates the label that displays the total calculations performed lblTotalCalculations.Text = "TotalCalculations are " + Calculations.ToString(); } private void FactorialMinusHandler(double Value, double Calculations) { lblFactorial2.Text = Value.ToString(); btnFactorial2.Enabled = true; lblTotalCalculations.Text = "TotalCalculations are " + Calculations.ToString(); } private void AddTwoHandler(int Value, double Calculations) { lblAddTwo.Text = Value.ToString(); btnAddTwo.Enabled = true; lblTotalCalculations.Text = "TotalCalculations are " + Calculations.ToString(); } private void LoopDoneHandler(double Calculations, int Count) { btnRunLoops.Enabled = true; lblRunLoops.Text = Count.ToString(); lblTotalCalculations.Text = "TotalCalculations are " + Calculations.ToString(); }
En el constructor de frmCalculations, agregue el siguiente código inmediatamente antes del carácter } para controlar los eventos personalizados que el formulario recibirá de Calculator1.
Calculator1.FactorialComplete += new Calculator.FactorialCompleteHandler(this.FactorialHandler); Calculator1.FactorialMinusOneComplete += new Calculator.FactorialCompleteHandler(this.FactorialMinusHandler); Calculator1.AddTwoComplete += new Calculator.AddTwoCompleteHandler(this.AddTwoHandler); Calculator1.LoopComplete += new Calculator.LoopCompleteHandler(this.LoopDoneHandler);
Probar la aplicación
Ha creado un proyecto que incorpora un formulario y un componente capaz de realizar varios cálculos complejos.Aunque no ha implementado aún la capacidad de multithreading, probará el proyecto para comprobar su funcionalidad antes de continuar.
Para probar el proyecto
En el menú Depurar, elija Iniciar depuración.
La aplicación se inicia y aparece frmCalculations.
En el cuadro de texto, escriba 4 y, a continuación, haga clic en el botón Agregar dos.
La etiqueta que aparece debajo debe mostrar el número "6" y "Total Calculations are 1" debe aparecer en lblTotalCalculations.
Ahora, haga clic en el botón con la etiqueta Factorial - 1.
Debería aparecer el numeral "6" debajo del botón, y "Total Calculations are 4" en lblTotalCalculations.
Cambie el valor del cuadro de texto por 20 y, a continuación, haga clic en el botón Factorial.
Debería aparecer el número "2.43290200817664E+18" debajo del botón, y "Total Calculations are 24" en lblTotalCalculations.
Cambie el valor del cuadro de texto a 50000; a continuación, haga clic en el botón denominado Ejecutar un bucle.
Observe que hay un pequeño pero perceptible intervalo antes de que el botón vuelva a estar habilitado.La etiqueta bajo este botón debería indicar "50000" y el total de cálculos mostrado debería ser "25000024".
Cambie el valor del cuadro de texto a 5000000 y haga clic en el botón Ejecutar un bucle; a continuación, haga clic inmediatamente en el botón Agregar dos.Haga clic en él de nuevo.
El botón no responde, ni lo hará ningún control del formulario hasta que el bucle haya finalizado.
Si el programa ejecuta sólo un subproceso de ejecución, los cálculos que precisan un gran trabajo por parte del procesador, como en el ejemplo anterior, tienden a acaparar el programa hasta que han acabado los cálculos.En la sección siguiente, agregará la capacidad de multithreading a la aplicación de forma que varios subprocesos puedan ejecutarse a la vez.
Agregar capacidad de multithreading
El ejemplo anterior mostraba las limitaciones de las aplicaciones que ejecutan un único subproceso.En la sección siguiente, utilizará el objeto de clase Thread para agregar varios subprocesos de ejecución al componente.
Para agregar la subrutina Threads
Abra Calculator.cs en el Editor de código.
Cerca de la parte superior del código, busque la declaración de clase e inmediatamente debajo de {, escriba lo siguiente:
// Declares the variables you will use to hold your thread objects. public System.Threading.Thread FactorialThread; public System.Threading.Thread FactorialMinusOneThread; public System.Threading.Thread AddTwoThread; public System.Threading.Thread LoopThread;
Justo antes del final de la declaración de clase, en la parte inferior del código, agregue el método siguiente:
public void ChooseThreads(int threadNumber) { // Determines which thread to start based on the value it receives. switch(threadNumber) { case 1: // Sets the thread using the AddressOf the subroutine where // the thread will start. FactorialThread = new System.Threading.Thread(new System.Threading.ThreadStart(this.Factorial)); // Starts the thread. FactorialThread.Start(); break; case 2: FactorialMinusOneThread = new System.Threading.Thread(new System.Threading.ThreadStart(this.FactorialMinusOne)); FactorialMinusOneThread.Start(); break; case 3: AddTwoThread = new System.Threading.Thread(new System.Threading.ThreadStart(this.AddTwo)); AddTwoThread.Start(); break; case 4: LoopThread = new System.Threading.Thread(new System.Threading.ThreadStart(this.RunALoop)); LoopThread.Start(); break; } }
Cuando se crea una instancia de un objeto Thread, requiere un argumento en la forma de un objeto ThreadStart.El ThreadStart es un delegado que señala a la dirección del método donde debe comenzar el subproceso.Un ThreadStart no puede aceptar parámetros ni pasar valores y, por tanto, sólo puede indicar un método void.El método ChooseThreads que acaba de implementar recibirá un valor del programa que lo ha llamado y usará ese valor para determinar el subproceso determinado que debe iniciar.
Para agregar el código correspondiente a frmCalculations
Abra el archivo frmCalculations.cs en el Editor de código y, a continuación, busque private void btnFactorial1_Click.
Convierta en comentario la línea que llama al método Calculator1.Factorial1 directamente, como se muestra a continuación:
// Calculator1.Factorial()
Agregue la línea siguiente para llamar al método Calculator1.ChooseThreads:
// Passes the value 1 to Calculator1, thus directing it to start the // correct thread. Calculator1.ChooseThreads(1);
Haga modificaciones similares en el resto de los métodos button_click.
[!NOTA]
Asegúrese de incluir el valor apropiado para el argumento Threads.
Cuando haya finalizado, el código deberá ser similar al siguiente:
private void btnFactorial1_Click(object sender, System.EventArgs e) // Passes the value typed in the txtValue to Calculator.varFact1 { Calculator1.varFact1 = int.Parse(txtValue.Text); // Disables the btnFactorial1 until this calculation is complete btnFactorial1.Enabled = false; // Calculator1.Factorial(); Calculator1.ChooseThreads(1); } private void btnFactorial2_Click(object sender, System.EventArgs e) { Calculator1.varFact2 = int.Parse(txtValue.Text); btnFactorial2.Enabled = false; // Calculator1.FactorialMinusOne(); Calculator1.ChooseThreads(2); } private void btnAddTwo_Click(object sender, System.EventArgs e) { Calculator1.varAddTwo = int.Parse(txtValue.Text); btnAddTwo.Enabled = false; // Calculator1.AddTwo(); Calculator1.ChooseThreads(3); } private void btnRunLoops_Click(object sender, System.EventArgs e) { Calculator1.varLoopValue = int.Parse(txtValue.Text); btnRunLoops.Enabled = false; // Lets the user know that a loop is running lblRunLoops.Text = "Looping"; // Calculator1.RunALoop(); Calculator1.ChooseThreads(4); }
Llamadas de cálculo de referencias a controles
A continuación, facilitará la actualización de la presentación del formulario.Como es el subproceso de ejecución principal el que posee siempre los controles, cualquier llamada a un control de un subproceso secundario requiere utilizar una llamada auxiliar de cálculo de referencias (marshaling).El cálculo de referencias es el acto de pasar una llamada a través de límites de subprocesos y consume muchos recursos.Para minimizar los cálculos de referencias necesarios, y para garantizar que las llamadas se administran de un modo seguro desde el punto de vista de los subprocesos, utilizará el método Control.BeginInvoke para invocar métodos en el subproceso de ejecución principal, reduciendo así la cantidad de cálculos de referencias entre los límites de los subprocesos que se deben producir.Este tipo de llamada es necesaria cuando se llama a métodos que manipulan controles.Para obtener información detallada, vea Cómo: Manipular controles a partir de subprocesos.
Para crear los procedimientos que invocan a controles
Abra el Editor de código para frmCalculations.En la sección de declaraciones, agregue el código siguiente:
public delegate void FHandler(double Value, double Calculations); public delegate void A2Handler(int Value, double Calculations); public delegate void LDHandler(double Calculations, int Count);
Los métodos Invoke y BeginInvoke requieren un delegado del método correspondiente como argumento.Estas líneas declaran las firmas de delegado que utilizará BeginInvoke para invocar los métodos correspondientes.
Agregue los siguientes métodos vacíos al código.
public void FactHandler(double Value, double Calculations) { } public void Fact1Handler(double Value, double Calculations) { } public void Add2Handler(int Value, double Calculations) { } public void LDoneHandler(double Calculations, int Count) { }
En el menú Edición, use Cortar y Pegar para cortar todo el código del método FactorialHandler y pegarlo en FactHandler.
Repita el paso anterior para FactorialMinusHandler, Fact1Handler, AddTwoHandler,Add2Handler, LoopDoneHandler y LDoneHandler.
Cuando haya finalizado no debería quedar código FactorialHandler, Factorial1Handler, AddTwoHandler y LoopDoneHandler, y todo el código que contuvieran debería haberse movido a los correspondientes métodos nuevos.
Llame al método BeginInvoke para invocar a los métodos de forma asincrónica.Puede llamar al método BeginInvoke desde el formulario (this) o desde cualquiera de los controles del formulario.
Una vez terminado, el código debe ser similar al siguiente:
protected void FactorialHandler(double Value, double Calculations) { // BeginInvoke causes asynchronous execution to begin at the address // specified by the delegate. Simply put, it transfers execution of // this method back to the main thread. Any parameters required by // the method contained at the delegate are wrapped in an object and // passed. this.BeginInvoke(new FHandler(FactHandler), new Object[] {Value, Calculations}); } protected void FactorialMinusHandler(double Value, double Calculations) { this.BeginInvoke(new FHandler(Fact1Handler), new Object [] {Value, Calculations}); } protected void AddTwoHandler(int Value, double Calculations) { this.BeginInvoke(new A2Handler(Add2Handler), new Object[] {Value, Calculations}); } protected void LoopDoneHandler(double Calculations, int Count) { this.BeginInvoke(new LDHandler(LDoneHandler), new Object[] {Calculations, Count}); }
Puede parecer que el controlador de eventos está haciendo sólo una llamada al método siguiente.Sin embargo, da lugar a que se invoque un método en el subproceso principal.Este sistema ahorra llamadas fuera de los límites de los subprocesos y permite a las aplicaciones multiproceso ejecutarse de forma eficiente y sin peligro de que se produzcan bloqueos.Para obtener más detalles sobre cómo trabajar con controles en un entorno multiproceso, vea Cómo: Manipular controles a partir de subprocesos.
Guarde su trabajo.
Pruebe la solución; para ello, en el menú Depurar elija Iniciar depuración.
Escriba 10000000 en el cuadro de texto y haga clic en Ejecutar un bucle.
Aparecerá "Looping" en la etiqueta situada debajo del botón.Este bucle debe tardar un tiempo considerable en ejecutarse.Si finaliza demasiado pronto, ajuste el tamaño del número en consecuencia.
En una sucesión rápida, haga clic en los tres botones que están habilitados.Verá que todos los botones responden a su acción.La etiqueta situada debajo de Agregar dos debería ser la primera en mostrar un resultado.Los resultados se mostrarán posteriormente en las etiquetas situadas debajo de los botones factoriales.Estos resultados se evalúan a infinito, ya que el número devuelto por un factorial 10.000.000 es demasiado grande para que lo contenga una variable de doble precisión.Finalmente, después de un intervalo adicional, los resultados se devuelven debajo del botón Ejecutar un bucle.
Como habrá podido observar, se han realizado cuatro conjuntos de cálculos independientes de forma simultánea en cuatro subprocesos independientes.La interfaz de usuario permanece receptiva a las entradas y los resultados se devolvieron después de que finalizara cada subproceso.
Coordinar los subprocesos
Un usuario con experiencia en aplicaciones multiproceso puede percibir un sutil defecto con el código tal y como está escrito.Llame de nuevo a las líneas de código desde los métodos de cálculo de Calculator:
varTotalCalculations += 1;
varTotalAsOfNow = varTotalCalculations;
Estas dos líneas de código incrementan la variable pública varTotalCalculations y asignan su valor a la variable local varTotalAsOfNow este valor.Dicho valor se devuelve a frmCalculations y se muestra en un control de etiqueta.Pero no se sabe si el valor es correcto.Si sólo se está ejecutando un subproceso, la respuesta claramente es sí.Pero si se están ejecutando varios subprocesos, la respuesta se vuelve incierta.Cada subproceso tiene la capacidad de incrementar la variable varTotalCalculations.Por tanto, es posible que, después de que un subproceso incremente esta variable, pero antes de que copie el valor en varTotalAsOfNow, otro subproceso altere el valor de esta variable incrementándolo.Esto nos lleva a la posibilidad de que cada subproceso está, en realidad, suministrando resultados inexactos.Visual C# proporciona lock (Instrucción, Referencia de C#) para permitir la sincronización de los subprocesos con el fin de garantizar que cada uno de ellos devuelva siempre un resultado exacto.La sintaxis de lock es la siguiente:
lock(AnObject)
{
// Insert code that affects the object.
// Insert more code that affects the object.
// Insert more code that affects the object.
// Release the lock.
}
Cuando se entra en el bloque lock, la ejecución sobre la expresión especificada se bloquea hasta que el subproceso especificado dispone de un bloqueo exclusivo sobre el objeto en cuestión.En el ejemplo anterior, la ejecución queda bloqueada en AnObject.lock se debe utilizar con un objeto que devuelva una referencia en vez de un valor.Entonces, la ejecución puede continuar como un bloque sin interferencias de otros subprocesos.Un conjunto de instrucciones que se ejecutan como una unidad se denomina atómico.Cuando se encuentra }, se libera la expresión y se permite a los subprocesos ejecutarse normalmente.
Para agregar la instrucción lock a la aplicación
Abra Calculator.cs en el Editor de código.
Busque todas las instancias del código siguiente:
varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations;
Debería haber cuatro instancias de este código, una en cada método de cálculo.
Modifique este código de modo que sea como el siguiente:
lock(this) { varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations; }
Guarde el trabajo y pruébelo como en el ejemplo anterior.
Puede notar un leve impacto en el rendimiento del programa.Esto se debe a que la ejecución de los subprocesos se detiene cuando se obtiene un bloque exclusivo en el componente.Aunque garantiza la precisión, esta solución impide algunas de las ventajas de rendimiento de varios subprocesos.Por tanto, debería considerar concienzudamente la necesidad de bloquear los subprocesos e implementarlos sólo cuando sea absolutamente necesario.
Vea también
Tareas
Cómo: Coordinar varios subprocesos de ejecución
Tutorial: Crear un componente sencillo con múltiples procesos en Visual Basic
Referencia
Conceptos
Información general sobre el modelo asincrónico basado en eventos