Как максимально эффективно обрабатывать большое количество одновременных запросов на запись на диск

Скажем, приведенный ниже метод вызывается несколько тысяч раз разными потоками в приложении .net 4. Как лучше всего поступить в этой ситуации? Понимаю, что узким местом здесь является диск, но хотелось бы, чтобы метод WriteFile() возвращался быстро.

Данные могут быть до нескольких МБ. Мы говорим о threadpool, TPL и т.п.?

public void WriteFile(string FileName, MemoryStream Data)
{
   try
   {
      using (FileStream DiskFile = File.OpenWrite(FileName))
      {
         Data.WriteTo(DiskFile);
         DiskFile.Flush();
         DiskFile.Close();
      }
   }
   catch (Exception e)
   {
      Console.WriteLine(e.Message);
   }
}

person Canacourse    schedule 14.09.2011    source источник
comment
Файл должен быть записан сразу? Кроме того, будет ли вместо этого работать база данных (для ваших нужд)?   -  person Cameron    schedule 14.09.2011
comment
Не сразу и не по порядку. Не могу использовать базу данных.   -  person Canacourse    schedule 14.09.2011
comment
Открытие файла стоит очень дорого. Запись файла очень дешева, если на машине достаточно оперативной памяти. Пара мегабайт не проблема.   -  person Hans Passant    schedule 14.09.2011


Ответы (4)


Поскольку вы говорите, что файлы не нужно записывать ни по порядку, ни сразу, самым простым подходом было бы использование Task:

private void WriteFileSynchronous(string FileName, MemoryStream Data)
{
    Task.Factory.StartNew(() => WriteFileSynchronously(FileName, Data));
}

private void WriteFileSynchronous(string FileName, MemoryStream Data)
{
    try
    {
        using (FileStream DiskFile = File.OpenWrite(FileName))
        {
            Data.WriteTo(DiskFile);
            DiskFile.Flush();
            DiskFile.Close();
        }
    }

    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }
}

TPL использует внутренний пул потоков и должен быть достаточно эффективным даже для большого количества задач.

person Cameron    schedule 14.09.2011
comment
@Canacourse: Нет! Пул потоков ограничивает количество фактических потоков выполнение работы. См. это сообщение в блоге об очередях кражи работы для хорошего объяснения того, как реализованы задачи. - person Cameron; 14.09.2011
comment
этот подход имеет свои ограничения, поскольку вы также не пытаетесь каким-либо образом контролировать поток. Представьте, что скорость записи на диск составляет 1 КБ в секунду (чтобы увидеть проблему более четко). Если вы получаете запросы на запись из Интернета с мегабайтами в секунду, ваше приложение быстро взорвется. - person Valentin Kuzub; 15.09.2011
comment
@Валентин: Хороший вопрос. Если это так для ОП, то мое решение ужасно! Если очереди последовательно заполняются быстрее, чем они могут быть освобождены, то в какой-то момент нам придется прекратить заполнение очередей и подождать, пока они немного опустеют. Ваш ответ намного лучше в таком случае (мой просто). Хорошая схема, кстати :-) - person Cameron; 15.09.2011
comment
Opps.только что обнаружил, что я не принял ответ. Интересно, что в .net 4.5 будет встроен асинхронный файловый ввод-вывод. - person Canacourse; 25.04.2012

Если вы хотите вернуться быстро и не заботитесь о том, чтобы операция была синхронной, вы можете создать какую-то память в памяти Queue, куда вы будете помещать запросы на запись, и пока очередь не заполнена, вы можете быстро вернуться из метода. Другой поток будет отвечать за отправку Queue и запись файлов. Если ваш WriteFile вызывается и очередь заполнена, вам придется подождать, пока вы не сможете поставить в очередь, и выполнение снова станет синхронным, но таким образом у вас может быть большой буфер, поэтому, если запросы на запись в файл процесса не являются линейными, а вместо этого более острыми ( с паузами между всплесками вызовов записи файла) такое изменение можно рассматривать как улучшение вашей производительности.

ОБНОВЛЕНИЕ: Сделал для вас небольшую картинку. Обратите внимание, что узкое место существует всегда, все, что вы можете сделать, это оптимизировать запросы с помощью очереди. Обратите внимание, что очередь имеет ограничения, поэтому, когда она заполнена, вы не можете помещать файлы в очередь, вам нужно подождать, чтобы в этом буфере также было свободное место. Но для ситуации, представленной на картинке (3 запроса корзины), очевидно, что вы можете быстро поставить корзины в очередь и вернуться, в то время как в первом случае вы должны сделать это по одной и заблокировать выполнение.

