У меня есть единый поток данных, который нужно обрабатывать как можно быстрее. Единый поток содержит данные максимум из 200 источников. Не все источники предоставляют одинаковый объем данных, и скорость может варьироваться.

В качестве первоначальной попытки я решил создать 10 (в зависимости от спецификации сервера, двухъядерный четырехъядерный процессор) долго выполняющихся задач. Каждая задача будет считываться из коллекции блоков. Перед тем как начать, я создал карту, чтобы по мере получения данных во входящем потоке я знал, в какую коллекцию BlockingCollection добавить данные этих источников.

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

Если у меня есть 8 аппаратных потоков и я создал около 10 очередей, а задачи не привязаны к потоку (опять же не уверен, верно ли это с TaskCreationOptions.LongRunning), то даже если одна очередь не занята, другая занятая очередь не может использовать резервный поток, поскольку теоретически я мог бы обработать часть данных не по порядку.

Могу ли я просто создать коллекцию задач и блокировок для каждого источника, тогда TPL сможет наилучшим образом использовать доступные потоки, поскольку данные наиболее разделены?

Другой моей альтернативой было как-то потренироваться на прошлой статистике и различной внешней / человеческой информации, как лучше всего распределить источники среди конечного набора BlockingCollections / Tasks, а затем скорректировать сопоставление с течением времени.

Надеюсь, я достаточно хорошо объяснил свой сценарий.

Я использую класс, который инкапсулирует BlockingCollection и Задача

У меня есть то, что можно визуализировать как более 40 чередующихся потоков, которые при разделении обрабатываются одновременно (при условии, что каждый поток хранится в своей собственной последовательности), но потоков намного больше, чем доступных аппаратных потоков.

РЕДАКТИРОВАТЬ - Попытка уточнить мой запрос

Чтобы попытаться уточнить, что я ищу. В настоящее время я фактически разделяю источники на подгруппы и выделяю каждой группе отдельную очередь. Мой вопрос действительно таков: сколько групп создать? Если у меня 200 источников, должен ли я создать 200 групп (то есть 200 задач и блокирующих коллекций), а затем позволить TPL работать, как сумасшедший, распределяя потоки там, где это возможно, поскольку каждая задача получает время процессора. Или мне лучше выделить одну группу на базовый аппаратный поток?

3
MattC 6 Мар 2012 в 00:04

3 ответа

Лучший ответ

Если вы создали одну Task и одну очередь для каждого источника, это должно работать, пока этот Task заканчивается, когда очередь пуста. Вы будете точно знать, что заказы на товары в одном источнике сохраняются, и у вас должна быть полная загрузка ЦП, когда это необходимо. Но если все потоки в настоящее время обрабатываются, а новые данные поступают из низкочастотного источника, вы можете долго ждать, прежде чем эти данные будут обработаны.

Если это проблема для вас, вы должны сами управлять точным порядком обработки, а не полагаться на Task s. Для этого у вас может быть одна глобальная очередь, а затем локальная очередь для каждого источника. Для поддержания порядка в глобальной очереди или обрабатываемых в данный момент данных может быть не более одного элемента данных. Когда обработка элемента завершена, элемент перемещается из правильной локальной очереди в глобальную очередь, если это возможно. Таким образом, вы получите более справедливый порядок обработки данных.

Код может выглядеть так:

class SourcesManager<T>
{
    private readonly BlockingCollection<Tuple<T, Source<T>>> m_queue =
        new BlockingCollection<Tuple<T, Source<T>>>();

    public Source<T> CreateSource()
    {
        return new Source<T>(m_queue);
    }

    // blocks if no items are available and Complete() hasn't been called
    public bool TryProcess(Action<T> action)
    {
        Tuple<T, Source<T>> tuple;
        if (m_queue.TryTake(out tuple, Timeout.Infinite))
        {
            action(tuple.Item1);
            tuple.Item2.TryDequeue();
            return true;
        }

        return false;
    }

    public void Complete()
    {
        m_queue.CompleteAdding();
    }
}

class Source<T>
{
    private readonly Queue<T> m_localQueue = new Queue<T>();
    private readonly BlockingCollection<Tuple<T, Source<T>>> m_managerQueue;
    private volatile bool m_managerHasData = false;

    internal Source(BlockingCollection<Tuple<T, Source<T>>> managerQueue)
    {
        m_managerQueue = managerQueue;
    }

    public void Enqueue(T data)
    {
        lock (m_localQueue)
        {
            if (!m_managerHasData)
            {
                m_managerQueue.Add(Tuple.Create(data, this));
                m_managerHasData = true;
            }
            else
                m_localQueue.Enqueue(data);
        }
    }

    internal bool TryDequeue()
    {
        lock (m_localQueue)
        {
            if (m_localQueue.Count == 0)
            {
                m_managerHasData = false;
                return false;
            }

            m_managerQueue.Add(Tuple.Create(m_localQueue.Dequeue(), this));
            return true;
        }
    }
}
0
svick 6 Мар 2012 в 16:19

Я бы лично использовал здесь поток данных TPL и просто определил ActionBlock<T>, который представляет вашу работу, и связал бы BufferBlock<T> "перед", чтобы предотвратить перенасыщение различными производителями. Затем все, что вам нужно сделать, это отправить сообщение в BufferBlock<T> из различных источников (производителей) и убедиться, что вы протестировали / настроили параметры блокировки (BoundedCapacity, MaxDegreeOfParallelism, MaxMessagesPerTask и т. д.) соответственно, и пусть TPL Dataflow творит свое чудо. Снимает с рук всю тяжелую работу.

1
Drew Marsh 8 Мар 2012 в 00:14

Я считаю, что подход Pipelines может вам помочь.

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

См. Следующие документы и примеры MSDN:

0
sll 5 Мар 2012 в 20:08