Я создаю игрушечную базу данных на C#, чтобы узнать больше о компиляторах, оптимизаторах и технологиях индексирования.
Я хочу поддерживать максимальный параллелизм между запросами (по крайней мере на чтение) для переноса страниц в буферный пул, но я не понимаю, как лучше всего это сделать в .NET.
Вот несколько вариантов и проблем, с которыми я столкнулся с каждым:
Используйте System.IO.FileStream и метод BeginRead
Но позиция в файле не является аргументом для BeginRead, это свойство FileStream (устанавливается с помощью метода Seek), поэтому я могу отправлять только один запрос за раз и должен блокировать поток на время. (Или я? В документации неясно, что произойдет, если я удерживаю блокировку только между вызовами Seek и BeginRead, но снимаю ее перед вызовом EndRead. Кто-нибудь знает?) Я знаю, как это сделать, я просто не уверен это лучший способ.
Кажется, есть другой способ, основанный на структуре System.Threading.Overlapped и P \ Invoke функции ReadFileEx в kernel32.dll.
К сожалению, не хватает образцов, особенно на управляемых языках. Этот маршрут (если его вообще можно заставить работать), по-видимому, также включает метод ThreadPool.BindHandle и потоки завершения ввода-вывода в пуле потоков. У меня сложилось впечатление, что это санкционированный способ работы с этим сценарием под Windows, но я этого не понимаю и не могу найти точку входа в документацию, которая была бы полезна для непосвященных.
Что-то другое?
В комментарии Джейкоб предлагает создавать новый FileStream для каждого чтения в полете.
Прочитать весь файл в память.
Это сработало бы, если бы база данных была небольшой. Кодовая база мала, и есть много других недостатков, но сама база данных - нет. Я также хочу быть уверенным, что выполняю всю бухгалтерию, необходимую для работы с большой базой данных (которая оказывается огромной частью сложности: разбиение на страницы, внешняя сортировка, ...), и я беспокоюсь, что это может быть слишком легко случайно обмануть.
Редактировать
Разъяснение того, почему я подозрительно отношусь к решению 1: удержание единственной блокировки на всем пути от BeginRead до EndRead означает, что мне нужно заблокировать любого, кто хочет инициировать чтение только потому, что выполняется другое чтение. Это кажется неправильным, потому что поток, инициирующий новое чтение, мог бы (в общем) проделать еще некоторую работу, прежде чем результаты станут доступными. (На самом деле, просто написание этого заставило меня придумать новое решение, которое я поставил как новый ответ.)





Я не уверен, что понимаю, почему вариант 1 не сработает для вас. Имейте в виду, что у вас не может быть двух разных потоков, пытающихся одновременно использовать один и тот же FileStream - это определенно вызовет проблемы. BeginRead / EndRead предназначен для того, чтобы ваш код продолжал выполняться, пока выполняется потенциально дорогостоящая операция ввода-вывода, а не для обеспечения какого-либо многопоточного доступа к файлу.
Поэтому я бы посоветовал вам поискать, а затем начать чтение.
Что, если вы сначала загрузите ресурс (данные файла или что-то еще) в память, а затем поделитесь им между потоками? Поскольку это небольшой дб. - у вас не будет столько проблем.
В некоторых случаях это работает, но я имел в виду «маленький» в смысле «мало функций», а не «мало данных».
Используйте подход №1, но
При поступлении запроса возьмите блокировку A. Используйте ее для защиты очереди ожидающих запросов на чтение. Добавьте его в очередь и верните новый асинхронный результат. Если это приводит к первому добавлению в очередь, вызовите шаг 2 перед возвратом. Отпустите фиксатор A перед возвращением.
Когда чтение завершается (или вызывается на шаге 1), снимите блокировку A. Используйте ее для защиты от выталкивания запроса на чтение из очереди. Возьмите замок B. Используйте его для защиты последовательности Seek -> BeginRead -> EndRead. Снять блокировку B. Обновить результат асинхронной обработки, созданный на шаге 1 для этой операции чтения. (Поскольку операция чтения завершена, вызовите это снова.)
Это решает проблему отсутствия блокировки любого потока, который начинает чтение только потому, что выполняется другое чтение, но по-прежнему считываются последовательности, так что текущая позиция файлового потока не нарушается.
Что мы сделали, так это написали небольшой слой вокруг портов завершения ввода-вывода, ReadFile и статуса GetQueuedCompletion в C++ / CLI, а затем обратный вызов в C# после завершения операции. Мы выбрали этот маршрут вместо BeginRead и шаблона асинхронной операции C#, чтобы обеспечить больший контроль над буферами, используемыми для чтения из файла (или сокета). Это был довольно большой выигрыш в производительности по сравнению с чисто управляемым подходом, который выделяет новый byte [] в куче при каждом чтении.
Кроме того, в сети есть более полные примеры использования портов завершения ввода-вывода на C++.
Это хорошая идея. Вы также можете избежать выделения новых байтов [] (и перегрузки кучи больших объектов), предварительно выделяя их большими порциями при создании (или увеличении) пула буферов.
Кроме того, я не стал сейчас говорить о GetQueuedCompletionStatus (или как-то его не читал), что, вероятно, объясняет, почему мои попытки этого не удались. Пора почитать еще.
Согласовано; вы должны использовать новый объект FileStream для каждого асинхронного чтения в полете.