Не совсем понимаю работу многопоточности, в моем представлении многопоточность выглядит так.
Это человек который роет яму (один поток) и к нему ты добавляешь еще одного и типа они должны рыть два раза быстрее. Но тут встает вопрос о разделении площади работы на двоих, чтобы они друг-другу не мешали? Так ли это?
Чтобы перейти к более практичным вещам, я разрабатываю парсер, который работает по такой схеме.
У меня есть xml файл с ссылками на страницу товара, я читаю этот xml и затем сразу же перехожу по ссылке загружаю не достающую информацию по товару в базу.
Скорость работы меня не устраивает и я решил разобраться в многопоточности.
Обдумываю такую схему, я читаю весь xml и записываю его в базу, после того как я его полностью записал в базу, начинаю создавать потоки и каждому из них передавать записи, одному например четную запись из базы, другому не четную.
По идеи скорость должна возрасти в два раза и не возникнет ли у меня проблем если эти два потока решат одновременно произвести операцию записи в базу. И вообще является ли мой алгоритм правильным?
Будет приятно, если вы напишите на образных примерах и немного кода тоже не помешает.
Ответ
Дело в том, что нет смысла читать файл(ы) с одного физического накопителя в несколько потоков - это абсолютно не увеличит производительность (а может даже уменьшит). Так что, в вашем случае, скорее всего, узким местом будет именно чтение xml.
А вот дальнейшую обработку, уже после чтения с диска, вполне можно выполнять в отдельных потоках. Для этого удобно применять конвейеры (pipelines).
Например, шаблон может выглядеть следующим образом:
var inputValues = new BlockingCollection
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
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
Таким образом, заполнив коллекцию до указанного значения, он будет ждать, пока другие потоки не выберут данные.
Обработку данных внутри каждой стадии конвейера можно дополнительно распараллелить, если это необходимо и вообще возможно:
foreach (var value in inputValues.GetConsumingEnumerable()
.AsParallel() // распараллеливаем обработку
.AsOrdered() // обеспечиваем правильный порядок, если нужно
)
{
// обрабатываем value
}
Можно ли в несколько потоков записывать данные в БД? Вероятно, на эту тему нужно задать отдельный вопрос, обязательно указав, какая именно СУБД используется и прочую информацию.
Многие СУБД имеют специальные возможности по массовому эскпорту и импорту данных. Например, SQL Server. Вероятно, их использование будет эффективней, чем ручное написание многопоточного кода.