Utilizando lock en dotNet

Los locks son mecanismos utilizados en programación concurrente para controlar el acceso a recursos compartidos y evitar problemas de sincronización. Se utilizan para garantizar que solo un thread tenga acceso a un recurso compartido en un momento dado.
A continuación, voy a mostrar cómo utilizarlo.
Usando lock
Lock se utiliza para crear una sección crítica de código que garantiza que solo un subproceso pueda acceder a ella en un momento dado. Esto ayuda a evitar problemas de sincronización entre subprocesos al acceder a recursos compartidos.
Para entenderlo un poco mejor, vamos a ver un ejemplo.
En el siguiente código se incrementa un valor entero cada vez que se ejecuta DoOperation. Observándolo, no podríamos asegurar cual va a ser la salida cuando lo ejecutemos, ya que al lanzar las tareas de forma simultanea, no sabemos el orden en el que accederán al value para modificar su valor.
Console.WriteLine("Test 1");
var lockTest1 = new Test();
await lockTest1.RunTestAsync();
public class LockExample
{
private int value;
public LockExample()
{
value = 0;
}
public void DoOperation()
{
value++;
Console.Write($"{value}, ");
value--;
}
}
public class Test
{
private readonly LockExample lockExample;
public Test()
{
lockExample = new LockExample();
}
public async Task RunTestAsync()
{
var tasks = new List<Task>();
for (int i = 0; i < 20; i++)
tasks.Add(Task.Run(() => lockExample.DoOperation()));
await Task.WhenAll(tasks);
}
}
En una ejecución síncrona el valor impreso sería siempre 1, pero al lanzar varios Tasks, en el ejemplo anterior, no sabemos cual será la salida. De hecho, lanzando 3 ejecuciones del mismo código, vemos que los resultado no coinciden.
Console.WriteLine("Test 1");
var lockTest1 = new Test();
await lockTest1.RunTestAsync();
Console.WriteLine();
Console.WriteLine();
Console.WriteLine("Test 2");
var lockTest2 = new Test();
await lockTest2.RunTestAsync();
Console.WriteLine();
Console.WriteLine();
Console.WriteLine("Test 3");
var lockTest3 = new Test();
await lockTest3.RunTestAsync();
//Output:
// Test 1
// 3, 2, 3, 1, 1, 2, 1, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1,
// Test 2
// 1, 1, 1, 3, 2, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 2, 3,
// Test 3
// 1, 1, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 2, 4, 4, 5, 1,
Si queremos asegurar que el resultado sea siempre 1, es necesario añadir un lock, que garantice que las operaciones realizadas en DoOperation se ejecutan por un único proceso a la vez. Por lo tanto, la clase LockExample quedaría como se muestra a continuación.
public class LockExample
{
private readonly object resultLock = new object();
private int value;
public LockExample()
{
value = 0;
}
public void DoOperation()
{
lock (resultLock)
{
value++;
Console.WriteLine(value);
value--;
}
}
}
Si ahora volvemos a lanzar 3 ejecuciones, como hemos hecho antes, vemos que los resultado coinciden, imprimiendo siempre el valor 1.
Console.WriteLine("Test 1");
var lockTest1 = new Test();
await lockTest1.RunTestAsync();
Console.WriteLine();
Console.WriteLine();
Console.WriteLine("Test 2");
var lockTest2 = new Test();
await lockTest2.RunTestAsync();
Console.WriteLine();
Console.WriteLine();
Console.WriteLine("Test 3");
var lockTest3 = new Test();
await lockTest3.RunTestAsync();
//Output:
// Test 1
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
// Test 2
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
// Test 3
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
Consideraciones
Hay varios puntos a tener en cuenta antes de utilizar un lock. Voy a enumerar algunos de ellos:
-
No se permiten esperas con async dentro de un bloque lock. Esta restricción está orientada a evitar bloqueos.
-
Impacto en el rendimiento. El uso excesivo de locks puede ralentizar la aplicación debido a la competencia por los recursos y la necesidad de esperar a que los bloqueos se liberen.
-
Deadlocks. Si no se gestionan adecuadamente, los bloqueos pueden llevar a situaciones de deadlock, donde varios subprocesos quedan atrapados esperando mutuamente la liberación de recursos.
-
Mayor complejidad. La utilización de locks puede aumentar la complejidad del código haciéndolo más difícil de entender, propenso a errores y más difícil de depurar.
Con todo lo anterior, podemos resumir que los locks son un mecanismo potente cuando queremos restringir el acceso a un bloque de código, pero lo tenemos que utilizar siempre que no tengamos otra alternativa, ya que tiene contraindicaciones que debemos tener en cuenta.
Foto de Muhammad Zaqy Al Fattah en Unsplash