[英]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();
}
没有更改您的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
方法以执行以下四项操作:
BlockingCollection
信号表示我们在所有任务完成后完成了。 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);
}
}
}
}
这很简单
我把你的代码放在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.