Что происходит с Directory.EnumerateFiles, если содержимое каталога изменяется во время итерации?

Я читал обсуждения о разнице между Directory.EnumerateFiles и Directory.GetFiles().

Я понимаю, что внутренне они оба используют System.IO.FileSystemEnumerableFactory.CreateFileNameIterator()

Разница заключается в том, что EnumerateFiles может использовать отложенное выполнение (lazy), а GetFiles () - ToArray, поэтому функция уже выполнена.

Но что произойдет, если файлы и папки будут добавлены в словарь во время итерации. Будет ли итерация повторять только те элементы, которые присутствовали во время EnumerateFiles()?

Еще хуже: что произойдет, если файлы будут удалены во время итераций: будут ли они все еще повторяться?

2 ответа

Спасибо Михал Коморовский. Однако, когда я сам попробовал его решение, я увидел замечательное различие между Directory.EnumerateFiles и Directory.GetFiles():

Directory.CreateDirectory(@"c:\MyTest");
// Create fies: b c e
File.CreateText(@"c:\MyTest\b.txt").Dispose();
File.CreateText(@"c:\MyTest\c.txt").Dispose();
File.CreateText(@"c:\MyTest\e.txt").Dispose();

string[] files = Directory.GetFiles(@"c:\MyTest");
var fileEnumerator = Directory.EnumerateFiles(@"c:\MyTest");

// delete file c; create file a d f
File.Delete(@"c:\MyTest\c.txt");
File.CreateText(@"c:\MyTest\a.txt").Dispose();
File.CreateText(@"c:\MyTest\d.txt").Dispose();
File.CreateText(@"c:\MyTest\f.txt").Dispose();

Console.WriteLine("Result from Directory.GetFiles");
foreach (var file in files) Console.WriteLine(file);
Console.WriteLine("Result from Directory.EnumerateFiles");
foreach (var file in fileEnumerator) Console.WriteLine(file);

Это даст другой результат.

Result from Directory.GetFiles
c:\MyTest\b.txt
c:\MyTest\c.txt
c:\MyTest\e.txt
Result from Directory.EnumerateFiles
c:\MyTest\b.txt
c:\MyTest\d.txt
c:\MyTest\e.txt
c:\MyTest\f.txt

Результаты:

  • GetFiles все еще видел старые файлы: B C E, как и ожидалось
  • EnumerateFiles увидел новые файлы D и F. Он правильно пропустил удаленный файл C, но пропустил новый файл A.

Таким образом, разница в использовании между EnumerateFiles и GetFiles заключается не только в производительности.

  • GetFiles возвращает файлы, которые были в папке в тот момент, когда вы вызвали функцию. Что и следовало ожидать, потому что это просто перечисление по коллекции строк
  • EnumerateFiles правильно пропускает удаленные файлы, но не видит все добавленные файлы. Если папка изменяется при перечислении, результат довольно неопределен.

Поэтому, если вы ожидаете, что ваша папка изменится при перечислении, тщательно выберите нужную функцию

  • Ожидайте GetFiles, чтобы увидеть удаленные файлы
  • Ожидайте, что EnumerateFiles пропустит некоторые из новых файлов.

Есть только один способ проверить:

Directory.CreateDirectory(@"c:\\Temp");
File.Create(@"c:\\Temp\\a.txt").Close();
File.Create(@"c:\\Temp\\b.txt").Close();
File.Create(@"c:\\Temp\\c.txt").Close();
foreach (var f in Directory.EnumerateFiles(@"c:\\Temp"))
{
    Console.WriteLine(f);
    //Let's delete a file
    File.Delete(@"c:\\Temp\\c.txt");
    //Let's create a new file
    File.Create(@"c:\\Temp\\d.txt").Close();
}

Изначально C:\Temp содержит 3 файла: a.txt, b.txt и c.txt. Во время итерации один из этих файлов удаляется, а другой создается. Наконец, C:\Temp содержит следующие файлы: a.txt, b.txt и d.txt. Однако в консоли вы увидите исходное содержимое этого каталога, а именно:

c:\\Temp\a.txt
c:\\Temp\b.txt
c:\\Temp\c.txt

