异步/等待和任务

Posted

技术标签:

【中文标题】异步/等待和任务【英文标题】:Async/Await and Tasks 【发布时间】:2013-02-10 11:02:03 【问题描述】:

好的,我想我已经理解了整个 async/await 的事情。每当您等待某事时,您正在运行的函数都会返回,从而允许当前线程在异步函数完成时执行其他操作。优点是不用开新线程。

这并不难理解,因为它有点像 Node.JS 的工作原理,除了 Node 使用大量回调来实现这一点。然而,这是我无法理解优势的地方。

socket 类目前没有任何 Async 方法(与 async/await 一起使用)。我当然可以将一个套接字传递给流类,并在那里使用异步方法,但是这会给接受新套接字留下一个问题。

据我所知,有两种方法可以做到这一点。在这两种情况下,我都在主线程的无限循环中接受新套接字。在第一种情况下,我可以为我接受的每个套接字启动一个新任务,并在该任务中运行 stream.ReceiveAsync。但是,await 不会真正阻止该任务,因为该任务将无事可做?这又会导致在线程池上产生更多线程,这又不比在任务中使用同步方法更好?

我的第二个选择是将所有接受的套接字放在几个列表之一中(每个线程一个列表),并在这些线程内运行一个循环,为每个套接字运行 await stream.ReceiveAsync。这样,每当我遇到 await,stream.ReceiveAsync 并开始从所有其他套接字接收。

我想我真正的问题是这是否比线程池更有效,在第一种情况下,它是否真的比仅使用 APM 方法更糟糕。

我也知道您可以使用 await/async 将 APM 方法包装到函数中,但在我看来,您仍然会得到 APM 方法的“缺点”,即 async/await 中状态机的额外开销。

【问题讨论】:

