la lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

64
La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream. La idea detrás del stream existe hace tiempo, cuando los datos son pensados como una transferencia de un punto al otro como un flujo de datos. En el ambiente .NET usted puede encontrar muchas clases que representan este concepto que trabaja con archivos o con datos de memoria (ver la figura de abajo). Figura 1. Clases del Framework .NET para el uso de Streams. BufferedStream Esta clase se utiliza para leer y para escribir a otro stream. Se utiliza por razones del performance, cuando el caché de los datos del archivo es utilizado por el sistema operativo subyacente. El uso de streams para la lectura y escritura de archivo es directa pero lenta con bajo performance. Por esta razón la clase BufferedStream existe y es más eficiente. Puede ser utilizado por cualquier clase de stream. Para operaciones de archivo es posible utilizar FileStream, donde el buffering está ya incluido. Leyendo desde un archivo usando BufferedStream using System; using System.Text; using System.IO; static void Main(string[] args)

Upload: francocoello13

Post on 29-Jul-2015

341 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream. La idea detrás del stream existe hace tiempo, cuando los datos son pensados como una transferencia de un punto al otro como un flujo de datos. En el ambiente .NET usted puede encontrar muchas clases que representan este concepto que trabaja con archivos o con datos de memoria (ver la figura de abajo).

Figura 1. Clases del Framework .NET para el uso de Streams.

BufferedStream

Esta clase se utiliza para leer y para escribir a otro stream. Se utiliza por razones del performance, cuando el caché de los datos del archivo es utilizado por el sistema operativo subyacente.

El uso de streams para la lectura y escritura de archivo es directa pero lenta con bajo performance. Por esta razón la clase BufferedStream existe y es más eficiente. Puede ser utilizado por cualquier clase de stream. Para operaciones de archivo es posible utilizar FileStream, donde el buffering está ya incluido.

Leyendo desde un archivo usando BufferedStream

using System;using System.Text;using System.IO;

static void Main(string[] args){ string path = "c:\\sample\\sample.xml"; Stream instream = File.OpenRead(path);

// crear buffer para abrir stream BufferedStream bufin = new BufferedStream(instream); byte[] bytes = new byte[128];

Page 2: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

// leer los primeros 128 bytes del archivo bufin.Read(bytes, 0, 128); Console.WriteLine("Allocated bytes: "+Encoding.ASCII.GetString(bytes));}

Leyendo desde un archivo de texto

using System;using System.IO;

static void Main(string[] args){ string fileName = "temp.txt"; FileStream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read); StreamReader reader = new StreamReader(stream);

while (reader.Peek() > -1) Console.WriteLine(reader.ReadLine()); reader.Close();}

Escribiendo en un archive de texto

using System;using System.IO;

static void Main(string[] args){ string fileName = "temp.txt"; FileStream stream = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.Write); StreamWriter writer = new StreamWriter(stream);

writer.WriteLine("Esta es la primera línea del archivo."); writer.Close();}

Creando un archivo y escribiendo en este

Este ejemplo usa el método CreateText() el cual crea un Nuevo archive y retorna un objeto StreamWriter que escribe a un archivo usando formato UTF-8.

using System;using System.IO;

static void Main(string[] args){ string fileName = "temp.txt"; StreamWriter writer = File.CreateText(fileName);

writer.WriteLine("Este es mi Nuevo archivo creado."); writer.Close();}

Insertando texto en un archivo

Page 3: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

using System;using System.IO;

