С# Параллельный Vs. Производительность резьбового кода

Я тестировал производительность System.Threading.Parallel и Threading, и я удивлен, увидев, что Parallel занимает больше времени для завершения задач, чем потоки. Я уверен, что это связано с моим ограниченным знанием Parallel, которое я только начал читать.

Я думал, что поделюсь несколькими фрагментами, и если кто-нибудь может указать мне, что код paralle работает медленнее vs-кода. Также попытался выполнить такое же сравнение для нахождения простых чисел и найти параллельную обработку кода намного позже, чем код с резьбой.

public class ThreadFactory
{
    int workersCount;
    private List<Thread> threads = new List<Thread>();

    public ThreadFactory(int threadCount, int workCount, Action<int, int, string> action)
    {
        workersCount = threadCount;

        int totalWorkLoad = workCount;
        int workLoad = totalWorkLoad / workersCount;
        int extraLoad = totalWorkLoad % workersCount;

        for (int i = 0; i < workersCount; i++)
        {
            int min, max;
            if (i < (workersCount - 1))
            {
                min = (i * workLoad);
                max = ((i * workLoad) + workLoad - 1);
            }
            else
            {
                min = (i * workLoad);
                max = (i * workLoad) + (workLoad - 1 + extraLoad);
            }
            string name = "Working Thread#" + i; 

            Thread worker = new Thread(() => { action(min, max, name); });
            worker.Name = name;
            threads.Add(worker);
        }
    }

    public void StartWorking()
    {
        foreach (Thread thread in threads)
        {
            thread.Start();
        }

        foreach (Thread thread in threads)
        {
            thread.Join();
        }
    }
}

Вот программа:

Stopwatch watch = new Stopwatch();
watch.Start();
int path = 1;

List<int> numbers = new List<int>(Enumerable.Range(0, 10000));

if (path == 1)
{
    Parallel.ForEach(numbers, x =>
    {
        Console.WriteLine(x);
        Thread.Sleep(1);

    });
}
else
{
    ThreadFactory workers = new ThreadFactory(10, numbers.Count, (min, max, text) => {

        for (int i = min; i <= max; i++)
        {
            Console.WriteLine(numbers[i]);
            Thread.Sleep(1);
        }
    });

    workers.StartWorking();
}

watch.Stop();
Console.WriteLine(watch.Elapsed.TotalSeconds.ToString());

Console.ReadLine();

Update:

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

path = 1;       cieling = 10000000;

    List<int> numbers = new List<int>();

    if (path == 1)
    {
        Parallel.For(0, cieling, x =>
        {
            lock (numbers)
            {
                numbers.Add(x);    
            }

        });
    }

    else
    {
        ThreadFactory workers = new ThreadFactory(10, cieling, (min, max, text) =>
        {

            for (int i = min; i <= max; i++)
            {
                lock (numbers)
                {
                    numbers.Add(i);    
                }                       

            }
        });

        workers.StartWorking();
    }

Обновление 2: Просто быстрое обновление, что моя машина имеет четырехъядерный процессор. Таким образом, Parallel имеет 4 ядра.

Ответ 1

Ссылка на сообщение в блоге от Рида Копси-младшего:

Parallel.ForEach немного сложнее. При работе с общим IEnumerable количество элементов, необходимых для обработки, заранее неизвестно и должно быть обнаружено во время выполнения. Кроме того, поскольку у нас нет прямого доступа к каждому элементу, планировщик должен перечислить коллекцию для ее обработки. Поскольку IEnumerable не является потокобезопасным, он должен блокировать элементы по мере их перечисления, создавать временные коллекции для каждого обрабатываемого блока и планировать его.

Блокировка и копирование могут привести к тому, что Parallel.ForEach займет больше времени. Кроме того, разбиение на разделы и планировщик ForEach могут повлиять и дать накладные расходы. Я проверил ваш код и увеличил сон каждой задачи, а затем результаты были ближе, но все же ForEach работает медленнее.

[Изменить - больше исследований]

Я добавил следующие циклы выполнения:

if (Thread.CurrentThread.ManagedThreadId > maxThreadId)
   maxThreadId = Thread.CurrentThread.ManagedThreadId;

То, что это показывает на моей машине, заключается в том, что он использует 10 потоков меньше с ForEach, по сравнению с другим с текущими настройками. Если вам нужно больше потоков из ForEach, вам придется играть с ParallelOptions и Планировщиком.

См. Ограничивает ли Parallel.ForEach количество активных потоков?

Ответ 2

Я думаю, что могу ответить на ваш вопрос. Прежде всего, вы не написали, сколько ядер у вашей системы. если вы используете двухъядерный процессор, только 4 потока будут работать с использованием Parallel.For пока вы работаете с 10 потоками в вашем примере Thread. Больше потоков будет работать лучше, так как задача, которую вы выполняете (Printing + Short sleep), очень короткая задача для многопоточности, а издержки потока очень велики по сравнению с задачей, я почти уверен, что если вы напишите тот же код без потоков это будет работать быстрее.

Оба ваших метода работают примерно одинаково, но если вы заранее создаете все потоки, вы экономите много, так как Parallel.For использует пул задач, который добавляет некоторые накладные расходы на перемещение.

Ответ 3

Сравнение не очень справедливо в отношении Threading.Parallel. Вы сообщаете своему пользовательскому пулу потоков, что для этого потребуется 10 потоков. Threading.Parallel не знает, сколько потоков потребуется, поэтому он пытается адаптироваться во время выполнения с учетом таких вещей, как текущая загрузка процессора и другие вещи. Так как количество итераций в тесте достаточно мало, вы можете это количество штрафов за адаптацию потоков. Предоставление того же намека на Threading.Parallel заставит его работать намного быстрее:


int workerThreads;
int completionPortThreads;
ThreadPool.GetMinThreads(out workerThreads, out completionPortThreads);
ThreadPool.SetMinThreads(10, completionPortThreads);

Ответ 4

Это логично: -)

Это будет первый случай в истории, когда добавление одного (или двух) уровней кода улучшит производительность. Когда вы используете удобные библиотеки, вы должны заплатить цену. Кстати, вы не разместили номера. Опубликован результаты: -)

Чтобы сделать вещи немного более неудачными (или предвзятыми:-) для Parallel-s, преобразуйте список в массив.

Затем, чтобы сделать их совершенно несправедливыми, разделите работу самостоятельно, сделайте массив из 10 элементов и полностью загрузите ложные действия в Parallel. Вы, конечно же, выполняете работу, которую Parallel-s обещал сделать для вас на этом этапе, но она должна быть интересным номером: -)

Кстати, я только что прочитал этот блог Рида. Разделение, используемое в этом вопросе, - это то, что он называет самым простым и наивным разбиением. Это действительно очень хороший тест на удаление. Вам все равно нужно проверить нулевой рабочий случай, чтобы узнать, полностью ли он был закрыт.