Фирма

«Инрэко ЛАН»

Работа с директориями/файлами

В пространство имён System.IO добавлены методы, которые возвращают не массив, а перечислитель (Enumerator). По заявлениям Microsoft эти методы позволят повысить производительность приложений, которые работают с большим количеством директорий/файлов на дисках. Давайте посмотрим, как это может выглядеть на практике. Допустим, на диске C необходимо найти все файлы с расширением .txt, размер которых превышает 1 мегабайт,  и посчитать количество строк в этих файлах. При использовании нового метода EnumerateFiles() обработку найденного файла можно начинать без ожидания окончания поиска всех требуемых файлов. Для этого, разумеется, придётся запускать отдельный поток, чтобы не приостанавливать выполнение основной программы. При вызове же метода GetFiles() сначала формируется массив, который поэлементно обрабатывается после заполнения (в примере подсчет количества строк выполняется в отдельном потоке для обоих вариантов; это сделано для наглядности выполнения по времени, не более). Ниже приведён исходный код примера.

private static int threadCount;
static void Main(string[] args)
{
var dirInfo = new DirectoryInfo(@"C:\");
var pattern = "*.txt";
var fileSize = 1024 * 1024;

threadCount = 0;
var stopWatchOld = Stopwatch.StartNew();
foreach (var fileInfo in dirInfo.GetFiles(pattern, SearchOption.AllDirectories))
{
if (fileInfo.Length >= fileSize)
{
new Thread(o => CalculateLinesCount(Convert.ToString(o))).Start(fileInfo.FullName);
}
}
Console.WriteLine("Searching of files finished.");
while (threadCount != 0)
{
Thread.Sleep(500);
Console.WriteLine("Waiting while all threads executing will be finished...");
}
stopWatchOld.Stop();

threadCount = 0;
var stopWatchNew = Stopwatch.StartNew();
foreach (var fileInfo in dirInfo.EnumerateFiles(pattern, SearchOption.AllDirectories))
{
if (fileInfo.Length >= fileSize)
{
new Thread(o => CalculateLinesCount(Convert.ToString(o))).Start(fileInfo.FullName);
}
}
Console.WriteLine("Searching of files finished.");
while (threadCount != 0)
{
Thread.Sleep(500);
Console.WriteLine("Waiting while all threads executing will be finished...");
}
stopWatchNew.Stop();

Console.WriteLine("Time elapsed (new): {0}", stopWatchNew.Elapsed);
Console.WriteLine("Time elapsed (old): {0}", stopWatchOld.Elapsed);
}

private static void CalculateLinesCount(string fileName)
{
try
{
Interlocked.Increment(ref threadCount);
Console.WriteLine("File {0} has {1} lines.", fileName, File.ReadLines(fileName).Count());
}
catch (Exception e)
{
Console.WriteLine("File {0} cannot be processed: {1}", fileName, e.Message);
}
finally
{
Interlocked.Decrement(ref threadCount);
}
}

Если присмотреться, то можно заметить ещё одно изменение, которое вошло в 4-ую версию фреймворка − это статический метод File.ReadLines(). Он предоставляет схожие возможности с методом EnumerateFiles() (т.е. возвращает Enumerator вместо массива) и может использоваться при чтении строк из текстового файла. При таком способе чтения имеется возможность обрабатывать нужные строки, не дожидаясь окончания просмотра всего файла. Например, можно сохранять полученные значения в БД, пока идёт чтение файла (необходимо использовать отдельный поток, по аналогии с директориями).

Результаты выполнения представленного выше примера могут сильно разниться в зависимости от компьютера и директории, в которой будет осуществляться поиск. Все последние изменения в фреймворке направлены на использование многопоточного программирования, которое, при наличии многопроцессорного компьютера, будет более выигрышным в сравнении с последовательной обработкой данных.

Специальные папки в Environment.SpecialFolder

Данное перечисление (enum) определяет набор констант для получения полного пути к специальным системным папкам. В .NET Framework 4 список констант пополнился новыми значениями. Для отображения значения и соответствующей ему директории можно воспользоваться следующим примером:

foreach(Environment.SpecialFolder folder in
Enum.GetValues(typeof(Environment.SpecialFolder)))
{
Console.WriteLine("Name: {0}, path: {1}",
folder, Environment.GetFolderPath(folder));
}

Файлы, отображаемые в памяти (Memory-Mapped Files)

Такими называют файлы, содержимое которых находится в виртуальной памяти. Cоответствие между файлом и памятью позволяет приложению осуществлять чтение/запись напрямую в память. Начиная с версии 4, стало возможным использовать управляемый код (Managed Code) для доступа к файлам, отображаемым в памяти. Существует два типа таких файлов:

  1. Сохраняемые. Такие файлы связаны с файлом-источником на диске. И когда последний процесс закончит работу с таким файлом, данные будут сохранены на диск. Данный тип  обычно используется для обработки очень больших по размеру файлов.
  2. Несохраняемые. В свою очередь, это файлы, которые не связаны с областью памяти на диске и после окончания работы данные теряются, а занимаемая память освобождается Garbage Collector'ом. Данный тип файлов подходит для создания совместно используемой памяти при межпроцессовых взаимодействиях (IPC). Примечание: использование термина «файл» в данном случае уместно. Если вспомнить определение. Файл − именованная область памяти. В примере будет видно, что работа с несохраняемым файлом осуществляется через имя. Для файлов описанного типа определены всего лишь два атрибута: имя и размер.

Пример работы с сохраняемым файлом. Открывается файл и на экран выводятся первые и последние 256 символов в виде строки.

string fileName = @"c:\temp\large_file.dat";
int charCount = 256;
using (var file = MemoryMappedFile.CreateFromFile(fileName, FileMode.Open))
{
byte[] buffer = new byte[charCount];
using (var accessor = file.CreateViewAccessor(0, charCount))
{
accessor.ReadArray(0, buffer, 0, charCount);
Console.WriteLine("Content of first {0} chars: {1}", charCount, Encoding.ASCII.GetString(buffer));
}

using (var accessor = file.CreateViewAccessor(new FileInfo(fileName).Length - charCount, charCount))
{
accessor.ReadArray(0, buffer, 0, charCount);
Console.WriteLine("Content of last {0} chars: {1}", charCount, Encoding.ASCII.GetString(buffer));
}
}

Теперь давайте рассмотрим пример работы с несохраняемыми файлами. В примере будет реализован механизм межпроцессового взаимодействия с использованием несохраняемого файла. Имеется два консольных приложения, первое приложение прочитает и выведет на экран сообщение, которое будет записано в несохраняемый файл вторым приложением.

Листинг приложения №1:

static void Main(string[] args)
{
using (var file = MemoryMappedFile.CreateNew("TestIPC", 512))
{
bool mutexCreated;
var mutex = new Mutex(true, "TestIPCMutex", out mutexCreated);
Console.WriteLine("Start process #2 and press ENTER to continue.");
Console.ReadLine();
mutex.ReleaseMutex();
mutex.WaitOne();
using (var stream = file.CreateViewStream())
using (var sw = new StreamReader(stream))
{
Console.WriteLine("Process #2 said: {0}", sw.ReadLine());
}
mutex.ReleaseMutex();
}
Console.ReadLine();
}

Листинг приложения №2:

static void Main(string[] args)
{
try
{
using (var file = MemoryMappedFile.OpenExisting("TestIPC"))
{
var mutex = Mutex.OpenExisting("TestIPCMutex");
Console.WriteLine("Press ENTER in process #1.");
mutex.WaitOne();
using (var stream = file.CreateViewStream())
using (var sw = new StreamWriter(stream))
{
sw.WriteLine("Hello from process #2.");
}
mutex.ReleaseMutex();
Console.WriteLine("Check process #1.");
}
}
catch (FileNotFoundException)
{
Console.WriteLine("Memory-mapped file does not exist. Run process #1.");
}
Console.ReadLine();
}

Отмечу, что при работе с файлами, отображаемыми в памяти, должен создаваться View, который является представлением всего файла или его части. Через представление осуществляется  чтение/запись данных в файл. Можно создавать несколько представлений, которые будут работать с одним файлом. Также не запрещено создавать представления, которые будут перекрывать друг друга, тем самым, образуя конкурентные участки памяти. Технически невозможно создать представление размером более 2 Гб, поэтому для работы с файлами, размер которых превышает 2 Гб, в любом случае придется использовать несколько представлений для одновременного доступа к любой из его частей. Имеется два вида представлений. Первый вид − представление потокового доступа, когда чтение/запись осуществляется через работу с потоком данных. И второй вид − представление произвольного доступа, при котором чтение/запись осуществляется в произвольном порядке. Первый рекомендуется для реализации межпроцессного взаимодействия, второй − для работы с сохраняемыми файлами. Поскольку жесткого ограничения на использование того или иного вида представления не накладывается, можно пользоваться ими по своему усмотрению. На свой страх и риск, как говорится.

Работа с директориями/файлами является неотъемлемой частью практически любого программного продукта. И, как видно из вышеизложенного, многое будет полезным при разработке прикладного ПО.

Продолжение следует...

Метки: .NET 4 | C# | enumerate directories/files | IPC | memory-mapped file | Microsoft | новые возможности

Добавить комментарий