tl;dr... 你有任何编码问题吗? 查看blogs.msdn.com/b/pfxteam/archive/2011/12/15/10248293.aspx 一个可重用方法的示例,该方法可以异步用于以有效方式等待套接字操作。 我应该补充一点,基于任务的异步模式是 MS 推荐的新开发模式。 msdn.microsoft.com/en-us/library/vstudio/hh873175.aspx @svick:啊——我还没有检查签名。太可怕了:( 【参考方案1】:

异步套接字 API 不是基于 Task[<T>],因此它不能直接从 async/await 使用 - 但您可以相当容易地桥接 - 例如(完全未经测试):

public class AsyncSocketWrapper : IDisposable

    public void Dispose()
    
        var tmp = socket;
        socket = null;
        if(tmp != null) tmp.Dispose();
    
    public AsyncSocketWrapper(Socket socket)
    
        this.socket = socket;
        args = new SocketAsyncEventArgs();
        args.Completed += args_Completed;
    

    void args_Completed(object sender, SocketAsyncEventArgs e)
    
        // might want to switch on e.LastOperation
        var source = (TaskCompletionSource<int>)e.UserToken;
        if (ShouldSetResult(source, args)) source.TrySetResult(args.BytesTransferred);
    
    private Socket socket;
    private readonly SocketAsyncEventArgs args;
    public Task<int> ReceiveAsync(byte[] buffer, int offset, int count)
    

        TaskCompletionSource<int> source = new TaskCompletionSource<int>();
        try
        
            args.SetBuffer(buffer, offset, count);
            args.UserToken = source;
            if (!socket.ReceiveAsync(args))
            
                if (ShouldSetResult(source, args))
                
                    return Task.FromResult(args.BytesTransferred);
                
            
        
        catch (Exception ex)
        
            source.TrySetException(ex);
        
        return source.Task;
    
    static bool ShouldSetResult<T>(TaskCompletionSource<T> source, SocketAsyncEventArgs args)
    
        if (args.SocketError == SocketError.Success) return true;
        var ex = new InvalidOperationException(args.SocketError.ToString());
        source.TrySetException(ex);
        return false;
    

注意:您可能应该避免在循环中运行接收 - 我建议让每个套接字在接收数据时负责自己泵送。唯一需要循环的就是定期扫描僵尸,因为并非所有套接字死亡都可以检测到。

还要注意,原始异步套接字 API 在没有 Task[&lt;T&gt;] 的情况下完全可用 - 我广泛使用它。虽然await 在这里可能有用,但不是必需的。

【讨论】:

【参考方案2】:

这并不难理解,因为它在某种程度上是 Node.JS 的工作原理,除了 Node 使用大量回调来实现这一点。然而,这是我无法理解优势的地方。

Node.js 确实使用回调,但它还有另一个重要方面真正简化了这些回调:它们都被序列化到同一个线程。因此,当您查看 .NET 中的异步回调时,您通常会处理多线程以及异步编程(EAP-style callbacks 除外)。

使用回调的异步编程称为“连续传递样式”(CPS)。它是 Node.js 的唯一真正选择,但也是 .NET 上的众多选择之一。特别是,CPS 代码可能会变得极其复杂且难以维护,因此引入了 async/await 编译器转换,这样您就可以编写“看起来很正常”的代码,编译器会为您将其转换为 CPS。

在这两种情况下,我都会在主线程的无限循环中接受新的套接字。

如果您正在编写服务器,那么是的,您将在某个地方反复接受新的客户端连接。此外,您应该不断地从每个连接的套接字读取数据,因此每个套接字也有一个循环。

在第一种情况下,我可以为我接受的每个套接字启动一个新任务,并在该任务中运行 stream.ReceiveAsync。

您不需要新任务。这就是异步编程的全部意义所在。

我的第二个选择是将所有接受的套接字放入多个列表之一(每个线程一个列表),并在这些线程内运行一个循环,为每个套接字运行 await stream.ReceiveAsync。

我不确定您为什么需要多个线程,或者根本不需要任何专用线程。

您似乎对asyncawait 的工作方式有些困惑。我建议按顺序阅读my own introduction、MSDN overview、Task-Based Asynchronous Pattern guidance 和async FAQ。

我也知道您可以使用 await/async 将 APM 方法包装到函数中,但在我看来,您仍然会得到 APM 方法的“缺点”,即 async/await 中状态机的额外开销。

我不确定你指的是什么缺点。状态机的开销虽然不为零,但在套接字 I/O 面前可以忽略不计。


如果您希望进行套接字 I/O,您有多种选择。对于读取,您可以使用 APM 或 Task 围绕 APM 或 Async 方法的包装器在“无限”循环中执行它们。或者,您可以使用 Rx 或 TPL Dataflow 将它们转换为类似流的抽象。

另一个选择是我几年前写的一个库,叫做Nito.Async。它提供了 EAP 样式(基于事件)的套接字,可以为您处理所有线程封送处理,因此您最终会得到像 Node.js 这样更简单的东西。当然,就像 Node.js 一样,这种简单性意味着它不会像更复杂的解决方案那样扩展。

【讨论】:

我在第一个示例中在任务中运行的原因是,据我所知,没有办法等待 socket.Accept() 调用...这意味着我会运行一个带有我所有套接字的 ReadAsync 循环,然后等待下一个套接字连接......所以我可以在他们自己的读取循环任务中运行所有套接字......或者我可以将所有套接字放在一个列表中(每个套接字一个线程(如果有线程)和单独线程中的列表(用于性能)(如果有线程)。我关于包装 APM 方法的观点是,我没有摆脱 APM 的任何缺点,而是增加了状态机的开销...... Accept 可以像任何其他操作一样包装成Task;换行 BeginAccept/EndAcceptAcceptAsync

以上是关于异步/等待和任务的主要内容,如果未能解决你的问题,请参考以下文章

异步/等待上传任务

使用 SendGrid 了解异步任务和等待 [重复]

C#异步编程概念和使用

运行多个异步任务并等待它们全部完成

在启动 Angular JS 之前等待异步任务完成

Spring Boot - 异步任务