AsParallel() 或 async/await

AsParallel() or async/await

让我们假设做一个方法CountString,给定一个字符串数组和一个 int,returns 长度大于该 int 的字符串的数量。 如果我必须尽可能利用多核硬件,这样做是否足够:

public int CountString(string[] s, int i) 
{
  return s.AsParallel().Count( res => res.length > i);
}

或者我必须以某种方式使用 Tasks,甚至混合使用 task 和 PLinq?

只能考虑一个简单的例子,我知道这种方法对硬件性能影响不大。

我想知道这样做是否更好,使用 AsParallel(),还是声明方法 async 并使用 await 更好(即使我不知道如何)在方法体中。

编辑:

我看到你的实际问题有点误导,我会尝试回答有意的问题。具体来说,使用 AsParallel 将是一个很好的方法,因为实际上您不需要 await 任何东西。由于您正在处理一个集合,PLINQ 或 Paralle.ForEach 是一个不错的选择。当您具有自然的异步 I/O 绑定操作时,请考虑使用 async-await。建议不要wrap synchronous methods with async wrappers.


如果您实际测试您的代码,您甚至可能会惊讶地发现并行这段代码实际上会对您的方法执行产生 负面 性能成本,具体取决于 你正在迭代的数组的大小

很多时候人们忘记了使用线程实际上是有开销的,即使使用线程池之外的线程也是如此。您必须进行最少的 CPU 密集型工作,才能使并行化的性能受到影响。

如果您的数组足够长,那么使用 AsParallel 应该就足够了。没有理由添加 Task,因为 PLINQ 会很好地处理并行化。

好的,让我们实际测试这段代码。我将迭代充满 GUID 的 string[]。这是代码:

主要方法:

void Main()
{
    //JIT
    Test(0);

    Test(100);
    Test(1000);
    Test(10000);
    Test(1000000);
    Test(10000000);
}

public void Test(int itemAmount)
{
    string[] strings = Enumerable.Range(0, itemAmount).Select(i => Guid.NewGuid()
                                                      .ToString()).ToArray();

    var stopWatch = Stopwatch.StartNew();
    CountStringInParallel(strings, itemAmount);
    stopWatch.Stop();
    Console.WriteLine("Parallel Call: String amount: {0}, Time: {1}", 
                                                        itemAmount, stopWatch.Elapsed);
    
    stopWatch.Restart();
    CountStringSync(strings, itemAmount);
    stopWatch.Stop();
    Console.WriteLine("Synchronous Call: String amount: {0}, Time: {1}", 
                                                        itemAmount, stopWatch.Elapsed);
}

并行和同步:

public int CountStringInParallel(string[] s, int i) 
{
    return s.AsParallel().Count( res => res.Length > i);
}

public int CountStringSync(string[] s, int i) 
{
    return s.Count(res => res.Length > i);
}

结果:

Parallel Call: String amount: 100, Time: 00:00:00.0000197

Synchronous Call: String amount: 100, Time: 00:00:00.0000026


Parallel Call: String amount: 1000, Time: 00:00:00.0000266

Synchronous Call: String amount: 1000, Time: 00:00:00.0000201


Parallel Call: String amount: 10000, Time: 00:00:00.0002060

Synchronous Call: String amount: 10000, Time: 00:00:00.0002003


Parallel Call: String amount: 1000000, Time: 00:00:00.0080492

Synchronous Call: String amount: 1000000, Time: 00:00:00.0135279


Parallel Call: String amount: 10000000, Time: 00:00:00.0744104

Synchronous Call: String amount: 10000000, Time: 00:00:00.1402474

你可以看到最多10,000个字符串,同步方法实际上比并行方法更快。

只要不用async-await AsParallel就够了。没有理由直接使用任务,因为 AsParallel 会在后台为您完成。

重要的是要记住,并行性会产生开销,在您的情况下,这可能会大于并行性的附加值。要真正提高性能的并行性,您应该处理许多项目,并且工作本身应该很重要。

但是,如果您这样做,则需要使用 async-await AsParallel(以及 PLinq 的其余部分)不适合,因为它早于 TAP。您需要使用 Task.Run 来并行化处理,并使用 Task.WhenAllawait 来并行化处理。类似这样:

var tasks = items.Select(item => Task.Run(() => Process(item));
var results = await Task.WhenAll(tasks);
// process results