Использование параллельной библиотеки задач в реализации IEnumerable для повышения скорости - PullRequest
3 голосов
/ 27 января 2012

Следующий код является упрощенной версией кода, который я пытаюсь оптимизировать.

void Main()
{
    var words = new List<string> {"abcd", "wxyz", "1234"};

    foreach (var character in SplitItOut(words))
    {
        Console.WriteLine (character);
    }
}

public IEnumerable<char> SplitItOut(IEnumerable<string> words)
{
    foreach (string word in words)
    {
        var characters = GetCharacters(word);

        foreach (char c in characters)
        {
            yield return c;
        }
    }
}

char[] GetCharacters(string word)
{
    Thread.Sleep(5000);
    return word.ToCharArray();
}

Я не могу изменить сигнатуру метода SplitItOut. Метод GetCharacters дорогой, но он поточно-ориентированный.Входные данные для метода SplitItOut могут содержать более 100 000 записей, а один вызов метода GetCharacters () может занять около 200 мс.Он также может генерировать исключения, которые я могу игнорировать.Порядок результатов не имеет значения.

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

public IEnumerable<char> SplitItOut(IEnumerable<string> words)
{
    Task<char[][]> tasks = Task<char[][]>.Factory.StartNew(() =>
    {
        ConcurrentBag<char[]> taskResults = new ConcurrentBag<char[]>();

        Parallel.ForEach(words,
            word => 
            {
                taskResults.Add(GetCharacters(word));
            });

        return taskResults.ToArray();
    });

    foreach (var wordResult in tasks.Result)
    {
        foreach (var c in wordResult)
        {
            yield return c;
        }
    }
}

Я ищу любую лучшую реализацию метода SplitItOut (), чем эта.Меньшее время обработки - мой приоритет.

Ответы [ 2 ]

4 голосов
/ 27 января 2012

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

В подобных случаях мне нравится думать о вещах как о том, что я делю процесс на производителей и потребителей.Ваш продюсер поток (ов) возьмет доступные слова и вызовет GetCharacters, а затем выдаст результаты куда-нибудь.Потребитель выдаст символы для вызывающего SplitItOut, как только они будут готовы.На самом деле, потребитель является вызывающим абонентом SplitItOut.

. Мы можем использовать BlockingCollection как для получения символов, так и в качестве «где-то» дляпоставить результаты.Мы можем использовать ConcurrentBag в качестве места для размещения слов, которые еще предстоит разделить:

static void Main()
        {
            var words = new List<string> { "abcd", "wxyz", "1234"};

            foreach (var character in SplitItOut(words))
            {
                Console.WriteLine(character);
            }
        }


        static char[] GetCharacters(string word)
        {
            Thread.Sleep(5000);
            return word.ToCharArray();
        }

Без изменений в main или GetCharacters - так как этипредставлять ваши ограничения (не может изменить вызывающего абонента, не может изменить дорогостоящую операцию)

        public static IEnumerable<char> SplitItOut(IEnumerable<string> words)
        {
            var source = new ConcurrentBag<string>(words);
            var chars = new BlockingCollection<char>();

            var tasks = new[]
                   {
                       Task.Factory.StartNew(() => CharProducer(source, chars)),
                       Task.Factory.StartNew(() => CharProducer(source, chars)),
                       //add more, tweak away, or use a factory to create tasks.
                       //measure before you simply add more!
                   };

            Task.Factory.ContinueWhenAll(tasks, t => chars.CompleteAdding());

            return chars.GetConsumingEnumerable();
        }

Здесь мы изменим метод SplitItOut, чтобы сделать четыре вещи:

  1. Initializeпараллельная сумка со всеми словами, которые мы хотим разделить.(примечание: если вы хотите перечислять слова по требованию, вы можете запустить новое задание, чтобы вставить их вместо того, чтобы делать это в конструкторе)
  2. Запустить наши задачи типа «производитель».Вы можете начать набор номера, использовать фабрику, что угодно.Я предлагаю не сходить с ума от заданий до того, как вы измерите.
  3. Сигнализирует BlockingCollection, что мы закончили, когда все задачи завершены.
  4. «Поглотили» все произведенные символы (мы делаемлегко для себя и просто верните IEnumerable<char> вместо foreach и yield, но вы можете сделать это долго, если хотите)

Все, чего не хватает, - это реализации нашего производителя.Я расширил все ярлыки linq, чтобы прояснить его, но он очень прост:

        private static void CharProducer(ConcurrentBag<string> words, BlockingCollection<char> output)
        {
            while(!words.IsEmpty)
            {
                string word;
                if(words.TryTake(out word))
                {
                    foreach (var c in GetCharacters(word))
                    {
                        output.Add(c);
                    }
                }
            }
        }

Это просто

  1. Вынимает слово из ConcurrentBag (если оно не пусто).- если это так, задача выполнена!)
  2. Вызывает дорогой метод
  3. Помещает вывод в BlockingCollection
2 голосов
/ 27 января 2012

Я поместил ваш код через профилировщик, встроенный в Visual Studio, и похоже, что накладные расходы Задачи причиняли вам боль.Я слегка переработал его, чтобы удалить Task, и это немного улучшило производительность.Без вашего фактического алгоритма и набора данных трудно сказать точно, в чем проблема или где производительность может быть улучшена.Если у вас VS Premium или Ultimate, есть встроенные инструменты профилирования, которые вам очень помогут.Вы также можете получить пробную версию ANTS .

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

В любом случае, вот мой рефакторинг вашего алгоритма:

    public static IEnumerable<char> SplitItOut(IEnumerable<string> words)
    {
        var taskResults = new ConcurrentBag<char[]>();

        Parallel.ForEach(words, word => taskResults.Add(GetCharacters(word)));

        return taskResults.SelectMany(wordResult => wordResult);
    }
...