可以在ForEachAsync中使用Task.Run吗?

时间:2019-03-17 00:28:39

标签: .net multithreading parallel-processing async-await

我们正在使用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中。

1 个答案:

答案 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,因为所有消息都是在后台处理的。