С# Параллельная библиотека, XmlReader, XmlWriter

У меня есть прецедент, где мне нужно:

  • итерация через каждый вход node в документе Xml
  • выполнить расчет по времени на каждом входе и
  • записать результаты в файл XML.

Ввод выглядит примерно так:

<Root>
  <Input>
    <Case>ABC123</Case>
    <State>MA</State>
    <Investor>Goldman</Investor>
  </Input>
  <Input>
    <Case>BCD234</Case>
    <State>CA</State>
    <Investor>Goldman</Investor>
  </Input>
</Root>

и вывод:

<Results>
  <Output>
    <Case>ABC123</Case>
    <State>MA</State>
    <Investor>Goldman</Investor>
    <Price>75.00</Price>
    <Product>Blah</Product>
  </Output>
  <Output>
    <Case>BCD234</Case>
    <State>CA</State>
    <Investor>Goldman</Investor>
    <Price>55.00</Price>
    <Product>Ack</Product>
  </Output>
</Results>

Я хотел бы запускать вычисления параллельно; типичный входной файл может содержать 50 000 входных узлов, а общее время обработки без потоковой передачи может составлять 90 минут. Примерно 90% времени обработки расходуется на шаге 2 (расчеты).

Я могу многократно прокручивать XmlReader параллельно:

static IEnumerable<XElement> EnumerateAxis(XmlReader reader, string axis)
{
  reader.MoveToContent();
  while (reader.Read())
  {
    switch (reader.NodeType)
    {
      case XmlNodeType.Element:
        if (reader.Name == axis)
        {
          XElement el = XElement.ReadFrom(reader) as XElement;
          if (el != null)
            yield return el;
        }
        break;
    }
  }
}
...
Parallel.ForEach(EnumerateAxis(reader, "Input"), node =>
{ 
  // do calc
  // lock the XmlWriter, write, unlock
});

В настоящее время я склонен использовать блокировку при записи в XmlWriter для обеспечения безопасности потоков.

Есть ли более элегантный способ обработки XmlWriter в этом случае? В частности, должен ли я иметь код Parallel.ForEach передать результаты обратно в исходный поток и связать этот поток с XmlWriter, избегая необходимости блокировки? Если это так, я не уверен в правильности этого подхода.

Ответ 1

Это мой любимый вид проблемы: тот, который можно решить с помощью конвейера.

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

Отказ от ответственности: вы должны в идеале рассмотреть TPL Dataflow для этого, но это не то, что я хорошо разбираюсь, поэтому я просто возьму знакомый маршрут Task + BlockingCollection<T>.

Сначала я собирался предложить трехэтапный конвейер (чтение, процесс, запись), но потом я понял, что вы уже объединили первые два этапа с тем, как вы "потоки" узлов, поскольку они читать и подавать их на ваш Parallel.ForEach (да, вы уже реализовали конвейер сортов). Еще лучше - меньше синхронизации потоков.

С учетом этого теперь код становится следующим:

public class Result
{
    public string Case { get; set; }
    public string State { get; set; }
    public string Investor { get; set; }
    public decimal Price { get; set; }
    public string Product { get; set; }
}

...

using (var reader = CreateXmlReader())
{
    // I highly doubt that this collection will
    // ever reach its bounded capacity since
    // the processing stage takes so long,
    // but in case it does, Parallel.ForEach
    // will be throttled.
    using (var handover = new BlockingCollection<Result>(boundedCapacity: 100))
    {
        var processStage = Task.Run(() =>
        {
            try
            {
                Parallel.ForEach(EnumerateAxis(reader, "Input"), node =>
                {
                    // Do calc.
                    Thread.Sleep(1000);

                    // Hand over to the writer.
                    // This handover is not blocking (unless our 
                    // blocking collection has reached its bounded
                    // capacity, which would indicate that the
                    // writer is running slower than expected).
                    handover.Add(new Result());
                });
            }
            finally
            {
                handover.CompleteAdding();
            }
        });

        var writeStage = Task.Run(() =>
        {
            using (var writer = CreateXmlReader())
            {
                foreach (var result in handover.GetConsumingEnumerable())
                {
                    // Write element.
                }
            }
        });

        // Note: the two stages are now running in parallel.
        // You could technically use Parallel.Invoke to
        // achieve the same result with a bit less code.
        Task.WaitAll(processStage, writeStage);
    }
}

Ответ 2

struct nodeParams
{
    internal string State;
    internal string Investor;
    internal double Price;
    internal string Product;
}

internal ConcurrentDictionary<string, nodeParams> cd = new ConcurrentDictionary<string, nodeParams>();

Затем измените свой код:

static IEnumerable<XElement> EnumerateAxis(XmlReader reader, string axis)
{
  reader.MoveToContent();
  while (reader.Read())
  {
    switch (reader.NodeType)
    {
      case XmlNodeType.Element:
        if (reader.Name == axis)
        {
          XElement el = XElement.ReadFrom(reader) as XElement;
          if (el != null)
            yield return el;
        }
        break;
    }
  }
}
...
Parallel.ForEach(EnumerateAxis(reader, "Input"), node =>
{ 
  nodeParams np = new nodeParams();

// do calc and put result in np and add to cd using Case as a Key

  });

// Update XML doc based on the content of cd