异步/等待和任务

时间:2013-02-25 10:17:08

标签: c# sockets async-await

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

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

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

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

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

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

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

2 个答案:

答案 0 :(得分: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;
    }
}

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

另请注意,原始异步套接字API在没有Task[<T>]的情况下完全可用 - 我广泛使用它。虽然await可能在这里使用,但它并不重要。

答案 1 :(得分: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 introductionMSDN overviewTask-Based Asynchronous Pattern guidanceasync FAQ

  

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

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


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

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