viernes, 7 de septiembre de 2007

Bloqueo exclusivo de objetos en C# y VB.NET

Leyendo el libro Microsoft Windows Workflow Foundation Step by Step escrito por Kenn Scribner me encontré con una instrucción para realizar un bloqueo de exclusión mutua en objetos. Posiblemente esto sea a lo que se refiere Ciro Franz el comentario que me hizo en mi blog en BcnGeeks, y como me quedaría un comentario muy largo, prefiero hacer un post.


C# (lock)

Vamos a ver de qué se trata, en C# se realiza con la instrucción lock() que según MSDN: marca un bloque de instrucciones como una sección crucial, para lo cual utiliza el bloqueo de exclusión mutua de un objeto, la ejecución de una instrucción y, posteriormente, la liberación del bloqueo.

Vaya, otra definición extraña de MSDN, pero si seguimos leyendo lo aclaran un poco más: La instrucción lock permite garantizar que un subproceso no va a entrar en una sección crucial de código mientras otro subproceso ya se encuentre en ella. Si otro subproceso intenta entrar en un código bloqueado, esperará, o se bloqueará, hasta que el objeto se libere.

Y esto no es ni más ni menos que una forma de control de concurrencia sobre un objeto, pero lo más interesante es la forma de usarlo, esta instrucción contiene un bloque de código que se ejecuta mientras el exista el bloqueo. Mejor me explico con código:



Object objeto;

lock(objeto)
{
//codigo....
//codigo....
}

Si otro proceso intenta ejecutar la instrucción lock(), y el objeto está bloqueado, tendrá que esperar a que se acabe de ejecutar todo el bloque de código.

Vale, vamos a poner un ejemplo práctico en el que no hay bloqueos, para luego demostrar como funciona la instrucción lock. Como casi siempre, será una aplicación de consola en C#, a la que añadimos el siguiente código:




using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace PruebaLock
{
class Program
{
static void Main(string[] args)
{
//Declaramos 10 hilos que ejecuten en método EjecutarMetodoEstatico
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++)
{
Thread t = new Thread(new ThreadStart(ClaseEstatica.EjecutarMetodoEstatico));
threads[i] = t;
}

//Ejecutamos los hilos
for (int i = 0; i < 10; i++)
{
threads[i].Start();
}

//Esperamos a que se pulse una tecla para finalizar
Console.WriteLine("Pulse una tecla para salir...");
Console.ReadKey();
}
}

public static class ClaseEstatica
{
public static void EjecutarMetodoEstatico()
{

Console.WriteLine("Ejecutando bloque de código...");
// La instrucción Sleep suspende la ejecución del hilo,
// la utilizo en vez de un bloque de código que tarde
// 1 segundo en ejecutarse
Thread.Sleep(1000);
Console.WriteLine("Fin bloque de código.");
}
}
}

Ejecutamos (F5), y se obtiene este resultado:



Como podemos ver, los objetos se ejecutan en paralelo y finalizan a la vez.

Bien, vamos a probar el bloqueo, cambiamos el código que teníamos antes por este, fijaos en que el bloque en el que está la instrucción Sleep(), empieza con lock(objetoBloqueo):



using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace PruebaLock
{
class Program
{
static void Main(string[] args)
{
//Declaramos 10 hilos que ejecuten en método EjecutarMetodoEstatico
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++)
{
Thread t = new Thread(new ThreadStart(ClaseEstatica.EjecutarMetodoEstatico));
threads[i] = t;
}

//Ejecutamos los hilos
for (int i = 0; i < 10; i++)
{
threads[i].Start();
}

//Esperamos a que se pulse una tecla para finalizar
Console.WriteLine("Pulse una tecla para salir...");
Console.ReadKey();
}
}

public static class ClaseEstatica
{
// Declaramos el objeto que se va a bloquear, debe ser private
private static object objetoBloqueo = new object();

public static void EjecutarMetodoEstatico()
{
lock (objetoBloqueo)
{
Console.WriteLine("Ejecutando bloque de código...");
//La instrucción Sleep suspende la ejecución del hilo,
//la utilizo en vez de un bloque de código que tarde 1 segundo en ejecutarse
Thread.Sleep(1000);
}
Console.WriteLine("Fin bloque de código.");
}
}
}



Y el resultado:



La instrucción Console.WriteLine("Pulse una tecla para salir..."); se ejecuta la primera, porque los hilos están ocupados con los bloqueos, y no se ejecuta un hilo hasta que haya acabado el anterior, con lo que ya tenemos montado una especie de control de concurrencia pesimista.


VB.NET (Synclock)

En VB.NET la instrucción que realiza el bloqueo es Synclock() y su uso es idéntico al de lock() de C#, lo curioso es que en MSDN, esté descrita de la siguiente forma: Adquiere un bloqueo exclusivo para un bloque de instrucciones antes de ejecutar el bloque.

Si alguien lo entiende, que me lo explique, porque la definición de lock() es complicada, pero por lo menos se entiende.

Bueno, vamos a olvidarnos de las definiciones de MSDN, y vamos al mismo código de ejemplo pero en VB.NET:




Imports System.Threading

Module Module1

Sub Main()
'Declaramos 10 hilos que ejecuten en método EjecutarMetodoEstatico
Dim threads(10) As Thread
For i As Integer = 0 To 10
Dim t As Thread = New Thread(New ThreadStart(AddressOf ClaseEstatica.EjecutarMetodoEstatico))
threads(i) = t
Next

' Ejecutamos los hilos

For i As Integer = 0 To 10
threads(i).Start()
Next

' Esperamos a que se pulse una tecla para finalizar
Console.WriteLine("Pulse una tecla para salir...")
Console.ReadKey()
End Sub

End Module

Public Class ClaseEstatica

' Declaramos el objeto que se va a bloquear, debe ser private
Private Shared objetoBloqueo As Object = New Object()

Public Shared Sub EjecutarMetodoEstatico()
SyncLock objetoBloqueo

Console.WriteLine("Ejecutando bloque de código...")
' La instrucción Sleep suspende la ejecución del hilo,
' la utilizo en vez de un bloque de código que tarde 1 segundo en ejecutarse
Thread.Sleep(1000)

End SyncLock
Console.WriteLine("Fin bloque de código.")
End Sub
End Class


Si lo ejecutáis, funciona exactamente igual que el ejemplo de C#.


Conclusión

Como siempre alguno se puede preguntar ¿y para qué quiero bloquear el acceso a un bloque de código?

Pues en el libro de Kevin Scribner sirve para hacer una clase singleton que devuelve una única instancia de WorkflowRuntime a todas los procesos que llamen a una clase estática, que es una solución que a mi me parece muy interesante.

Pero está claro que si no estás realizando un proceso multi-hilo, la concurrencia no te debería importar demasiado, pero, a poco que crezca tu aplicación, los hilos serán imprescindibles y el control de concurrencia puede ser muy útil.

Aunque el uso que le deis queda en vuestras manos, o más bien abierto a vuestra imaginación.