2

У меня есть служба WCF .NET 4.5 Single Instance, которая поддерживает набор элементов в списке, которые будут иметь одновременные одновременные читатели и писатели, но с гораздо большим количеством читателей, чем писатели.ConcurrentBag vs Custom Thread Safe List

я в настоящее время решение о том, чтобы использовать BCL ConcurrentBag<T> или использовать свой собственный общий ThreadSafeList класс (который простирается IList<T> и инкапсулирует BCL ReaderWriterLockSlim как это больше подходит для нескольких одновременных читателей).

Я обнаружил многочисленные различия в производительности при тестировании этих реализаций путем моделирования параллельного сценария чтения 1 м (просто запуск запроса Sum Linq) и всего 100 авторов (добавление элементов в список).

Для моего теста производительности У меня есть список задач:

List<Task> tasks = new List<Task>(); 

Тест 1: Если я создаю 1е задачи читателя следует 100 писательских задачи, используя следующий код:

tasks.AddRange(Enumerable.Range(0, 1000000).Select(n => new Task(() => { temp.Where(t => t < 1000).Sum(); })).ToArray()); 
tasks.AddRange(Enumerable.Range(0, 100).Select(n => new Task(() => { temp.Add(n); })).ToArray()); 

Я получаю следующие результаты синхронизации:

  • ConcurrentBag: ~ 300мс
  • ThreadSafeList: ~ 520ms

Тест 2: Тем не менее, если я создаю 1е задачи читателя смешан с 100 задачами писателя (когда список задач, которые должны быть выполнено может быть {читатель, читатель, писатель, читатель, читатель, писатель и т.д.}

foreach (var item in Enumerable.Range(0, 1000000)) 
{ 
    tasks.Add(new Task(() => temp.Where(t => t < 1000).Sum())); 
    if (item % 10000 == 0) 
     tasks.Add(new Task(() => temp.Add(item))); 
} 

я получить следующие результаты синхронизации:

  • ConcurrentBag: ~ 4000ms
  • ThreadSafeList: ~ 800ms

Мой код для получения времени выполнения для каждого теста выглядит следующим образом:

Stopwatch watch = new Stopwatch(); 
watch.Start(); 
tasks.ForEach(task => task.Start()); 
Task.WaitAll(tasks.ToArray()); 
watch.Stop(); 
Console.WriteLine("Time: {0}ms", watch.Elapsed.TotalMilliseconds); 

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

Q1. Почему результаты отличаются друг от друга, когда единственное, что я меняю, - это упорядочение задач в списке?

Q2. Есть ли лучший способ изменить мой тест, чтобы сделать его более справедливым?

+1

ConcurrentBag оптимизирован для одной и той же темы, которая написана в сумке, которая считывается из сумки. Если у вас нет того же самого потока, который читается из переключателя мешка, вместо ConcurrentQueue. Прочитайте [эту страницу MSDN] (https://msdn.microsoft.com/en-us/library/dd997373 (v = vs.110) .aspx), в ваших примерах вы используете «Чистый сценарий производителя-потребителя», каждая нить либо читает, либо только записывает в сумку, ни одна нить не читает и не записывает в тот же поток. –

+0

Я согласен - это приводит меня к тому, чтобы спросить, когда вы когда-либо используете ConcurrentBag, потому что из моего понимания вы не можете одновременно читать и писать в одном и том же потоке ........ можете ли вы? Поэтому у каждого потока будет свой собственный список. Наверное, называть Сумку частью параллельных коллекций было плохой идеей из-за ее неэффективности в многопоточном сценарии? .... если нет чего-то, чего я не вижу! – user978139

+0

Сумка предназначена для ситуаций, когда у вас есть куча рабочих потоков, которые все дампы работают в пул, а затем по завершении выходят из того же пула. Если работник заканчивает всю работу, которую он вложил (способ, в котором он хранит данные внутри, он вернет данные, в которые вставлен поток), он начнет «красть» работу из других потоков. –

ответ

1

Почему результаты отличаются друг от друга, когда единственное, что я меняю, это порядок задач в списке?

Мое предположение, что Test #1 фактически не чтения предметов, так как нет ничего, чтобы читать.Порядок выполнения задачи:

  1. Чтение из разделяемого пула 1M раз и вычислить сумму
  2. Запись в общий бассейн в 100 раз

Ваш Test # 2 смешивает читает и пишет, и именно поэтому, я я думаю, вы получите другой результат.

Есть ли лучший способ изменить мой тест, чтобы сделать его более справедливым?

Прежде чем приступить к выполнению задач, попробуйте рандомизированный порядок задач. Возможно, будет трудно воспроизвести тот же результат, но вы можете приблизиться к реальному использованию в мире.

В конечном счете, ваш вопрос касается разницы оптимистического параллелизма (Concurrent* классов) против пессимистического параллелизма (на основе блокировки). Как правило, при низких шансах одновременного доступа к общему ресурсу предпочитают оптимистичный параллелизм. Когда шансы на одновременный доступ высоки, предпочитают пессимистичную.

+0

Спасибо за ваш вклад! В тесте 1, изменив порядок округления так, чтобы записи были первыми, а чтения стали вторыми, они имеют огромное различие в производительности. ConcurrentBag выходит почти на ~ 9 с, где мой пользовательский ThreadSafeList составляет 1300 мс. Я считаю, что тест 2 - хорошая попытка случайного упорядочения задач? Я считаю, что существует высокая вероятность пессимистического параллелизма, поскольку служба WCF является сервером публикации подписки, где подписки (запись в список) редки, но часто повторяется параллельная широковещательная рассылка (чтение из списка) нескольких сообщений. – user978139

+0

Я бы использовал что-то вроде Fisher-Yates-Durstenfeld shuffle. См. Реализацию [здесь] (http://stackoverflow.com/a/3173893/706456). И когда вы запускаете задачи, это может выглядеть примерно так: 'tasks.Shuffle(). ForEach (task => task.Start());' Похоже, что ваша реализация лучше (но только для вашего конкретного варианта использования) – oleksii