Обратите внимание, что вам никогда не нужно выполнять много потоков ввода-вывода одновременно, так как все они будут использовать одно и то же узкое место, и вы просто будете тратить память, если попытаетесь распараллелить это сильно, я полагаю, что 2-10 потоков максимум легко захватят всю доступную пропускную способность ввода-вывода. , а также ограничит использование памяти приложения.

введите здесь описание изображения

person Valentin Kuzub    schedule 14.09.2011
comment
Пробовал что-то подобное. Поместил ConcurrentBag перед WriteFile, но проблема в том, что файлы поступают ко мне из стороннего обратного вызова, поэтому ConcurrentBag оказался с кучей файлов и никогда не имел возможности очиститься до того, как поступило больше файлов. - person Canacourse; 14.09.2011
comment
Вот почему вам нужно проверить, заполнена ли очередь, прежде чем вы сможете вернуться. Установите некоторый максимальный размер для очереди и проверьте, меньше ли она заполнена. Если ваша скорость записи составляет 10 мб с, а входящие запросы настолько велики, что для их хранения вам потребуется 1 гигабайт с, то здесь ничего не поделаешь, без серьезных аппаратных изменений. - person Valentin Kuzub; 14.09.2011

Если данные поступают быстрее, чем вы можете их зарегистрировать, у вас есть реальная проблема. Дизайн производителя/потребителя, в котором WriteFile просто помещается материал в ConcurrentQueue или подобную структуру, а отдельный поток, обслуживающий эту очередь, отлично работает ... до тех пор, пока очередь не заполнится. И если вы говорите об открытии 50 000 различных файлов, резервное копирование будет происходить быстро. Не говоря уже о том, что ваши данные, которые могут составлять несколько мегабайт для каждого файла, будут еще больше ограничивать размер вашей очереди.

У меня была аналогичная проблема, которую я решил, добавив метод WriteFile к одному файлу. Записи, которые он записывал, имели номер записи, имя файла, длину, а затем данные. Как указал Ханс в комментарии к вашему исходному вопросу, запись в файл выполняется быстро; открытие файла происходит медленно.

Второй поток в моей программе начинает читать файл, в который пишет WriteFile. Этот поток читает заголовок каждой записи (номер, имя файла, длина), открывает новый файл, а затем копирует данные из файла журнала в окончательный файл.

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

Его недостатком является необходимость в 2 раза больше места на диске, но с 2-терабайтными дисками стоимостью менее 150 долларов я не считаю это большой проблемой. Кроме того, в целом это менее эффективно, чем прямая запись данных (поскольку вам приходится обрабатывать данные дважды), но имеет то преимущество, что основной поток обработки не останавливается.

person Jim Mischel    schedule 14.09.2011
comment
хорошо, если доступ ввода-вывода является узким местом, то вы предлагаете, чтобы 2 записывающих устройства работали одновременно, поэтому узкое место для записи становится в 2 раза меньше. Один поток пишет на диск без остановки, другой читает И записывает куда-то еще. если раньше очередь заполнялась за X раз, то теперь она заполняется намного быстрее, это даже не X/2. Представьте, что вы получаете входящие файлы размером 1 ГБ со скоростью, равной скорости записи на ваш диск. Теперь ваше решение просто не будет работать, в отличие от моего, которое будет использовать всю доступную скорость записи на диск ввода-вывода, а не половину или даже меньше. - person Valentin Kuzub; 15.09.2011
comment
В этом примере, чтобы иметь возможность даже позволить первому потоку выполнять свою работу по записи необработанных данных (что не даст нам близкого к конечному результату записи реальных файлов на диск), вам придется полностью отключить второй поток , или у вас есть джем, который быстро взорвет память. - person Valentin Kuzub; 15.09.2011
comment
@Valentin: Вы правы в том, что общая производительность системы будет ниже с моим подходом. Это займет больше времени. Но из вопроса ОП следует, что проблема заключается в том, чтобы рабочие не останавливались. Мое решение делает это, потому что метод WriteFile просто добавляется к файлу. Это очень быстрая операция. Узким местом является создание новых файлов, которые обрабатываются отдельным потоком. Да, будут некоторые разногласия по IO. Но рабочие потоки не будут блокироваться. Я написал и использую то, что очень похоже на то, что я описал. И это работает, как рекламируется. - person Jim Mischel; 15.09.2011
comment
ну, если мы говорим об абстрактных цифрах, да, открытие дороже, но это не связано с максимальной пропускной способностью ввода-вывода. Максимальная пропускная способность ввода-вывода фиксирована, и если наши процессы уже оптимизированы для записи на максимальной скорости (с помощью любого подхода, для простоты мы можем сказать, что обычно входящие файлы огромны, поэтому в основном это запись), мы создаем второй поток, который будет читать и писать это может быть убийцей производительности. так как мы получили X (по скорости) и Y (по скорости диска), если Y-X > 0, у нас все в порядке, но как только Y-X становится ‹0 на один байт в секунду, мы обречены на сбой приложения. - person Valentin Kuzub; 16.09.2011
comment
@Valentin: Несомненно, есть предел, когда это не удастся. Но это должно нормально работать для приложения OP. Пока кэш записи ОС достаточно велик, чтобы хранить поступающие данные, пока другой поток читает и записывает один файл, все работает нормально. Кэш записи будет буферизоваться, а затем ОС будет сброшена в файл за одну большую запись. Предполагая, конечно, что есть момент, когда данные перестают поступать или, по крайней мере, замедляются настолько, чтобы поток отложенной записи мог наверстать упущенное. Но это предположение встроено в любую схему кэширования. - person Jim Mischel; 16.09.2011