static void Main(string[] args){ try { string fileName = "temp.txt"; // esto inserta texto en un archivo existente, si el archivo no existe lo crea StreamWriter writer = File.AppendText(fileName); writer.WriteLine("Este es el texto adicionado."); writer.Close(); } catch { Console.WriteLine("Error"); }}

Leyendo un archivo binario

using System;using System.IO;

static void Main(string[] args){ try { string fileName = "temp.txt"; int letter = 0; FileStream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read); BinaryReader reader = new BinaryReader(stream);

while (letter != -1) { letter = reader.Read(); if (letter != -1) Console.Write((char)letter); } reader.Close(); stream.Close(); } catch { Console.WriteLine("Error"); }}

Escribiendo en un archivo binario

static void Main(string[] args){ try { string fileName = "temp.txt"; // data a ser guardada int[] data = {0, 1, 2, 3, 4, 5}; FileStream stream = new FileStream(fileName, FileMode.Open, FileAccess.Write);

Page 4: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

BinaryWriter writer = new BinaryWriter(stream);

for(int i=0; i<data.Length; i++) { // números son guardados en formáto UTF-8 format (4 bytes) writer.Write(data[i]); }

writer.Close(); stream.Close(); } catch { Console.WriteLine("Error");}

Ver cambios en el file system

.NET Framework prove la calse System.IO.FileSystemWatcher la cual permite ver si hay cambios en el file system.

using System;using System.IO;class WatcherSample{ static void Main(string[] args) { // ver los cambios en el directorio de la aplicación y sobre todos los archivos FileSystemWatcher watcher = new FileSystemWatcher(System.Windows.Forms.Application.StartupPath, "*.*");

// ver el nombre del archivo y tamaño cambiado watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.Size; watcher.Changed += new FileSystemEventHandler(OnChange); watcher.Created += new FileSystemEventHandler(OnChange); watcher.Deleted += new FileSystemEventHandler(OnChange); watcher.Renamed += new RenamedEventHandler(OnChange); watcher.EnableRaisingEvents = true;

// espera por una tecla para terminar la aplicación Console.ReadLine(); } private static void OnChange(object sender, FileSystemEventArgs e) { Console.WriteLine("File: {0} - change type: {1}", e.FullPath, e.ChangeType); } private static void OnChange(object sender, RenamedEventArgs e) { Console.WriteLine("File: {0} renamed to {1}", e.OldName, e.Name); }}

Page 5: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

Por favor no olvides calificar el artículo en la caja de PanoramaBox que se muestra al inicio de la página.

Tipos de archivo y extensiones de archivo en Visual Basic, Visual C# y Visual J#Visual Studio 2005 Otras versiones

Los elementos de proyecto hacen referencia a los tipos de archivos que se agregan a un proyecto. Los elementos del proyecto disponibles varían según se esté trabajando en un proyecto local o un proyecto Web. Para obtener más información, vea Proyectos locales y proyectos Web y Visita guiada sobre la creación de sitios Web en Visual Web Developer.

Agregar elementos de proyectoSe tiene acceso al cuadro de diálogo Agregar nuevo elemento a través del menú Proyecto. Para obtener más información sobre cómo agregar elementos al proyecto, vea Cómo: Agregar nuevos elementos de proyecto.

Las tablas siguientes enumeran los elementos de proyecto disponibles para los proyectos locales y los proyectos Web. Observe que, para algunos elementos de proyecto, la extensión de archivo varía según aparezca el elemento en un proyecto de Visual Basic (indicado por .vb), en un proyecto de Visual C# (indicado por .cs) o en un proyecto de Visual J# (indicado por .jsl). Para otros elementos de proyecto, la extensión de archivo no distingue entre un proyecto de Visual Basic, uno de Visual C# y uno de Visual J#; es decir, sólo hay una extensión de archivo posible para el elemento del proyecto.

Si ha instalado Diseñadores de sistemas distribuidos, vea la siguiente información acerca del Modelo de definición del sistema (SDM) y los tipos de archivo del Diseñador de sistemas distribuidos:

Tema UbicaciónInformación general sobre el modelo de definición del sistema (SDM)

Interfaz de usuario de Visual Studio Enterprise

Información general sobre el modelo de definición del sistema (SDM)

Interfaz de usuario de Visual Studio Enterprise

Page 6: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

Elementos de proyecto local

Elemento de proyecto

Extensión de archivo

Propósito del elemento de proyecto

Formulario Windows Forms

.vb o .cs o .jsl o .java

Un formulario utilizado para crear aplicaciones para Windows.

Formulario del explorador

.vb o .cs o .jsl o .java

Un formulario del explorador con una vista de árbol y controles de exploración.

Formulario primario MDI

.vb o .cs o .jsl o .java

Un formulario para una aplicación de interfaz de múltiples documentos.

Cuadro Acerca de.vb o .cs o .jsl o .java

Un formulario de cuadro de diálogo Acerca de para aplicaciones para Windows.

Formulario de inicio de sesión

.vb o .cs o .jsl o .java

Un formulario de inicio de sesión para recopilar un nombre de usuario y una contraseña.

Pantalla de bienvenida

.vb o .cs o .jsl o .java

Un formulario preconfigurado para su uso como pantalla de bienvenida.

Cuadros de diálogo.vb o .cs o .jsl o .java

Un cuadro de diálogo para aplicaciones para Windows.

Clase   .vb o .cs o .jsl o .java

Archivo de código que contiene una declaración de clase. Para obtener más información, vea Clases del sistema de tipos común.

Interfaz.vb o .cs o .jsl o .java

Una nueva interfaz.

Módulo (sólo Visual Basic)

.vbArchivo para el almacenamiento de grupos de funciones.

Clase de componentes

.vb o .cs o .jsl o .java

Clase para crear componentes utilizando el diseñador visual. Para obtener más información, vea Clases de componentes.

Control de usuario.vb o .cs o .jsl o .java

Una clase para crear un control de formularios Windows Forms utilizando el diseñador visual.

Servicio de Windows

.vb o .cs o .jsl o .java

Una clase para crear servicios de Windows. Para obtener más información, vea Cómo: Crear servicios de Windows.

DataSet .xsd

Un archivo para crear un esquema XML con clases DataSet. Para obtener más información, vea Escribir la información de esquema de DataSet en formato de Definición de esquema XML (XSD).

Base de datos SQL .mdf Una base de datos SQL vacía para los datos locales.

Archivo XML .xmlUn archivo XML en blanco. Para obtener más información, vea Diseñador de esquemas XML (Diseñador XML).

Esquema XML .xsdUn archivo para crear un esquema para documentos XML. Para obtener más información, vea Introducción a esquemas XML (Diseñador XML).

Archivo de configuración

.settings Un archivo de configuración del usuario en blanco.

Archivo de código.vb o .cs o .jsl o .java

Un archivo de código en blanco.

Page 7: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

Control personalizado

.vb o .cs o .jsl o .java

Una clase para crear un control de formularios Windows Forms orientado al usuario. Para obtener más información, vea Cómo: Crear controles de formularios Windows Forms.

Página HTML .htmUna página HTML que puede incluir códigos del lado del cliente. Para obtener más información, vea Vista Diseño.

Formulario heredado

.vb o .cs o .jsl o .java

Un nuevo formulario basado en un formulario existente. Para obtener más información, vea Herencia visual de formularios Windows Forms.

Control de usuario heredado

.vb o .cs o .jsl o .java

Un nuevo control basado en un control de formulario Windows Forms existente. Para obtener más información, vea Cómo: Heredar de controles de formularios Windows Forms existentes.

Control Web personalizado

.vb o .cs o .jsl o .java

Una clase para crear un control de servidores ASP.NET. Para obtener más información, vea Controles de servidor Web ASP.NET (Visual Studio).

Clase COM .vb o .csUna clase que puede estar expuesta a COM. Para obtener más información, vea Interoperabilidad COM en aplicaciones .NET Framework.

Componente transaccional

.vb o .csUna clase que se utiliza con los componentes transaccionales.

Archivo de texto .txt Archivo de texto vacío.

Archivo XSLT .xsltUn archivo utilizado para transformar documentos XML.

Clase del instalador.vb o .cs o .jsl o .java

Una clase que se va a invocar durante la configuración. Para obtener más información, vea Utilizar componentes de instalación.

Crystal Report .rptUn archivo de Crystal Report que publica datos en un formulario de Windows.

Archivo de mapa de bits

.bmpUn archivo de imagen de mapa de bits en blanco, que puede utilizarse para crear imágenes simples.

Archivo de cursor .curUn archivo para crear cursores personalizados. Para obtener más información, vea Creación de cursores.

Archivo de icono .icoUn archivo de imagen para crear un icono personalizado. Para obtener más información, vea Iconos.

Archivo de recursos .resxUn archivo utilizado para editar y definir recursos de aplicaciones. Para obtener más información, vea Recursos en aplicaciones.

Archivo de información sobre el ensamblado

.vb o .cs o .jsl

Un archivo que contiene información general acerca del ensamblado. Para obtener más información, vea Ensamblados en Common Language Runtime.

Archivo de configuración de la aplicación

.configUn archivo utilizado para configurar los valores de la aplicación. Para obtener más información, vea Archivos de configuración.

Visualizador del .vb o .cs Un visualizador del depurador sencillo

Page 8: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

depurador o .jsl o .javaDiagrama de clase .cd Un diagrama de clase.Informe .rdlc Un nuevo informe vacío.

Archivo JScript .jsUn archivo de comandos que contiene código JScript. Para obtener más información, vea Introducción a JScript.

Archivo VBScript .vbsUn archivo de comandos que contiene código VBScript. Para obtener más información, vea Manual del usuario de VBScript.

Windows Script Host

.wsf

Un archivo que contiene una secuencia de comandos que se ejecuta como un programa de Windows. Para obtener más información, vea Utilizando archivos de Windows Script (.wsf).

Elementos de proyecto WebElemento de

proyecto Extensión de

archivo Propósito del elemento de proyecto

Formulario Web Forms

Dos archivos: .aspx y .vb para Visual Basic, .cs para Visual C# o .jsl para Visual J#

Un formulario utilizado para crear aplicaciones Web.

Página principal .master Una página principal para las aplicaciones Web.Control de usuario Web

.ascxUn control de servidor ASP.NET creado mediante un diseñador visual.

Página HTML .htmUna página HTML que puede incluir códigos del lado del cliente. Para obtener más información, vea Vista Diseño.

Servicio Web

Dos archivos: .asmx y .vb para Visual Basic, .cs para Visual C# o .jsl para Visual J#

Un componente que ofrece la posibilidad de intercambiar mensajes interoperables mediante el uso de protocolos estándar, como HTTP, XML, XSD, SOAP y WSDL. Para obtener más información, vea Crear servicios Web en código administrado.

Clase    .vb o .cs o .jslArchivo de código que contiene una declaración de clase.

Hoja de estilos .css

Una hoja de estilos en cascada utilizada para definiciones de estilo HTML enriquecido. Para obtener más información, vea Cómo: Crear hojas de estilo en cascada externas (Visual Studio).

Clase de aplicación global

.asax

A veces conocido como archivo asax, permite escribir código para controlar eventos globales ASP.NET de nivel de aplicación tales como Session_OnStart y Application_OnStart. El archivo tiene como nombre global.asax y no es posible cambiarlo.

Page 9: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

Archivo de configuración Web

.config

ASP.NET utiliza este archivo para configurar los valores Web de un proyecto Web. El archivo tiene como nombre Web.config y no es posible cambiarlo.

Archivo XML .xmlUn archivo XML en blanco. Para obtener más información, vea Diseñador de esquemas XML (Diseñador XML).

Esquema XML .xsd

Un archivo para crear un esquema para documentos XML. Para obtener más información, vea Introducción a esquemas XML (Diseñador XML).

Archivo XSLT .xsltUn archivo utilizado para transformar documentos XML.

Archivo de texto .txt Archivo de texto vacío.Archivo de recursos de ensamblado

.resxUn archivo utilizado para editar y definir recursos de aplicaciones. Para obtener más información, vea Recursos en aplicaciones.

Archivo de información sobre el ensamblado

.vb o .cs o .jsl

Un archivo que contiene información general acerca del ensamblado. Para obtener más información, vea Ensamblados en Common Language Runtime.

Base de datos SQL

.mdfUna base de datos SQL vacía para los datos locales.

DataSet .xsd

Un archivo para crear un esquema XML con clases DataSet. Para obtener más información, vea Escribir la información de esquema de DataSet en formato de Definición de esquema XML (XSD).

Controlador genérico

.ashxUna página para implementar un controlador genérico

Mapa del sitio .sitemap Un archivo utilizado para crear un mapa del sitio.

Formulario Mobile Web Forms

.aspx

Formulario utilizado para crear aplicaciones Web para dispositivos móviles. Para obtener más información, vea Crear aplicaciones Web móviles de ASP.NET.

Control de usuario Web móvil

.ascx

Control de servidor ASP.NET creado con el diseñador visual y utilizado en una aplicación Web para dispositivos móviles. Para obtener más información, vea Crear aplicaciones Web móviles de ASP.NET.

Archivo de configuración Web móvil

.config

ASP.NET utiliza este archivo para configurar los valores Web de un proyecto Web móvil. El archivo tiene como nombre Web.config y no es posible cambiarlo. Para obtener más información, vea Crear aplicaciones Web móviles de ASP.NET.

Crystal Report .rptUn archivo Crystal Report que publica datos en un formulario Web Forms.

Archivo VBScript .vbsUn archivo de comandos que contiene código VBScript. Para obtener más información, vea Manual del usuario de VBScript.

Page 10: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

Archivo JScript .jsUn archivo de comandos que contiene código JScript. Para obtener más información, vea Introducción a JScript.

Windows Script Host

.wsf

Un archivo de código vacío que se utiliza para crear secuencias de comandos de Windows. Para obtener más información, vea Introducción a las aplicaciones de servicios de Windows.

Archivo de máscara

.skinUn archivo utilizado para definir un tema de ASP.NET.

Archivo de explorador

.browser Un archivo para las definiciones del explorador

Informe .rdlc Un nuevo informe vacío.Módulo (sólo Visual Basic)

.vbArchivo para el almacenamiento de grupos de funciones.

Clase de componentes

.vb o .csClase para crear componentes utilizando el diseñador visual.

Asistente para formularios de datos

.aspx (.vb o .cs para proyectos locales)

Un formulario de datos para aplicaciones Web.

Conjunto de marcos

.htm Archivo HTML que aloja varias páginas HTML.

Control Web personalizado

.vb o .cs o .jsl

Una clase para crear un control de servidores ASP.NET. Para obtener más información, vea Controles de servidor Web ASP.NET (Visual Studio).

Archivo de código

.vb o .cs o .jsl Un archivo de código en blanco.

Archivo de descubrimiento estático

.disco

Un archivo opcional que actúa como mecanismo de descubrimiento para el servicio Web XML. El archivo .disco no se crea automáticamente para un servicio Web XML. Para obtener información sobre cómo crear un archivo de descubrimiento para su servicio Web XML, vea Cómo habilitar el descubrimiento para servicios Web XML.

Clase del instalador

.vb o .cs o .jslUna clase que se va a invocar durante la configuración. Para obtener más información, vea Utilizar componentes de instalación.

Archivo de mapa de bits

.bmpUn archivo de imagen de mapa de bits en blanco, que puede utilizarse para crear imágenes simples.

Archivo de cursor .curUn archivo para crear cursores personalizados. Para obtener más información, vea Creación de cursores.

Archivo de icono .icoUn archivo de imagen para crear un icono personalizado. Para obtener más información, vea Iconos.

Page 11: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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/2004Actualizado el 21/Abr/2004

Esto que te voy a mostrar aquí es una actualización de el artículo que escribí en marzo del 2001, ejemplo en el cual usé la versión BETA1 de Visual Basic .NET.

Aunque los conceptos explicados anteriormente todavía siguen siendo válidos, (lo único que cambia es la forma de usar ciertas funciones de .NET), te lo pongo aquí aparte para que no te compliques mucho la vida, además de que también he cambiado parte del código para que sea más seguro, ya que al trabajar con múltiples hilos (threads) el problema que se nos puede presentar es que varios hilos intenten acceder al mismo código a un mismo tiempo (o casi) y al final se produzcan errores o resultados que no son los previstos.

¿Qué hace el código de este ejemplo?

El ejemplo sigue siendo el mismo:Recorrer unos directorios en busca de ficheros con la extensión indicada y extraer cada una de las palabras que contiene, acumular esas palabras en una colección, de forma que al final nos muestre cada una de las palabras halladas y nos indique cuantas veces se ha repetido.

 

 

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:

Page 12: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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

Page 13: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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.

Page 14: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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 SubProtected 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 Sub

OnProcesandoFichero 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

Page 15: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

nThread += 1ReDim Preserve mProcesarFic(nThread - 1)' creamos una nueva instancia de la clase' a la que le pasamos la colección "compartida" de palabrasmProcesarFic(nThread - 1) = New Guille.Clases.cProcesarFicheroThread(lasPalabras)' asignamos los "manejadores" de eventosAddHandler mProcesarFic(nThread - 1).ProcesandoFichero, AddressOf Me.OnProcesandoFicheroAddHandler mProcesarFic(nThread - 1).ProcesandoDirectorio, AddressOf Me.OnProcesandoDirectorio' asignamos el directorio y la extensión a procesarmProcesarFic(nThread - 1).sDir = sDirmProcesarFic(nThread - 1).sExt = sExt' creamos un nuevo threadmProcesarFic(nThread - 1).esteThread = New Thread(New ThreadStart(AddressOf mProcesarFic(nThread - 1).ProcesarDir))' iniciamos el nuevo threadmProcesarFic(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:

Page 16: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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 Sub

En 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 If

Realmente 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

Page 17: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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 SyncLockEnd Sub

Pero 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#).

Page 18: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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 GetEnd Property

Y 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

Page 19: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

End SyncLock End GetEnd 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)

Page 20: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

s = sr.ReadToEnd sr.Close() ' ' Buscar cada una de las palabras del fichero desglosar(s, tFiles(i)) Next ' TotalFicheros += tFiles.LengthEnd 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) '

Page 21: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

' 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) NextEnd Sub

Aquí 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 directorioFor 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

Page 22: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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.ToStringEnd Function

Page 23: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

Para 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 threadsDim i, j As IntegerDo 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

Una 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".

Page 24: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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.Guillermo

Los ficheros con el código de ejemplo:Código para Visual Basic .NET: WordCountThreadVB.zip 19.5 KBCódigo para C#: WordCountThreadCS.zip 18.0 KB

El código para VB .NET.

Es el mostrado a lo largo del artículo.

El código completo lo puedes conseguir en el fichero zip.

 

Page 25: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

El código para C#.

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

Page 26: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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 comprobaroPF.sDir = sDir;oPF.sExt = sExt;// Los procedimientos a usar no deben aceptar parámetrosoPF.esteThread = new Thread(new ThreadStart(oPF.ProcesarDir));//mProcesarFic.Add(oPF); //// Iniciar el threadoPF.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()) {

Page 27: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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)

Page 28: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

// // 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; //

Page 29: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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();

Page 30: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

// // 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

Page 31: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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();

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/2004Actualizado el 21/Abr/2004

Esto que te voy a mostrar aquí es una actualización de el artículo que escribí en marzo del 2001, ejemplo en el cual usé la versión BETA1 de Visual Basic .NET.

Aunque los conceptos explicados anteriormente todavía siguen siendo válidos, (lo único que cambia es la forma de usar ciertas funciones de .NET), te lo pongo aquí aparte para que no te compliques mucho la vida, además de que también he cambiado parte del código para que sea más seguro, ya que al trabajar con múltiples hilos (threads) el problema que se nos puede presentar es que varios hilos intenten acceder al mismo código a un mismo tiempo (o casi) y al final se produzcan errores o resultados que no son los previstos.

¿Qué hace el código de este ejemplo?

El ejemplo sigue siendo el mismo:Recorrer unos directorios en busca de ficheros con la extensión indicada y extraer cada una de las palabras que contiene, acumular esas palabras en una colección, de forma que al final nos muestre cada una de las palabras halladas y nos indique cuantas veces se ha repetido.

 

 

Las palabras se almacenan en una colección personalizada llamada cPalabras que almacena objetos del tipo cPalabra. Dicha colección personalizada utiliza una una

Page 32: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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

Page 33: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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

Page 34: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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 SubProtected 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 Sub

OnProcesandoFichero 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

Page 35: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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 arraynThread += 1ReDim Preserve mProcesarFic(nThread - 1)' creamos una nueva instancia de la clase' a la que le pasamos la colección "compartida" de palabrasmProcesarFic(nThread - 1) = New Guille.Clases.cProcesarFicheroThread(lasPalabras)' asignamos los "manejadores" de eventosAddHandler mProcesarFic(nThread - 1).ProcesandoFichero, AddressOf Me.OnProcesandoFicheroAddHandler mProcesarFic(nThread - 1).ProcesandoDirectorio, AddressOf Me.OnProcesandoDirectorio' asignamos el directorio y la extensión a procesarmProcesarFic(nThread - 1).sDir = sDirmProcesarFic(nThread - 1).sExt = sExt' creamos un nuevo threadmProcesarFic(nThread - 1).esteThread = New Thread(New ThreadStart(AddressOf mProcesarFic(nThread - 1).ProcesarDir))' iniciamos el nuevo threadmProcesarFic(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,

Page 36: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

(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 Sub

En 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 If

Realmente 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

Page 37: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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 SyncLockEnd Sub

Pero 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

Page 38: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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 GetEnd Property

Y 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

Page 39: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

' 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 GetEnd 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

Page 40: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

' 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.LengthEnd 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)

Page 41: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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) NextEnd Sub

Aquí 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 directorioFor 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. '------------------------------------------------------------------ '

Page 42: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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

Page 43: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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.ToStringEnd Function

Para 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 threadsDim i, j As IntegerDo 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

Una 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.

Page 44: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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.Guillermo

Los ficheros con el código de ejemplo:Código para Visual Basic .NET: WordCountThreadVB.zip 19.5 KBCódigo para C#: WordCountThreadCS.zip 18.0 KB

Page 45: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

El código para VB .NET.

Es el mostrado a lo largo del artículo.

El código completo lo puedes conseguir en el fichero zip.

 

El código para C#.

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);}

Page 46: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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 procesarcProcesarFicheroThread 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 comprobaroPF.sDir = sDir;oPF.sExt = sExt;// Los procedimientos a usar no deben aceptar parámetrosoPF.esteThread = new Thread(new ThreadStart(oPF.ProcesarDir));//mProcesarFic.Add(oPF); //// Iniciar el threadoPF.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(); } } }

Page 47: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

}

 

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)

Page 48: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

//------------------------------------------------------------------ // 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){

Page 49: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

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,

Page 50: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

// 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")

Page 51: La lectura y escritura a un archivo son hechas usando un concepto genérico llamado stream

// // 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();