2

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

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

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

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

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

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

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

4

2 回答 2

3

异步套接字 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[<T>]- 我广泛使用它。虽然await在这里可能有用,但它不是必需的。

于 2013-02-25T10:34:39.173 回答
2

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

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

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

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

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

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

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

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

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

您似乎对如何工作asyncawait工作感到有些困惑。我建议按顺序阅读我自己的介绍MSDN 概述基于任务的异步模式指南async常见问题解答

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

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


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

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

于 2013-02-25T17:11:42.663 回答