我有一个用例,我需要处理 n 个操作,其中涉及其他 I/O 操作。
方法1:使用
ConcurrentBag<T>
。
但担心ProcessSomething
方法中的I/O操作会随着Task.Run
添加async
而增加进一步的复杂性。
var fullList = new ConcurrentBag<ViewModel>();
var tasks = new List<Task>();
IncomingList.ForEach(w => tasks.Add(Task.Run(async () =>
{
ViewModel somethingCompletewithMetaData= await ProcessSomething(something, token);
if (somethingCompletewithMetaData!= null)
{
fullList.Add(somethingCompletewithMetaData);
}
})));
await Task.WhenAll(tasks);
方法2: 使用投影而不使用
ConcurrentBag<T>
。看起来更干净、更简单。
var tasks1 = IncomingList.Select(x => ProcessSomething(x, token))
.ToList();
ViewModel[] result1 = await Task.WhenAll(tasks1);
IEnumerable<ViewModel> fullWikis = result1.Where(x => x is not null);
方法三: 使用
Parallel.ForEachAsync
ParallelOptions parallelOptions = new()
{
MaxDegreeOfParallelism = 5
};
var fullContents = new ConcurrentBag<SomethingViewModel>();
await Parallel.ForEachAsync(contents, parallelOptions, async (content, token) =>
{
SomethingViewModel wikiComplete = await ProcessSomething(content, token);
if (contentComplete != null)
{
fullContents.Add(contentComplete);
}
});
为了澄清,
ProcessSomething
是一个长时间运行的任务,它进行外部API调用并处理一些数据并返回一个视图模型,该视图模型被添加到列表中。
public Task<ContentViewModel> ProcessSomething(content, token)
{
bool hasPermission = await _externalService.HasPermission(content.userId);
if (hasPermission)
{
var content = await _anotherService.Get(content.id);
var author = await _anotherService2.Get(content.userId);
return new ContentViewModel(content, author)
}
return null;
}
我的问题是,我应该使用哪种方法? 方法 3 是我理想中使用的方法,但在 azure 中运行时需要双倍的处理时间。方法 1 和方法 2 具有可比性。使用
Stopwatch
测量它们。
该应用程序作为 HTTP 触发函数应用程序在 Azure 中运行。
方法 1 和方法 2 之间唯一显着的区别是,第一个将
ProcessSomething
的调用卸载到 ThreadPool
,而第二个则不然。这是因为第一个使用了 Task.Run
而第二个则没有。是否使用 LINQ 来创建任务,以及是否使用并发集合来收集结果,这只是风格上的问题,不会产生任何行为效果。您也可以将 Task.Run
与 LINQ 一起使用:
Task<ViewModel>[] tasks = IncomingList
.Select(x => Task.Run(() => ProcessSomething(x, token)))
.ToArray();
ViewModel[] results = await Task.WhenAll(tasks);
仅当
Task.Run
不立即返回 ProcessSomething
时,添加 Task
才会产生可观察到的效果。换句话说,如果它在返回 Task
之前执行同步工作(CPU 密集型工作或阻塞 I/O 工作)。从您在问题中显示的内容来看, ProcessSomething
最有可能立即返回 Task<ContentViewModel>
,因此添加 Task.Run
应该不会产生太大影响。 Task.Run
本质上是并行任务的创建。如果任务的创建是瞬时的,则并行化不会提高整体性能。您无法优化需要零时间完成的操作的速度。第三种方法 () 是一般情况下并行化异步工作的推荐方法(ASP.NET 是一个可能的例外)。它比
Task.WhenAll
更可取,因为(主要)有两个原因:允许控制最大并行度,并在出现错误时尽快停止执行。控制最大并行度是一件好事,因为您不想用数百或数千个 Web 请求轰炸远程服务器。如果这样做,远程服务器的性能可能会降低,或者可能会认为您是 DOS 攻击者并阻止您的 IP。意识到远程计算机所承受的压力是让您成为负责任的专业人士的一部分。 如果您确定远程服务器(或任何支持您正在调用的 API 的硬件)可以处理无限并行性,您只需相应地配置
Parallel.ForEachAsync
:
ParallelOptions parallelOptions = new()
{
MaxDegreeOfParallelism = Int32.MaxValue
};
ParallelOptions
还有一个
CancellationToken
属性,您可能也想配置它。