我们正在使用Nesting await in Parallel.ForEach中的ForEachAsync方法,该方法最初是suggested by Stephen Toub(在其Blob的底部)。
public static async Task ForEachAsync<T>(
this IEnumerable<T> source, int degreeOfParallelism, Func<T, Task> body, Action<Task> handleException = null)
{
if (source.Any())
{
await Task.WhenAll(
from partition in Partitioner.Create(source).GetPartitions(degreeOfParallelism)
select Task.Run(async delegate
{
using (partition)
while (partition.MoveNext())
await body(partition.Current).ContinueWith(t =>
{
//observe exceptions
if (t.IsFaulted)
{
handleException?.Invoke(t);
}
});
}));
}
}
但是我们的一位同事对Task.Run开销感到担忧,这在Stephen Cleary系列文章中有描述 https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-even-in.html
在ASP.NET中与Task.Run一起使用时,至少会引入四个效率问题:
•额外的(不必要的)线程 切换到Task.Run线程池线程。同样,当 线程完成请求,它必须进入请求上下文 (这不是实际的线程开关,但确实有开销)。
•创建了多余的(不必要的)垃圾。异步编程是一个 权衡:您以更高的代价牺牲了响应能力 内存使用情况。在这种情况下,您最终会为 完全不需要的异步操作。 •ASP.NET Task.Run“意外地”抛出线程池启发式方法 借用线程池线程。我在这里没有很多经验, 但是我的直觉告诉我启发式方法应该可以很好地恢复 如果意外的任务真的很短,并且不会将其处理为 如果意外任务持续两秒以上,则非常优雅。
•ASP.NET无法提前终止请求,即,如果 客户端断开连接或请求超时。在同步情况下, ASP.NET知道请求线程并可以中止它。在里面 异步情况下,ASP.NET不知道其他线程池 线程是“针对”该请求的。有可能通过使用 取消令牌,但这不在本博客文章的范围内。
我的问题是可以将Task.Run用于ForEachAsync吗,还是存在一种更好的方法来与受控dog(并行度)并行运行多个异步任务? 例如,我要处理400个项目,包子并行运行不超过100个项目。
我们在.Net和.Net Core环境中都使用ForEachAsync方法,因此,如果不同环境的答案不同,那么我将很高兴知道这两种情况。
更新以阐明我们正在使用的技术:
我们有Windows服务/控制台(用.Net4.6.1编写),可以从数据库读取数千条记录,然后将它们并行并行地发布(例如dop = 100)到Web api服务(我们考虑成批发送,但是还没有)尚未实施)。
我们还拥有带有后台托管服务的Asp.Net Core服务,该服务定期(例如每10秒钟)拉出项目页面(例如最多400个),然后并行(例如dop = 100)将它们保存到单个Azure Blob中。
答案 0 :(得分:1)
以异步方式处理MDOP为100的400条消息的一种简单方法是使用ActionBlock<T>
。这样的事情会起作用:
.file "timestamp_shell.c"
.text
.section .rodata
.align 8
.LC0:
.string "%8d; Start %10u; Stop %10u; Difference %5d\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
pushq %r13
pushq %r12
pushq %rbx
subq $8, %rsp
.cfi_offset 13, -24
.cfi_offset 12, -32
.cfi_offset 3, -40
movl $100, %r12d
movl $200, %r13d
movl $-1, %r8d
movl $0, %r8d
jmp .L2
.L3:
mov $0, %eax
cpuid
rdtsc
movl %eax, %r12d
movl $0, %eax
# I use a perl script to copy the lines marked with #@ until there
# is the desired number of instructions between the calls to rdstc
#@ addl $1, %eax
#@ addl $1, %r10d
#@ addl $1, %ecx
rdtsc
subl %r12d, %eax
movl %eax, %r8d
movl %r13d, %ecx
movl %r12d, %edx
movl %r8d, %esi
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
addl $1, %r8d
.L2:
cmpl $999999, %r8d
jle .L3
movl $199, %eax
addq $8, %rsp
popq %rbx
popq %r12
popq %r13
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 8.2.1 20181127"
.section .note.GNU-stack,"",@progbits
public class ActionBlockExample
{
private ActionBlock<int> actionBlock;
public ActionBlockExample()
{
actionBlock = new ActionBlock<int>(x => ProcessMsg(x), new ExecutionDataflowBlockOptions()
{
MaxDegreeOfParallelism = 100
});
}
public async Task Process()
{
foreach (var msg in Enumerable.Range(0, 400))
{
await actionBlock.SendAsync(msg);
}
actionBlock.Complete();
await actionBlock.Completion;
}
private Task ProcessMsg(int msg) => Task.Delay(100);
}
默认情况下具有未绑定的输入缓冲区,它将接收所有400条消息,最多并行处理100条消息。这里不需要ActionBlock
,因为所有消息都是在后台处理的。