Это выглядит как хороший кандидат для конвейерной обработки .
Основная идея состоит в том, чтобы иметь 3 одновременных Задачи s, по одному для каждой "стадии" в конвейере, взаимодействующих друг с другом через очереди ( BlockingCollection ):
- Первая задача читает строку входного файла построчно и помещает строки чтения в очередь.
- Вторая задача получает строки из очереди, форматирует их и помещает результат в другую очередь.
- Третье задание получает отформатированные результаты из второй очереди и записывает их в результирующий файл.
В идеале задача 1 должна не ждать завершения задачи 2, прежде чем перейти к следующему файлу.
Вы даже можете прийти в бешенство и поместить конвейер каждого отдельного файла в отдельную параллельную задачу, но это может так сильно повредить голову вашего жесткого диска, что это, вероятно, повредит больше, чем поможет. С другой стороны, для твердотельных накопителей это может быть оправдано - во всяком случае, до принятия решения.
--- РЕДАКТИРОВАТЬ ---
Используя однопоточную реализацию Джона Скита в качестве базы, вот как должна выглядеть конвейерная версия (рабочий пример):
class Test {
struct Queue2Element {
public string CPF;
public List<string> Years;
public string XML;
}
public static void Main() {
Stopwatch stopwatch = Stopwatch.StartNew();
var queue1 = new BlockingCollection<string>();
var task1 = new Task(
() => {
foreach (var line in File.ReadLines("input.txt"))
queue1.Add(line);
queue1.CompleteAdding();
}
);
var queue2 = new BlockingCollection<Queue2Element>();
var task2 = new Task(
() => {
foreach (var line in queue1.GetConsumingEnumerable())
queue2.Add(
new Queue2Element {
CPF = ParseCpf(line),
XML = ParseXml(line),
Years = ParseYears(line).ToList()
}
);
queue2.CompleteAdding();
}
);
var task3 = new Task(
() => {
var lines =
from element in queue2.GetConsumingEnumerable()
from year in element.Years
select element.CPF + year + element.XML;
File.WriteAllLines("output.txt", lines);
}
);
task1.Start();
task2.Start();
task3.Start();
Task.WaitAll(task1, task2, task3);
stopwatch.Stop();
Console.WriteLine("Completed in {0}ms", stopwatch.ElapsedMilliseconds);
}
// Returns the CPF, in the form "CPF=xxxxxx;"
static string ParseCpf(string line) {
int start = line.IndexOf("CPF=");
int end = line.IndexOf(";", start);
// TODO: Validation
return line.Substring(start, end + 1 - start);
}
// Returns a sequence of year values, in the form "YEAR=2010;"
static IEnumerable<string> ParseYears(string line) {
// First year.
int start = line.IndexOf("YEARS=") + 6;
int end = line.IndexOf(" ", start);
// TODO: Validation
string years = line.Substring(start, end - start);
foreach (string year in years.Split(';')) {
yield return "YEARS=" + year + ";";
}
}
// Returns all the XML from the leading space onwards
static string ParseXml(string line) {
int start = line.IndexOf(" <?xml");
// TODO: Validation
return line.Substring(start);
}
}
Как оказалось, параллельная версия выше лишь немного быстрее, чем последовательная версия. По-видимому, задача связана с вводом-выводом больше, чем что-либо еще, поэтому конвейерная обработка не сильно помогает. Если вы увеличите объем обработки (например, добавите надежную проверку), это может изменить ситуацию в пользу параллелизма, но сейчас вам, вероятно, лучше всего сосредоточиться на последовательных улучшениях (как отметил сам Джон Скит, код не так как можно быстрее).
(Кроме того, я протестировал с кэшированными файлами - мне интересно, есть ли способ очистить кэш файлов Windows и посмотреть, позволит ли аппаратная очередь ввода-вывода глубиной 2 позволить жесткому диску оптимизировать движения головы по сравнению с I / O глубина 1 серийной версии.)