Инкапсулируйте полную реализацию метода в новый файл Thread(). Затем вы можете «запустить и забыть» эти потоки и вернуться к основному вызывающему потоку.

    foreach (file in filesArray)
    {
        try
        {
            System.Threading.Thread updateThread = new System.Threading.Thread(delegate()
                {
                    WriteFileSynchronous(fileName, data);
                });
            updateThread.Start();
        }
        catch (Exception ex)
        {
            string errMsg = ex.Message;
            Exception innerEx = ex.InnerException;
            while (innerEx != null)
            {
                errMsg += "\n" + innerEx.Message;
                innerEx = innerEx.InnerException;
            }
            errorMessages.Add(errMsg);
        }
    }
person Leon    schedule 14.09.2011
comment
запустить и забыть и сбой приложения, потому что вы так или иначе не контролируете количество этих одновременных потоков? - person Valentin Kuzub; 15.09.2011
comment
@Валентин Кузуб: По моему опыту, это прекрасно сработало. 20-100-150 потоков одновременно, и все работает отлично. Я использовал его для обработки данных и тяжелых вызовов веб-сервисов. Посмотрите на Thread.Join(), если вы хотите запустить несколько потоков одновременно, а затем подождите, пока все они вернутся, прежде чем продолжить. - person Leon; 15.09.2011
comment
Ну, я указываю вам на явную ошибку в вашем подходе, и вы говорите, что это красиво. Мне нечего добавить. - person Valentin Kuzub; 15.09.2011
comment
Где ошибка? Вы можете регулировать количество создаваемых потоков, если ожидаете большую нагрузку. - person Leon; 15.09.2011
comment
Рассматриваемое приложение может содержать более 50 000 файлов, большинство из которых должны быть зафиксированы на диске. - person Canacourse; 15.09.2011
comment
ну вы говорите 150 потоков, как насчет 10000 потоков с файлами по 1кб писать? Или больше? ты все еще считаешь это красивым или? - person Valentin Kuzub; 15.09.2011
comment
Канакурс: вы можете делать это партиями. Каждая партия будет синхронизирована, но внутри каждой партии она будет асинхронной. - person Leon; 15.09.2011
comment
Также метод Leon изначально не представляет вам массив файлов. Вам дается много вызовов метода с одним файлом, поэтому, если вы планируете сначала сгруппировать их в куски, чтобы ваш подход мог работать, мы перейдем к новым вопросам. Допустим, вы планируете записать фрагменты из 50 файлов и получили 30 запросов. Ты все еще ничего не пишешь? Что делать, если пользователи ожидают, что их файлы попадут в файловую систему не сразу, а в НЕКОТОРЫЙ МОМЕНТ после отправки запроса. здесь, если больше запросов не поступит, они никогда не увидят, что их файлы сбрасываются на диск. - person Valentin Kuzub; 15.09.2011
comment
Это называется обработка приложений — приходят какие-то данные, с ними что-то происходит, и они куда-то кладутся. Вы можете использовать очередь, которая заполняется вашими звонками. Таймер читает из очереди и сохраняет файлы асинхронно. Вы правы, я не предоставил полную основу для создания службы обработки. - person Leon; 15.09.2011
comment
ага, так что теперь ваши решения звучат очень похоже на мои;) просто должен сказать, что нет особого смысла выполнять несколько потоков записи файлов, потому что он не масштабируется, пара потоков или, может быть, 3-4 возьмут все доступные Пропускная способность ввода-вывода легко. 150 потоков не подходят для этих задач, вы просто тратите на них 150 МБ памяти. - person Valentin Kuzub; 15.09.2011
comment
Согласитесь, дисковый ввод-вывод плохо масштабируется. По крайней мере, в Windows доступ к IO оптимизирован как кэшем HD, так и ОС. У моего последнего работодателя мы обнаружили, что вещи пишутся быстрее с несколькими (10 или менее) одновременными многопоточными операциями записи, чем по одной за раз. Больше потоков, и это ускорение исчезнет и в конечном итоге станет похоже на синхронизацию. - person Leon; 15.09.2011