Я провел другой эксперимент, потому что меня интересовал случай медленного перечисления файлов, когда больше файлов создается внутри перечисляемого каталога. Сценарий медленного перебора может случиться, например, если есть SemaphoreSlim.WaitAsync внутри цикла перечисления (для целей регулирования). Приведенный ниже эксперимент начинается с удаления всех файлов из целевого каталога, затем создается определенное количество исходных файлов, а затем начинается перечисление файлов с задержкой 100 мс, в то время как другой асинхронный рабочий процесс создает дополнительные файлы со скоростью один файл каждые 150 мс.. Увидит ли счетчик вновь созданные файлы?

static async Task Main(string[] args)
{
    const string FOLDER_PATH = @"C:\DirectoryEnumerateFilesTest";
    const int FILES_COUNT = 10;
    Console.WriteLine($"Deleting files");
    DeleteAllFiles(FOLDER_PATH);
    Console.WriteLine($"Creating files");
    await CreateFiles(FOLDER_PATH, startIndex: 1, filesCount: FILES_COUNT, delay: 0);
    Console.WriteLine($"Enumerating files while creating more files");
    var filePaths = Directory.EnumerateFiles(FOLDER_PATH);
    var cts = new CancellationTokenSource();
    var producer = CreateFiles(FOLDER_PATH,
        startIndex: 501, filesCount: 100, delay: 150, cts.Token);
    var enumeratedCount = 0;
    foreach (var filePath in filePaths)
    {
        Console.WriteLine($"Enumerated:   {Path.GetFileName(filePath)}");
        await Task.Delay(100);
        enumeratedCount++;
    }
    Console.WriteLine($"Total files enumerated: {enumeratedCount:#,0}");
    cts.Cancel();
    await producer;
}

private static void DeleteAllFiles(string folderPath)
{
    int count = 0;
    foreach (var filePath in Directory.GetFiles(folderPath))
    {
        File.Delete(filePath);
        Console.WriteLine($"File deleted: {Path.GetFileName(filePath)}");
        count++;
    }
    Console.WriteLine($"Total files deleted: {count:#,0}");
}

private static async Task CreateFiles(string folderPath,
    int startIndex, int filesCount, int delay, CancellationToken token = default)
{
    int count = 0;
    foreach (var i in Enumerable.Range(startIndex, filesCount))
    {
        var delayTask = Task.Delay(delay, token);
        await Task.WhenAny(delayTask);
        if (delayTask.IsCanceled) break;
        var fileName = $"File-{i:000}.txt";
        var filePath = Path.Combine(folderPath, fileName);
        File.WriteAllText(filePath, "Content");
        count++;
        Console.WriteLine($"File created: {fileName}");
    }
    Console.WriteLine($"Total files created: {count:#,0}");
}

Ответ: это зависит от количества исходных файлов и длины имен файлов. Порог составляет около 50 начальных файлов, но он становится меньше, когда файлы имеют более длинные имена. Перечисление в конечном итоге прекратится при условии, что перечислитель работает быстрее, чем производитель файлов, и в этом случае некоторое количество файлов останется незамеченным (обычно около 20).

Вот результат вышеупомянутого эксперимента для FILES_COUNT = 10 (имеется в виду 10 существующих файлов на момент создания счетчика).

Deleting files
Total files deleted: 0
Creating files
File created: File-001.txt
File created: File-002.txt
File created: File-003.txt
File created: File-004.txt
File created: File-005.txt
File created: File-006.txt
File created: File-007.txt
File created: File-008.txt
File created: File-009.txt
File created: File-010.txt
Total files created: 10
Enumerating files while creating more files
Enumerated:   File-001.txt
Enumerated:   File-002.txt
File created: File-501.txt
Enumerated:   File-003.txt
File created: File-502.txt
Enumerated:   File-004.txt
Enumerated:   File-005.txt
File created: File-503.txt
Enumerated:   File-006.txt
File created: File-504.txt
Enumerated:   File-007.txt
Enumerated:   File-008.txt
File created: File-505.txt
Enumerated:   File-009.txt
File created: File-506.txt
Enumerated:   File-010.txt
Total files enumerated: 10
File created: File-507.txt
Total files created: 7

10 файлов - это слишком мало, поэтому счетчик не заметил ни одного из файлов, созданных впоследствии.

Другие вопросы по тегам