Как я могу наиболее эффективно использовать несколько ядер для коротких вычислений в .NET?

Вот контекст: я пишу интерпретатор в С# для небольшого языка программирования под названием Heron и имеет некоторые примитивные операции с списком которые могут выполняться параллельно.

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

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

Итак, у меня есть следующие основные вопросы:

  • Каков наиболее эффективный способ распространения работы по потокам, чтобы я мог гарантировать, что компьютер будет распределять работу по двум ядрам?

Меня также интересует связанный с ним вопрос:

  • Примерно, как долго должна выполняться операция, прежде чем мы сможем начать преодоление накладных расходов на разделение работы на другой поток?

Ответ 1

Если вы хотите много работать с параллельными операциями, вы захотите начать с .Net 4.0. Здесь Параллельное программирование для документации .Net. Вы захотите начать здесь, хотя..Net 4.0 добавляет LOT с точки зрения многоядерного использования. Вот краткий пример:

Current 3.5 Последовательный метод:

for(int i = 0; i < 30000; i++)
{
  doSomething(i);
}

Новый .Net 4.0 Параллельный метод:

Parallel.For(0, 30000, (i) => doSomething(i));

Метод Parallel.For автоматически масштабируется по количеству доступных ядер, вы можете увидеть, как быстро вы могли бы воспользоваться этим. В инфраструктуре есть десятки новых библиотек, поддерживающих полное управление потоками/задачами, например, ваш пример (включая все каналы для синхронизации, отмены и т.д.).

Существуют библиотеки для Parallel LINQ (PLINQ), Задача Фабрики, Планировщики задач и несколько других. Короче говоря, для конкретной задачи, которую вы изложили .Net 4.0 имеет огромные преимущества для вас, и я пойти вперед и захватить бесплатную бета-версию 2 (RC в ближайшее время) и начните. (Нет, я не работаю для Microsoft... но редко вижу, что предстоящий выпуск так хорошо подходит, поэтому я настоятельно рекомендую .Net 4.0 для вас)

Ответ 2

Потому что я не хотел развиваться с использованием VS 2010, и я обнаружил, что ThreadPool не имеет оптимальной производительности для распределения работы по ядрам (я думаю, потому что он запустил или остановил слишком много потоков). Я закончил своя. Надеюсь, что другие считают это полезным:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace HeronEngine
{
    /// <summary>
    /// Represents a work item.
    /// </summary>
    public delegate void Task();

    /// <summary>
    /// This class is intended to efficiently distribute work 
    /// across the number of cores. 
    /// </summary>
    public static class Parallelizer 
    {
        /// <summary>
        /// List of tasks that haven't been yet acquired by a thread 
        /// </summary>
        static List<Task> allTasks = new List<Task>();

        /// <summary>
        /// List of threads. Should be one per core. 
        /// </summary>
        static List<Thread> threads = new List<Thread>();

        /// <summary>
        /// When set signals that there is more work to be done
        /// </summary>
        static ManualResetEvent signal = new ManualResetEvent(false);

        /// <summary>
        /// Used to tell threads to stop working.
        /// </summary>
        static bool shuttingDown = false;

        /// <summary>
        /// Creates a number of high-priority threads for performing 
        /// work. The hope is that the OS will assign each thread to 
        /// a separate core.
        /// </summary>
        /// <param name="cores"></param>
        public static void Initialize(int cores)
        {
            for (int i = 0; i < cores; ++i)
            {
                Thread t = new Thread(ThreadMain);
                // This system is not designed to play well with others
                t.Priority = ThreadPriority.Highest;
                threads.Add(t);
                t.Start();
            }
        }

        /// <summary>
        /// Indicates to all threads that there is work
        /// to be done.
        /// </summary>
        public static void ReleaseThreads()
        {
            signal.Set();
        }

        /// <summary>
        /// Used to indicate that there is no more work 
        /// to be done, by unsetting the signal. Note: 
        /// will not work if shutting down.
        /// </summary>
        public static void BlockThreads()
        {
            if (!shuttingDown)
                signal.Reset();
        }

        /// <summary>
        /// Returns any tasks queued up to perform, 
        /// or NULL if there is no work. It will reset
        /// the global signal effectively blocking all threads
        /// if there is no more work to be done.
        /// </summary>
        /// <returns></returns>
        public static Task GetTask()
        {
            lock (allTasks)
            {
                if (allTasks.Count == 0)
                {
                    BlockThreads();
                    return null;
                }
                Task t = allTasks.Peek();
                allTasks.Pop();
                return t;
            }
        }

        /// <summary>
        /// Primary function for each thread
        /// </summary>
        public static void ThreadMain()
        {
            while (!shuttingDown)
            {
                // Wait until work is available
                signal.WaitOne();

                // Get an available task
                Task task = GetTask();

                // Note a task might still be null becaue
                // another thread might have gotten to it first
                while (task != null)
                {
                    // Do the work
                    task();

                    // Get the next task
                    task = GetTask();
                }
            }
        }

        /// <summary>
        /// Distributes work across a number of threads equivalent to the number 
        /// of cores. All tasks will be run on the available cores. 
        /// </summary>
        /// <param name="localTasks"></param>
        public static void DistributeWork(List<Task> localTasks)
        {
            // Create a list of handles indicating what the main thread should wait for
            WaitHandle[] handles = new WaitHandle[localTasks.Count];

            lock (allTasks)
            {
                // Iterate over the list of localTasks, creating a new task that 
                // will signal when it is done.
                for (int i = 0; i < localTasks.Count; ++i)
                {
                    Task t = localTasks[i];

                    // Create an event used to signal that the task is complete
                    ManualResetEvent e = new ManualResetEvent(false);

                    // Create a new signaling task and add it to the list
                    Task signalingTask = () => { t(); e.Set(); };
                    allTasks.Add(signalingTask);

                    // Set the corresponding wait handler 
                    handles[i] = e;
                }
            }

            // Signal to waiting threads that there is work
            ReleaseThreads();

            // Wait until all of the designated work items are completed.
            Semaphore.WaitAll(handles);
        }

        /// <summary>
        /// Indicate to the system that the threads should terminate
        /// and unblock them.
        /// </summary>
        public static void CleanUp()
        {
            shuttingDown = true;
            ReleaseThreads();
        }
    }    
}

Ответ 3

Я бы пошел с пулом потоков, хотя у него есть свои проблемы, MS инвестирует в его улучшение, и, похоже,.NET 4 будет иметь улучшенный. На данный момент я считаю, что лучше всего использовать пул потоков, завернутый в ваш собственный объект, и подождать, решив вашу собственную реализацию