3

Не совсем понимаю работу многопоточности, в моем представлении многопоточность выглядит так.

Это человек который роет яму (один поток) и к нему ты добавляешь еще одного и типа они должны рыть два раза быстрее. Но тут встает вопрос о разделении площади работы на двоих, чтобы они друг-другу не мешали? Так ли это?

Чтобы перейти к более практичным вещам, я разрабатываю парсер, который работает по такой схеме.

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

Скорость работы меня не устраивает и я решил разобраться в многопоточности.

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

По идеи скорость должна возрасти в два раза и не возникнет ли у меня проблем если эти два потока решат одновременно произвести операцию записи в базу. И вообще является ли мой алгоритм правильным?

Будет приятно, если вы напишите на образных примерах и немного кода тоже не помешает.

shatoidil
  • 1,492

2 Answers2

4

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

А вот дальнейшую обработку, уже после чтения с диска, вполне можно выполнять в отдельных потоках. Для этого удобно применять конвейеры (pipelines).

Например, шаблон может выглядеть следующим образом:

var inputValues = new BlockingCollection<string>();

var readXml = Task.Run(() =>
{
    try
    {
        using (var xmlReader = XmlReader.Create("test.xml"))
        {
            while (xmlReader.ReadToFollowing("nodeName"))
                inputValues.Add(xmlReader.ReadElementContentAsString());
        }
    }
    finally { inputValues.CompleteAdding(); }
});

var processValues = Task.Run(() =>
{
    foreach (var value in inputValues.GetConsumingEnumerable())
    {
        // обрабатываем value
    }
});

Task.WaitAll(readXml, processValues);

Такой способ также называют производитель-потребитель (producer-consumer). Один поток производит данные - другой(ие) их потребляет.

Использование BlockingCollection автоматически обеспечивает много преимуществ, о которых можно почитать в справке.

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

var inputValues = new BlockingCollection<string>();
var processedValues = new BlockingCollection<string>();

var readXml = Task.Run(() =>
{
    try
    {
        using (var xmlReader = XmlReader.Create("test.xml"))
        {
            while (xmlReader.ReadToFollowing("nodeName"))
                inputValues.Add(xmlReader.ReadElementContentAsString());
        }
    }
    finally { inputValues.CompleteAdding(); }
});

var processValues = Task.Run(() =>
{
    try
    {
        foreach (var value in inputValues.GetConsumingEnumerable())
        {
            // обрабатываем value
            var newValue = Process(value);
            processedValues.Add(newValue);
        }
    }
    finally { processedValues.CompleteAdding(); }
});

var writeToDB = Task.Run(() =>
{
    foreach (var value in processedValues.GetConsumingEnumerable())
    {
        // записываем данные в БД
    }
});

Task.WaitAll(readXml, processValues, writeToDB);

Если один из потоков работает намного быстрее других и забивает память, то можно ограничить ёмкость его коллекции:

var inputValues = new BlockingCollection<string>(boundedCapacity: 50);

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

Обработку данных внутри каждой стадии конвейера можно дополнительно распараллелить, если это необходимо и вообще возможно:

foreach (var value in inputValues.GetConsumingEnumerable()
    .AsParallel() // распараллеливаем обработку
    .AsOrdered() // обеспечиваем правильный порядок, если нужно
    )
{
    // обрабатываем value
}

Можно ли в несколько потоков записывать данные в БД? Вероятно, на эту тему нужно задать отдельный вопрос, обязательно указав, какая именно СУБД используется и прочую информацию.

Многие СУБД имеют специальные возможности по массовому эскпорту и импорту данных. Например, SQL Server. Вероятно, их использование будет эффективней, чем ручное написание многопоточного кода.

3

Парсить xml многопоточно - это сомнительная затея. Зачем-то использовать базу - на мой взгляд, тоже. По крайней мере, в вопросе я не увидел ничего, для чего была бы нужна БД.

А вот посылать запросы за недостающей информацией в отдельном потоке было бы вполне логично. Один поток парсит xml, и запрашивает недостающую информацию через другой. Также стоит обдумать, сколько одновременных соединений можно использовать.

Думаю, для этого подойдёт Parallel.For или можно самому написать что-нибудь подобное.

PS: Стоит обратить внимание на эту статью.

Qwertiy
  • 123,725