Crear una aplicación que utiliza múltiples hilos (threads) en Visual Basic .NET y C#Ejemplo para contar las palabras contenidas en ficheros |
Publicado el 19/Ene/2004
|
Las palabras se almacenan en una colección personalizada llamada cPalabras que almacena objetos del tipo cPalabra. Dicha colección personalizada utiliza una una colección del tipo Hashtable. Como sabrás, las colecciones de este tipo siempre almacenan un valor tipo DictionaryEntry con un valor para la clave (Key) y el valor propiamente dicho (Value). En la clave almacenamos el ID de cada palabra, que en realidad es la propia palabra hallada, en el valor almacenamos el objeto del tipo cPalabra, que, en esta versión del "buscador" de palabras, nos dirá también cuantas veces se ha encontrado dicha palabra.
Aquí tienes una captura del programa después de haber analizado el directorio con la copia local de lo que hay en mi Web:
Como puedes comprobar lo que más abunda son tags de HTML, por eso hay tantas p, a, etc., ya que eso realmente son: <p>, <a...>, etc.
Veamos "paso a paso" cada una de las cosas que hace el código, y de camino te iré explicando qué es lo que hago y dónde puede estar el problema.
Primero te diré las clases que se utilizan y para que sirve cada una de ellas:
Clase Para que sirve cPalabra Se deriva de cContenido, que a su vez se deriva de cID, de forma que tenga las propiedades de cada una de estas clases... esto no es realmente necesario hacerlo así, ya que se podía haber creado la clase directamente con esas propiedades, pero...
De esta clase se utiliza el ID, el Contenido y el total de veces que está esa palabra.cPalabras Colección para almacenar objetos del tipo cPalabra.
Internamente usa una colección del tipo Hashtable.cProcesarFicheroThread Una clase para procesar cada directorio y desglosar cada una de las palabras de los ficheros hallados que cumplan la especificación.
Internamente utiliza una colección compartida del tipo cPalabras, para almacenar todas las palabras halladas. Al estar compartida se usará por todas las instancias de esta clase.
Tiene dos campos (cuasi-propiedades) compartidos para almacenar el total de directorios y ficheros procesados.
Tiene otros tres campos para indicar el directorio a procesar, la extensión de los ficheros y el thread asignado para procesar ese directorio.
El método ProcesarDir será usado por cada hilo. Debido a que un procedimiento usado en un thread no puede recibir parámetros, se utilizan las propiedades sDir y sExt para saber "qué" buscar.
Esta clase produce dos eventos que nos informará de cada directorio y fichero que se está procesando.cBuscarPalabrasEnFicheros Esta clase es la que se encarga de hacer todo el trabajo.
Tiene un método público: Procesar que será el que se encargue de crear los objetos del tipo cProcesarFicheroThread y asignar los threads que se usarán.
Realmente utiliza un array del tipo cProcesarFicheroThread, cada elemento de dicho array se encargará de procesar un directorio diferente. En la versión de C#, en lugar de usar un array, utilizo una colección del tipo ArrayList. Es posible que en el código definitivo incluido en el fichero zip se use también una colección.
Esta clase produce tres eventos para informarnos de lo que va sucediendo.fBuscarPalabras Es el formulario usado para que el usuario indique qué es lo que hay que buscar y dónde buscarlo... Una vez procesados, se mostrarán en un ListView las palabras halladas. Veamos ahora, con un poco más de detalle, que hace cada una de esas clases, realmente no te voy a "pormenorizar" cada línea de código, simplemente resaltaré lo que crea conveniente resaltar.
Desde el formulario llamamos al método Procesar de la clase cBuscarPalabrasEnFicheros, ésta clase produce tres eventos para informarnos de lo que va sucediendo:
ProcesandoDirectorio, se produce cada vez que se empieza a procesar un nuevo directorio.
ProcesandoFichero, idem pero para cada fichero procesado.
FicherosProcesados, se produce cuando se ha terminado de procesar todo lo que había que procesar.
En un momento te explico cómo se sincronizan estos eventos con los de las clases que cada thread utiliza.El método Procesar se encarga de crear una (o varias) nueva instancia de la clase cProcesarFicheroThread que será la encargada de procesar cada uno de los directorios indicados.
Si se procesa sólo un directorio, se usa sólo una clase del tipo cProcesarFicheroThread.
Pero si son varios los directorios a procesar (porque se ha seleccionado "con subdirs" en el formulario), se creará un array de dicha clase y un método recursivo, el cual será llamado para uno de los directorios a procesar.En esta clase, he creado dos métodos, los cuales se usarán para interceptar los eventos de las clases que se encargan de procesar los directorios en hilos diferentes. De forma que cada vez que la clase "buscadora" produzca un evento, se llamen a estos métodos. Desde estos métodos se producirán los eventos que notificarán al formulario que algo ha sucedido.
Veamos el código de esos dos métodos y después veremos cómo "engancharlos" con los de cada clase:
' Estos métodos se usarán para producir los eventos de esta clase. Protected Sub OnProcesandoFichero(ByVal sMsg As String) ' Este evento se produce cuando se está procesando un fichero ' por uno de los threads. ' Desde aquí producimos nuestro evento a la aplicación. RaiseEvent ProcesandoFichero(sMsg) End Sub Protected Sub OnProcesandoDirectorio(ByVal sMsg As String) ' Este evento se produce cuando se está procesando un directorio ' por uno de los threads. ' Desde aquí producimos nuestro evento a la aplicación. RaiseEvent ProcesandoDirectorio(sMsg) End SubOnProcesandoFichero se encargará de producir el evento ProcesandoFichero y OnProcesandoDirectorio será el encargado de producir el otro: ProcesandoDirectorio. Esos dos eventos serán interceptados por el formulario para poder ir mostrando al usuario la información de qué es lo que está haciendo nuestra aplicación.
Ahora veremos cómo indicarle a cada una de las clases que se ejecutarán en hilos independientes cómo llamar a estos métodos.
La clase cBuscarPalabrasEnFicheros utiliza un array, declarado a nivel de módulo, llamado mProcesarFic del tipo cProcesarFicheroThread, que será el que realmente se utilice para cada uno de los hilos que se van a crear. En el código de C# en lugar de usar un array se utiliza una colección del tipo ArrayList y es posible que en el código completo de VB incluido en el zip se incluya la versión del código de Visual Basic que use la colección en lugar del array.
Cada vez que se vaya a procesar un directorio crearemos una nueva instancia de la clase, le indicaremos qué directorio debe procesar, la extensión de los ficheros que queremos procesar y qué método debe usarse para notificarnos de que algo ha ocurrido. También crearemos un nuevo hilo y le indicaremos qué método debe usarse en ese hilo. El método es uno de la propia clase que instanciamos y lo asignamos al campo esteThread de la propia clase. De esta forma no hace falta mantener un array con los threads y otro con las clases que procesará cada directorio. No se si esta forma tendrá menos sobrecarga que usando dos arrays independientes, (tal como hacía en el código de la beta1), pero creo que así será más claro y todo estará "más encapsulado".
Veamos el código que hace todas estas asignaciones:' redimensionamos el array nThread += 1 ReDim Preserve mProcesarFic(nThread - 1) ' creamos una nueva instancia de la clase ' a la que le pasamos la colección "compartida" de palabras mProcesarFic(nThread - 1) = New Guille.Clases.cProcesarFicheroThread(lasPalabras) ' asignamos los "manejadores" de eventos AddHandler mProcesarFic(nThread - 1).ProcesandoFichero, AddressOf Me.OnProcesandoFichero AddHandler mProcesarFic(nThread - 1).ProcesandoDirectorio, AddressOf Me.OnProcesandoDirectorio ' asignamos el directorio y la extensión a procesar mProcesarFic(nThread - 1).sDir = sDir mProcesarFic(nThread - 1).sExt = sExt ' creamos un nuevo thread mProcesarFic(nThread - 1).esteThread = New Thread(New ThreadStart(AddressOf mProcesarFic(nThread - 1).ProcesarDir)) ' iniciamos el nuevo thread mProcesarFic(nThread - 1).esteThread.Start()La variable nThread tendrá el total de hilos que se están ejecutando, inicialmente valdrá cero. Realmente nos servirá para poder crear un nuevo elemento del array de la clase que procesará cada directorio.
Después de crear una nueva instancia de la clase, la añadimos al array y le asignamos los procedimientos que se usarán para los dos eventos que dicha clase producirá.
A continuación asignamos el directorio y la extensión de los ficheros que se procesarán. Esto es necesario, porque el método que se encarga de procesar cada directorio, el usado con el hilo, no puede recibir parámetros, por tanto de alguna forma debe saber dónde buscar...
Finalmente creamos un nuevo hilo al que le indicamos qué método debe usar e iniciamos dicho hilo.
Nota:
En la asignación/creación del nuevo hilo he usado New ThreadStart que es el delegado usado por el constructor de la clase Thread. En Visual Basic .NET realmente no hace falta hacerlo así, ya que al usar AddessOf el compilador "sabe" que nuestra intención es usar un delegado, por tanto dicha asignación se podría hacer de esta otra forma:
New Thread(AddressOf mProcesarFic(nThread - 1).ProcesarDir)Desde este momento, será el propio sistema el que se encargará de que los threads se vayan ejecutando.
El método ProcesarDir de la clase cProcesarFicheroThread se encargará de recorrer todos los ficheros del directorio indicado, (que tengan la extensión especificada), leerá el contenido de cada uno de ellos y desglosará dicho contenido en palabras, (o números). Para ese desglose se utiliza el método Split de la clase String, de forma que se le pase como parámetro un array de los caracteres que queremos usar como separador. Realmente lo que hacemos es crear un array con cada una de las palabras que contenga el fichero y usamos dicho array para comprobar si la palabra está o no en la colección.
Para crear ese array, se usa la cadena sSep, la cual una vez convertida a un array de tipo Char, podemos usar con el método Split.
Esto se hace en el método desglosar, tal como se muestra a continuación:Private Sub desglosar(ByVal sText As String, ByVal sFic As String) ' Desglosar en palabras individuales el contenido del parámetro ' ' Estos signos se considerarán separadores de palabras Dim sSep As String = ".,;: ()<>[]{}¿?¡!/\'=+*-%&$|#@" & ChrW(34) & ChrW(9) & ChrW(10) & ChrW(13) ' crear un array con cada una de las palabras Dim palabras() As String = sText.Split(sSep.ToCharArray) Dim tPalabra As Guille.Clases.cPalabra Dim i As Integer ' For i = 0 To palabras.Length - 1 Dim s As String = palabras(i) ' sólo si no es una cadena vacía If s <> "" Then If lasPalabras.Exists(s) = False Then tPalabra = New Guille.Clases.cPalabra tPalabra.ID = s tPalabra.Veces = 1 ' el fichero en el que inicialmente apareció tPalabra.Contenido = sFic lasPalabras.Add(tPalabra) Else ' Incrementar el número de esta palabra 'lasPalabras(s).Veces += 1 lasPalabras(s).IncVeces() End If End If Next ' End SubEn el bucle comprobamos si la palabra está o no en la colección.
En caso de que no esté, se crea un nuevo objeto del tipo cPalabra y se añade a la colección.
En ese método Add de la colección hay que tener precaución. Ya que se utiliza este código para saber si se debe o no añadir la palabra a la colección:If m_ht.ContainsKey(tPalabra.ID) = False Then m_ht.Add(tPalabra.ID, tPalabra) End IfRealmente no hace falta hacer esa comprobación extra, pero... ese no sería precisamente el "punto problemático". El problema se podría presentar en el siguiente caso, cuando se hace esta asignación en la colección interna:
m_ht.Add(tPalabra.ID, tPalabra)
Si el ID indicado ya existe en la colección, se producirá una excepción.
Seguramente te dirás: Vale... pero si se comprueba antes que sólo se ejecute esa línea si no existe ese ID... ¿cómo se va a producir una excepción?
Imagínate que después de hacer esa comprobación, (y la correspondiente llamada al método Add de la colección interna), el sistema cambia de hilo y precisamente el hilo anterior se interrumpió justo después de comprobar que el ID no estaba en la colección, pero no llegó a añadirlo... lo que ocurrirá es que al querer añadir un elemento que ya existe, se producirá una excepción... echando al traste todo lo que hemos hecho...No se si has entendido este punto. Te lo aclaro mejor para que no te queden dudas:
Supongamos que tenemos dos hilos y que cada uno de estos hilos está comprobando la misma palabra.
El primero ejecuta el If m_ht.ContainsKey(..., pero justo en ese momento, el sistema operativo congela ese hilo y pasa a otro hilo el control de la ejecución, el cual "por casualidad", ejecuta esa misma comparación, pero el sistema le da un poco de más tiempo, y llega a ejecutar también lo que hay después del If, añadiendo esa palabra a la colección. Justo después de añadir la palabra a la colección, el sistema vuelve a darle el control al primer hilo, como resulta que la palabra que se comprobaba en ese primer hilo no estaba en la colección, se cumple la condición y se va a ejecutar el método Add. Pero ese método fallará, porque el segundo hilo ya añadió la palabra a la colección.Por tanto, para evitar esta situación, tenemos que decirle al sistema operativo:
¡Déjame hacer mi trabajo completo y tendremos menos problemas!
Vale, mu bonito, pero... ¿cómo se lo decimos?
Usando la instrucción SyncLock en Visual Basic .NET o lock en C#.Esto es lo que dice la ayuda de Visual Studio .NET sobre esta instrucción:
La instrucción SyncLock garantiza que múltiples subprocesos no ejecuten las mismas instrucciones al mismo tiempo. Cuando el subproceso llega al bloque SyncLock, evalúa la expresión y mantiene esta exclusividad hasta que obtenga un bloqueo en el objeto devuelto por la expresión. Esto impide que una expresión cambie valores durante la ejecución de varios subprocesos, lo que puede proporcionar resultados inesperados del código.En nuestro caso, se utiliza la propia clase como expresión a evaluar, ya que dicha expresión debe ser un tipo por referencia.
El código quedaría de esta forma:
Public Overridable Sub Add(ByVal tPalabra As cPalabra) ' Añadir un nuevo elemento a la colección ' ' bloquear este método por si se usan Threads (17/Ene/04) SyncLock Me.GetType If m_ht.ContainsKey(tPalabra.ID) = False Then m_ht.Add(tPalabra.ID, tPalabra) End If End SyncLock End SubPero lo mismo nos puede ocurrir si resulta que la palabra ya está en la colección.
En ese caso, se usa este código:lasPalabras(s).IncVeces()Aunque es posible que pienses que la sincronización habría que hacerla en el método IncVeces de la clase cPalabra, el problema no estará ahí.
Ya que realmente esa llamada al método IncVeces se hace por medio de lasPalabras(s), que no es ni más ni menos que una llamada al bloque Get de la propiedad Item de la colección cPalabras. Cuantas vueltas da nuestro código ¿verdad?Para comprenderlo mejor, voy a sustituir esa llamada al método IncVeces() por el que de verdad ve el compilador de Visual Basic .NET:
lasPalabras.Item(s).IncVeces()Esto es así porque la propiedad Item es la propiedad predeterminada (el indizador si el código fuese en C#).
Por tanto, antes de llamar al método IncVeces se llama al bloque Get de la propiedad "predeterminada" Item; esa propiedad devuelve un objeto del tipo cPalabra que tenga el ID (o clave) indicado en el parámetro. El código usado en esa propiedad (que es de solo lectura), es el siguiente:
Default Public Overridable ReadOnly Property Item(ByVal sIndex As String) As cPalabra Get ' Método predeterminado Dim tPalabra As cPalabra ' If m_ht.ContainsKey(sIndex) Then Return CType(m_ht(sIndex), cPalabra) Else ' Creamos una nuevo objeto tPalabra = New cPalabra tPalabra.ID = sIndex ' lo añadimos a la colección m_ht.Add(sIndex, tPalabra) ' Return tPalabra End If End Get End PropertyY si has comprendido el problema que se podía producir en el método Add, aquí tenemos algo parecido, ya que se comprueba si dicha clave está en la colección, en caso de que esté, se devuelve el objeto correspondiente, y si no existe, se añade a la colección.
Debes tener presente que en el código que estoy utilizando nunca se cumplirá que no esté dicha clave en la colección, y por tanto nunca se producirá una excepción desde la propiedad Item.
Pero en otras circunstancias, es posible que quieras usar dicha propiedad sin hacer la comprobación "extra" de que ya existe, en cuyo caso, tendrás que "sincronizar" también dicha propiedad. Por tanto deberías saber cómo usar el bloque SyncLock en una propiedad, ya que en un método en más o menos evidente, pero no tanto en una propiedad, ya que en el caso de las propiedades, la sincronización hay que hacerla en cada uno de los bloques Get y Set.
Por tanto el código anterior, una vez "sincronizado" quedaría así:Default Public Overridable ReadOnly Property Item(ByVal sIndex As String) As cPalabra Get ' bloquear esta propiedad por si se usan Threads (17/Ene/04) SyncLock Me.GetType ' Método predeterminado Dim tPalabra As cPalabra ' If m_ht.ContainsKey(sIndex) Then Return CType(m_ht(sIndex), cPalabra) Else ' Creamos una nuevo objeto tPalabra = New cPalabra tPalabra.ID = sIndex ' lo añadimos a la colección m_ht.Add(sIndex, tPalabra) ' Return tPalabra End If End SyncLock End Get End Property
Este es el procedimiento que será usado por cada hilo. Desde aquí se llamará al método desglosar para que compruebe cada una de las palabras que hay en el fichero examinado.
Public Sub ProcesarDir() '------------------------------------------------------------------ ' Procesar los ficheros del path y extensión indicadas. ' Convertido en Sub para usar con Thread (25/Mar/01) ' ' Modificaciones para adaptarlo a la versión definitiva (17/Ene/04) '------------------------------------------------------------------ ' Dim tFiles() As String Dim i, n As Integer Dim s As String Dim sr As System.IO.StreamReader ' ' Asigna a tFiles un array con los nombres de los ficheros, ' path incluido tFiles = Directory.GetFiles(sDir, sExt) ' 'Debug.WriteLine(sDir) ' ' Examinar el contenido de cada fichero y trocearlo en palabras ' n = tFiles.Length - 1 ' ' este evento se irá produciendo de forma independiente al orden ' en el que se procesen los ficheros... ' y realmente no producirá el efecto deseado: ' que se vaya mostrando cada directorio conforme se procesan los ficheros RaiseEvent ProcesandoDirectorio(String.Format("Dir: {0}, con {1} ficheros.", sDir, n + 1)) ' un respiro para que se actualice la información (no funciona siempre) 'esteThread.Sleep(10) TotalDirectorios += 1 ' For i = 0 To n RaiseEvent ProcesandoFichero(" Procesando: " & (i + 1).ToString & " / " & (n + 1).ToString & " " & Path.GetFileName(tFiles(i)) & " ...") ' si se pone aquí, si que se irá mostrando 'RaiseEvent ProcesandoDirectorio(String.Format("Dir: {0}, con {1} ficheros.", sDir, n + 1)) 'esteThread.Sleep(50) ' ' Abrir el fichero usando la codificación estándard de Windows sr = New StreamReader(tFiles(i), System.Text.Encoding.Default) s = sr.ReadToEnd sr.Close() ' ' Buscar cada una de las palabras del fichero desglosar(s, tFiles(i)) Next ' TotalFicheros += tFiles.Length End Sub
Para terminar, veamos cómo controlar que cada hilo ha terminado, ya que de alguna forma tenemos que saber cuando ha terminado todo para poder seguir con la ejecución normal.
Como te comentaba antes, dependiendo de que se procese uno o varios directorios, se usará un código u otro, pero en ambos casos, usaremos el método Procesar de la clase cBuscarPalabrasEnFicheros. Desde aquí se decidirá si procesar un directorio o varios.
En caso de que sean varios los directorios a procesar (se habrá marcado la opción conSubDirs del formulario), se llamará al método "recursivo" procesarSubDir, en el cual se examina un directorio y si contiene más directorios, se llamará nuevamente a este mismo método. Para que lo comprendas, te muestro el código:Private Sub procesarSubDir(ByVal sDir As String, _ Optional ByVal sExt As String = "*.*") '------------------------------------------------------------------ ' Este procedimiento será llamado de forma recursiva para procesar ' cada uno de los directorios. '------------------------------------------------------------------ Dim tDir As String Dim tSubDirs() As String ' tSubDirs = Directory.GetDirectories(sDir) ' 'Debug.WriteLine(sDir) ' ' Crear un thread para cada directorio a procesar nThread += 1 ReDim Preserve mProcesarFic(nThread - 1) mProcesarFic(nThread - 1) = New Guille.Clases.cProcesarFicheroThread(lasPalabras) ' asignar el manejador del evento (17/Ene/04) AddHandler mProcesarFic(nThread - 1).ProcesandoFichero, AddressOf Me.OnProcesandoFichero AddHandler mProcesarFic(nThread - 1).ProcesandoDirectorio, AddressOf Me.OnProcesandoDirectorio ' asignar los datos del directorio y la extensión a comprobar mProcesarFic(nThread - 1).sDir = sDir mProcesarFic(nThread - 1).sExt = sExt ' Los procedimientos a usar no deben aceptar parámetros mProcesarFic(nThread - 1).esteThread = New Thread(New ThreadStart(AddressOf mProcesarFic(nThread - 1).ProcesarDir)) 'mProcesarFic(nThread - 1).esteThread = New Thread(AddressOf mProcesarFic(nThread - 1).ProcesarDir) ' ' Iniciar el thread mProcesarFic(nThread - 1).esteThread.Start() ' Llamada recursiva a este procedimiento para procesar los subdirectorios ' de cada directorio For Each tDir In tSubDirs ' Procesar todos los directorios de cada subdirectorio procesarSubDir(tDir, sExt) Next End SubAquí creamos un array con cada uno de los subdirectorios del directorio procesado. Se crea el hilo que procesará dicho directorio y se llama de forma recursiva a este mismo método usando cada uno de los subdirectorios:
' Llamada recursiva a este procedimiento para procesar los subdirectorios ' de cada directorio For Each tDir In tSubDirs ' Procesar todos los directorios de cada subdirectorio procesarSubDir(tDir, sExt) Next
Por fin, veremos el código completo del método Procesar, que es donde se comprobará si los hilos aún están ejecutándose o no:
Public Function Procesar(ByVal sDir As String, _ Optional ByVal sExt As String = "*.*", _ Optional ByVal conSubDir As Boolean = False _ ) As String '------------------------------------------------------------------ ' Procesar los directorios del path indicado en sDir, ' buscando ficheros con la extensión sExt, ' y si se deben procesar los subdirectorios. ' ' Si se quieren usar varias extensiones se podría hacer, ' pero hay que tener en cuenta que Directory.GetFiles ' no procesará varias extensiones separadas por ; ' Por tanto, habrá que hacer un bucle para cada extensión, ' pero eso se hará en el método ProcesarDir de la clase ' cProcesarFicheroThread. '------------------------------------------------------------------ ' nThread = 0 ReDim mProcesarFic(0) ' ' Comprobar si se van a procesar los subdirectorios If conSubDir Then procesarSubDir(sDir, sExt) Else ' redimensionamos el array nThread += 1 ReDim Preserve mProcesarFic(nThread - 1) ' creamos una nueva instancia de la clase ' a la que le pasamos la colección "compartida" de palabras mProcesarFic(nThread - 1) = New Guille.Clases.cProcesarFicheroThread(lasPalabras) ' asignamos los "manejadores" de eventos AddHandler mProcesarFic(nThread - 1).ProcesandoFichero, AddressOf Me.OnProcesandoFichero AddHandler mProcesarFic(nThread - 1).ProcesandoDirectorio, AddressOf Me.OnProcesandoDirectorio ' asignamos el directorio y la extensión a procesar mProcesarFic(nThread - 1).sDir = sDir mProcesarFic(nThread - 1).sExt = sExt ' creamos un nuevo thread 'mProcesarFic(nThread - 1).esteThread = New Thread(New ThreadStart(AddressOf mProcesarFic(nThread - 1).ProcesarDir)) mProcesarFic(nThread - 1).esteThread = New Thread(AddressOf mProcesarFic(nThread - 1).ProcesarDir) ' iniciamos el nuevo thread mProcesarFic(nThread - 1).esteThread.Start() End If ' ' Aquí llegará incluso antes de terminar todos los threads 'Debug.WriteLine("Fin de Procesar todos los ficheros") ' ' Comprobar si han terminado los threads Dim i, j As Integer Do j = 0 For i = 0 To nThread - 1 ' Comprobar si alguno de los Threads está "vivo" ' si es así, indicarlo para que continúe el bucle If mProcesarFic(i).esteThread.IsAlive Then j = 1 Exit For End If Next ' Esto es necesario, para que todo siga funcionando System.Windows.Forms.Application.DoEvents() Loop While j = 1 ' 'Debug.WriteLine("Han finalizado los threads") ' ' Ahora podemos asignar el número de ficheros procesados i = cProcesarFicheroThread.TotalDirectorios j = cProcesarFicheroThread.TotalFicheros Dim sb As New System.Text.StringBuilder ' sb.AppendFormat("Procesado: {0} dir., ", i) If j = 1 Then sb.AppendFormat("{0} fichero.", j) Else sb.AppendFormat("{0} ficheros.", j) End If RaiseEvent FicherosProcesados(sb.ToString) ' ' Asignamos a cero el valor de total ficheros ' por si se vuelve a usar, para que no siga acumulando. cProcesarFicheroThread.TotalFicheros = 0 cProcesarFicheroThread.TotalDirectorios = 0 ' Return sb.ToString End FunctionPara saber si han terminado todos los hilos de hacer su trabajo, recorremos el contenido del array y comprobamos si hay algún hilo "vivo", en cuyo caso... seguimos esperando.
Esto lo comprobamos mediante los bucles anidados:' Comprobar si han terminado los threads Dim i, j As Integer Do j = 0 For i = 0 To nThread - 1 ' Comprobar si alguno de los Threads está "vivo" ' si es así, indicarlo para que continúe el bucle If mProcesarFic(i).esteThread.IsAlive Then j = 1 Exit For End If Next ' Esto es necesario, para que todo siga funcionando System.Windows.Forms.Application.DoEvents() Loop While j = 1Una vez que ha terminado todos los threads, seguimos nuestro curso... y lanzamos un evento que le indique al formulario que ya hemos terminado:
RaiseEvent FicherosProcesados(sb.ToString)
En cuanto el formulario reciba esa notificación, se encargará de mostrar el contenido de las palabras en el ListView... o donde quiera... aunque en este ejemplo se muestra en un ListView.
Aunque no te lo muestro en esta página, el ejemplo que puedes bajar en el link que hay un poco más abajo, también tienes los siguientes "conceptos":
- Abrir y guardar datos en un fichero usando la codificación "estándar" de Windows.
- Crear las columnas de un ListView en tiempo de ejecución.
- Añadir elementos a un ListView.
- Clasificar los elementos de un ListView dependiendo de la columna pulsada (para ello se utiliza mi clase ListViewColumnSort).
- Saber el directorio desde el que se ha ejecutado el ejecutable.
- Averiguar el nombre y directorio de un fichero.
- Usar un controlador genérico de errores.
- Usar los estilos de Windows XP (sólo para la versión 2003 de VS).
- Para C#: dos funciones para simular GetSetting y SaveSetting de Visual Basic.
- Y alguna cosilla más que ahora mismo no me acuerdo... je, je...
Espero que todo esto te sirva para aclararte un poco mejor cómo usar threads (hilos) con los lenguajes de .NET, que aunque en estos ejemplos haya usado código de Visual Basic .NET, en C# es casi igual de fácil... o complicado, según lo mires.
Como curiosidad, realmente no es una curiosidad sino el "sino" de los múltiples hilos, decirte que cuando ejecutes la aplicación verás que en la etiqueta que muestra los directorios que se van procesando, llega un momento en que ya no cambia, aunque si lo hace la etiqueta que muestra los ficheros procesados. Esto es así a causa de que los threads se van ejecutando sin ningún orden y van a "su aire".
El código que hace que se muestre esa información es el que se encarga de "lanzar" los eventos, estos eventos se lanzan en el método ProcesarDir de la clase cProcesarFicheroThread, el que informa de que se está procesando un directorio se lanza antes de empezar el bucle que recorre todos los ficheros de ese directorio, mientras que el que informa de cada fichero que se procesa, se lanza desde el bucle.
Aquí tienes parte de dicho código:RaiseEvent ProcesandoDirectorio(String.Format("Dir: {0}, con {1} ficheros.", sDir, n + 1)) ' For i = 0 To n RaiseEvent ProcesandoFichero(" Procesando: " & (i + 1).ToString & " / " & (n + 1).ToString & " " & Path.GetFileName(tFiles(i)) & " ...")Pero como te he dicho no consigue lo que quiere conseguir: Que se muestre el nombre del directorio procesado y se mantenga mientras se procesan los ficheros que contiene.
Y no lo loga, por lo que te he comentado antes, porque cada hilo va más o menos a su aire y no lleva ningún orden... o casi.Si quieres que se vaya mostrando cada directorio que se procesa, mientras se procesan los ficheros, tendrás que cambiar el código para que quede de esta forma: (en el código del fichero ZIP está, pero comentado):
For i = 0 To n RaiseEvent ProcesandoFichero(" Procesando: " & (i + 1).ToString & " / " & (n + 1).ToString & " " & Path.GetFileName(tFiles(i)) & " ...") ' si se pone aquí, si que se irá mostrando RaiseEvent ProcesandoDirectorio(String.Format("Dir: {0}, con {1} ficheros.", sDir, n + 1))
¡Que lo disfrutes! (y si te sobran unos euros, ya sabes... PayPal al canto!)
Nos vemos.
GuillermoLos ficheros con el código de ejemplo:
Código para Visual Basic .NET: WordCountThreadVB.zip 19.5 KB
Código para C#: WordCountThreadCS.zip 18.0 KB
Es el mostrado a lo largo del artículo.
El código completo lo puedes conseguir en el fichero zip.
Aquí te muestro los mismos trozos de código que hay en el texto.
El código completo lo puedes conseguir en el fichero zip.
Como "bono" especial, estas dos funciones que simulan las funciones homónimas (del mismo nombre) de Visual Basic:
private string getSetting(string appName, string section, string key, string sDefault) { // Los datos de VB se guardan en: // HKEY_CURRENT_USER\Software\VB and VBA Program Settings RegistryKey rk = Registry.CurrentUser.OpenSubKey(@"Software\VB and VBA Program Settings\" + appName + "\\" + section); string s = sDefault; if( rk != null ) s = (string)rk.GetValue(key); // return s; } private void saveSetting(string appName, string section, string key, string setting) { // Los datos de VB se guardan en: // HKEY_CURRENT_USER\Software\VB and VBA Program Settings RegistryKey rk = Registry.CurrentUser.CreateSubKey(@"Software\VB and VBA Program Settings\" + appName + "\\" + section); rk.SetValue(key, setting); }
// Estos métodos se usarán para producir los eventos de esta clase. protected void OnProcesandoFichero(string sMsg) { // Este evento se produce cuando se está procesando un fichero // por uno de los threads. // Desde aquí producimos nuestro evento a la aplicación. if( ProcesandoFichero != null ) ProcesandoFichero(sMsg); } protected void OnProcesandoDirectorio(string sMsg) { // Este evento se produce cuando se está procesando un directorio // por uno de los threads. // Desde aquí producimos nuestro evento a la aplicación. if( ProcesandoDirectorio != null ) ProcesandoDirectorio(sMsg); }
// Crear un thread para cada directorio a procesar cProcesarFicheroThread oPF = new cProcesarFicheroThread(lasPalabras); // asignar el manejador del evento (17/Ene/04) oPF.ProcesandoFichero += new cProcesarFicheroThread.ProcesandoFicheroDelegate(this.OnProcesandoFichero); oPF.ProcesandoDirectorio += new cProcesarFicheroThread.ProcesandoDirectorioDelegate(this.OnProcesandoDirectorio); // asignar los datos del directorio y la extensión a comprobar oPF.sDir = sDir; oPF.sExt = sExt; // Los procedimientos a usar no deben aceptar parámetros oPF.esteThread = new Thread(new ThreadStart(oPF.ProcesarDir)); // mProcesarFic.Add(oPF); // // Iniciar el thread oPF.esteThread.Start();
private void desglosar(string sText, string sFic){ // Desglosar en palabras individuales el contenido del parámetro // // Estos signos se considerarán separadores de palabras string sSep = @".,;: ()<>[]{}¿?¡!/\'=+*-%&$|#@" + "\"\t\n\r";" // crear un array con cada una de las palabras string[] palabras = sText.Split(sSep.ToCharArray()); cPalabra tPalabra; int i; // for(i = 0; i <= palabras.Length - 1; i++){ string s = palabras[i]; // sólo si no es una cadena vacía if( s != "" ){ if( lasPalabras.Exists(s) == false ){ tPalabra = new Guille.Clases.cPalabra(); tPalabra.ID = s; tPalabra.Veces = 1; // el fichero en el que inicialmente apareció tPalabra.Contenido = sFic; lasPalabras.Add(tPalabra); }else{ // Incrementar el número de esta palabra lasPalabras[s].IncVeces(); } } } }
public virtual void Add(cPalabra tPalabra) { // Añadir un nuevo elemento a la colección // // bloquear este método por si se usan Threads (17/Ene/04) lock(this.GetType()) { if( !m_ht.ContainsKey(tPalabra.ID) ) m_ht.Add(tPalabra.ID, tPalabra); } // lock }
public virtual cPalabra this[string sIndex] { get{ // bloquear esta propiedad por si se usan Threads (17/Ene/04) lock(this.GetType()) { cPalabra tPalabra; // if( m_ht.ContainsKey(sIndex) ) { return (cPalabra)m_ht[sIndex]; } else { // Creamos una nuevo objeto tPalabra = new cPalabra(); tPalabra.ID = sIndex; // lo añadimos a la colección m_ht.Add(sIndex, tPalabra); // return tPalabra; } } // lock } }
public void ProcesarDir() { //------------------------------------------------------------------ // Procesar los ficheros del path y extensión indicadas. // Convertido en Sub para usar con Thread (25/Mar/01) // // Modificaciones para adaptarlo a la versión definitiva (17/Ene/04) //------------------------------------------------------------------ // string[] tFiles; int i, n; string s; System.IO.StreamReader sr; // // Asigna a tFiles un array con los nombres de los ficheros, // path incluido tFiles = Directory.GetFiles(sDir, sExt); // //Debug.WriteLine(sDir) // // Examinar el contenido de cada fichero y trocearlo en palabras // n = tFiles.Length - 1; // // este evento se irá produciendo de forma independiente al orden // en el que se procesen los ficheros... // y realmente no producirá el efecto deseado: // que se vaya mostrando cada directorio conforme se procesan los ficheros if( ProcesandoDirectorio != null ) ProcesandoDirectorio(String.Format("Dir: {0}, con {1} ficheros.", sDir, n + 1)); // un respiro para que se actualice la información (no funciona siempre) //esteThread.Sleep(10) TotalDirectorios += 1; // for(i = 0; i <= n; i++){ if( ProcesandoFichero != null ) ProcesandoFichero(" Procesando: " + (i + 1).ToString() + " / " + (n + 1).ToString() + " " + Path.GetFileName(tFiles[i]) + " ..."); // si se pone aquí, si que se irá mostrando //if( ProcesandoDirectorio != null ) // ProcesandoDirectorio(String.Format("Dir: {0}, con {1} ficheros.", sDir, n + 1)); //esteThread.Sleep(50) // // Abrir el fichero usando la codificación estándard de Windows sr = new StreamReader(tFiles[i], System.Text.Encoding.Default); s = sr.ReadToEnd(); sr.Close(); // // Buscar cada una de las palabras del fichero desglosar(s, tFiles[i]); } // TotalFicheros += tFiles.Length; }
private void procesarSubDir(string sDir) { procesarSubDir(sDir, "*.*"); } private void procesarSubDir(string sDir, string sExt) { //------------------------------------------------------------------ // Este procedimiento será llamado de forma recursiva para procesar // cada uno de los directorios. //------------------------------------------------------------------ string[] tSubDirs; // tSubDirs = Directory.GetDirectories(sDir); // // Crear un thread para cada directorio a procesar cProcesarFicheroThread oPF = new cProcesarFicheroThread(lasPalabras); // asignar el manejador del evento (17/Ene/04) oPF.ProcesandoFichero += new cProcesarFicheroThread.ProcesandoFicheroDelegate(this.OnProcesandoFichero); oPF.ProcesandoDirectorio += new cProcesarFicheroThread.ProcesandoDirectorioDelegate(this.OnProcesandoDirectorio); // asignar los datos del directorio y la extensión a comprobar oPF.sDir = sDir; oPF.sExt = sExt; // Los procedimientos a usar no deben aceptar parámetros oPF.esteThread = new Thread(new ThreadStart(oPF.ProcesarDir)); // mProcesarFic.Add(oPF); // // Iniciar el thread oPF.esteThread.Start(); // // Llamada recursiva a este procedimiento para procesar los subdirectorios // de cada directorio foreach(string tDir in tSubDirs) // Procesar todos los directorios de cada subdirectorio procesarSubDir(tDir, sExt); }
public string Procesar(string sDir) { return Procesar(sDir, "*.*", false); } public string Procesar(string sDir, string sExt) { return Procesar(sDir, sExt, false); } public string Procesar(string sDir, string sExt, bool conSubDir) { //------------------------------------------------------------------ // Procesar los directorios del path indicado en sDir, // buscando ficheros con la extensión sExt, // y si se deben procesar los subdirectorios. // // Si se quieren usar varias extensiones se podría hacer, // pero hay que tener en cuenta que Directory.GetFiles // no procesará varias extensiones separadas por ; // Por tanto, habrá que hacer un bucle para cada extensión, // pero eso se hará en el método ProcesarDir de la clase // cProcesarFicheroThread. //------------------------------------------------------------------ // mProcesarFic.Clear(); // // Comprobar si se van a procesar los subdirectorios if( conSubDir ) procesarSubDir(sDir, sExt); else{ // Crear un thread para cada directorio a procesar cProcesarFicheroThread oPF = new cProcesarFicheroThread(lasPalabras); // asignar el manejador del evento (17/Ene/04) oPF.ProcesandoFichero += new cProcesarFicheroThread.ProcesandoFicheroDelegate(this.OnProcesandoFichero); oPF.ProcesandoDirectorio += new cProcesarFicheroThread.ProcesandoDirectorioDelegate(this.OnProcesandoDirectorio); // asignar los datos del directorio y la extensión a comprobar oPF.sDir = sDir; oPF.sExt = sExt; // Los procedimientos a usar no deben aceptar parámetros oPF.esteThread = new Thread(new ThreadStart(oPF.ProcesarDir)); // mProcesarFic.Add(oPF); // Iniciar el thread oPF.esteThread.Start(); } // // Aquí llegará incluso antes de terminar todos los threads //Debug.WriteLine("Fin de Procesar todos los ficheros") // // Comprobar si han terminado los threads int i, j; do{ j = 0; for(i = 0; i <= mProcesarFic.Count - 1; i++){ // Comprobar si alguno de los Threads está "vivo" // si es así, indicarlo para que continúe el bucle if( ((cProcesarFicheroThread)mProcesarFic[i]).esteThread.IsAlive ){ j = 1; break; } } // Esto es necesario, para que todo siga funcionando System.Windows.Forms.Application.DoEvents(); }while( j == 1); // //Debug.WriteLine("Han finalizado los threads") // // Ahora podemos asignar el número de ficheros procesados i = cProcesarFicheroThread.TotalDirectorios; j = cProcesarFicheroThread.TotalFicheros; System.Text.StringBuilder sb = new System.Text.StringBuilder(); // sb.AppendFormat("Procesado: {0} dir., ", i); if( j == 1 ){ sb.AppendFormat("{0} fichero.", j); }else{ sb.AppendFormat("{0} ficheros.", j); } // Producimos el evento... si está interceptado if( FicherosProcesados != null) FicherosProcesados(sb.ToString()); // // Asignamos a cero el valor de total ficheros // por si se vuelve a usar, para que no siga acumulando. cProcesarFicheroThread.TotalFicheros = 0; cProcesarFicheroThread.TotalDirectorios = 0; // return sb.ToString(); }