Saltar al contenido

Comprender y evitar las condiciones de la raza en aplicaciones multihilo de C#

Para entender la necesidad de la sincronización de datos, veamos un ejemplo: Digamos que estás escribiendo una aplicación de consola de rastreo web que descarga el HTML para una URL particular y escribe los enlaces (por ejemplo.

Hacerlo de forma sincronizada sería bastante lento porque la aplicación tendría que esperar a que el HTML de un enlace termine de descargarse antes de iniciar la solicitud del siguiente. Así que para acelerar las cosas, decides hacerlo asincrónicamente utilizando un hilo separado para cada solicitud de enlace. Una simple implementación de tal rastreador web podría parecerse a lo siguiente:

Comprender y evitar las condiciones de la raza en aplicaciones multihilo de C#
Comprender y evitar las condiciones de la raza en aplicaciones multihilo de C#
123456789101112131415161718192021constint MaxLinks =8000;constint MaxThreadCount =10;string[] links;int iteration =0;// Comienza con una sola URL (una página de Wikipedia, en este ejemplo).AddLinksForUrl("https://en.wikipedia.org/wiki/Web_crawler");while((links = File. ReadAllLines("links.txt")).Length < MaxLinks){int offset =(iteration * MaxThreadCount);var tasks =newList<Task>();for(int i =0; i < MaxThreadCount &&(offset + i)< links.Length -1; i++){ tasks.Add(Task.Run(()=>AddLinksForUrl(links[offset + i]));} Task.WaitAll(tasks.ToArray()); iteration++;}

csharp

Donde AddLinksForUrl se ve algo así como:

123456789101112131415estaticevitarAddLinksForUrl(string url){string html =/* recuperar el html para dicho url */; List<string> links =/* extraer los enlaces del html */;using(var fileStream =newFileStream("links.txt", FileMode. OpenOrCreate, FileAccess.ReadWrite, FileShare.None)){ List<string> existingLinks =/* leer el contenido del archivo */;foreach(var link in links.Except(existingLinks)){ fileStream.Write(/* la URL del enlace, en bytes, más una nueva línea */);}}}

csharp

El punto clave a tener en cuenta en el algoritmo principal es que un nuevo hilo se inicia con cada llamada a Task.Run. Como definimos un MaxThreadCount de diez, se iniciarían diez hilos, luego Task.WaitAll esperaría hasta que el trabajo en todos esos hilos se completara. Después de eso, un nuevo lote de hilos se inicia en la siguiente iteración del bucle while.

Completamente implementado, este rastreador de web puede funcionar bien. Pero si lo ejecutas lo suficiente, eventualmente obtendrás una excepción de IO. ¿Y eso por qué?

Aviso en AddLinksForUrl que usamos FileShare.None para obtener acceso exclusivo a links.txt. Y con razón, ya que múltiples procesos escribiendo en el mismo archivo simultáneamente pueden causar problemas, incluyendo la corrupción de datos. Dependiendo de cuándo responden los servidores web y de cuánto tiempo se tarda en descargar el HTML, de vez en cuando nuestro rastreador web puede tener más de un hilo intentando abrir links.txt exactamente al mismo tiempo. Por lo tanto, necesitamos sincronizar el acceso al archivo links.txt, de tal manera que nunca ocurra de más de un hilo simultáneamente. Esta sincronización es necesaria para cualquier dato que se comparta entre los hilos.