C # Параллельно Vs.Потоковая производительность кода - PullRequest
10 голосов
/ 26 августа 2010

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

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

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();

Обновление:

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

путь = 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 ядра.

Ответы [ 4 ]

3 голосов
/ 26 августа 2010

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

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

3 голосов
/ 26 августа 2010

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

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

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

[Редактировать - больше исследований]

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

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

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

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

0 голосов
/ 26 августа 2010

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

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

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

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

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

0 голосов
/ 26 августа 2010

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


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

...