繁体   English   中英

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

[英]Using task parallel library in IEnumerable implementation to achieve speed improvement

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

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()比这更好的实现。 较低的处理时间是我的首要任务。

如果我正确地阅读你的问题,那么你并不只是想加速从单词中创建字符的并行处理 - 你希望你的可枚举在它准备好后立即生成每个字符。 通过您当前的实现(以及我目前看到的其他答案), 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();
        }

没有更改您的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,但如果你愿意,你可以做很长的事情)

所有缺少的是我们的生产者实施。 我已经扩展了所有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中

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM