在IEnumerable实现中使用任务并行库来实现速度提升

时间:2012-01-27 02:39:19

标签: c# multithreading ienumerable task-parallel-library

以下代码是我尝试优化的代码的简化版本。

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()方法的单个调用可能需要大约200ms。它也可以抛出我可以忽略的异常。结果顺序无关紧要。

在我的第一次尝试中,我想出了使用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 个答案:

答案 0 :(得分:4)

如果我正确地阅读了您的问题,那么您并不只是想加快从字词中创建字符的并行处理速度 - 您希望您的枚举能够在每个准备好后立即生成 / em>的。通过您当前的实现(以及我目前看到的其他答案),SplitItOut将等到所有单词都发送到GetCharacters,并且在生成第一个单词之前返回所有结果。< / p>

在这种情况下,我喜欢把事情看作是将我的过程分解为生产者和消费者。您的生产者线程将获取可用的单词并调用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();
        }

您的mainGetCharacters没有变更 - 因为这些代表您的约束(无法更改来电者,无法更改昂贵的操作)

        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. 使用我们希望拆分的所有单词初始化一个concurrentbag。 (旁注:如果你想根据需要枚举单词,你可以启动一个新任务来推送它们而不是在构造函数中执行它)。
  2. 启动我们的char“制作人”任务。您可以开始一组号码,使用工厂,等等。我建议你在衡量之前不要疯狂。
  3. 在完成所有任务后发信号通知我们完成的BlockingCollection
  4. “消耗”所有产生的字符(我们让自己很容易,只返回IEnumerable<char>而不是foreach和yield,但如果你愿意的话,你可以做很长的事情。)
  5. 所有缺少的是我们的生产者实施。我已经扩展了所有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

答案 1 :(得分:2)

我把你的代码放在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);
    }