异步/等待和任务
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[<T>]
的情况下完全可用 - 我广泛使用它。虽然await
在这里可能有用,但不是必需的。
【讨论】:
【参考方案2】:这并不难理解,因为它在某种程度上是 Node.JS 的工作原理,除了 Node 使用大量回调来实现这一点。然而,这是我无法理解优势的地方。
Node.js 确实使用回调,但它还有另一个重要方面真正简化了这些回调:它们都被序列化到同一个线程。因此,当您查看 .NET 中的异步回调时,您通常会处理多线程以及异步编程(EAP-style callbacks 除外)。
使用回调的异步编程称为“连续传递样式”(CPS)。它是 Node.js 的唯一真正选择,但也是 .NET 上的众多选择之一。特别是,CPS 代码可能会变得极其复杂且难以维护,因此引入了 async
/await
编译器转换,这样您就可以编写“看起来很正常”的代码,编译器会为您将其转换为 CPS。
在这两种情况下,我都会在主线程的无限循环中接受新的套接字。
如果您正在编写服务器,那么是的,您将在某个地方反复接受新的客户端连接。此外,您应该不断地从每个连接的套接字读取数据,因此每个套接字也有一个循环。
在第一种情况下,我可以为我接受的每个套接字启动一个新任务,并在该任务中运行 stream.ReceiveAsync。
您不需要新任务。这就是异步编程的全部意义所在。
我的第二个选择是将所有接受的套接字放入多个列表之一(每个线程一个列表),并在这些线程内运行一个循环,为每个套接字运行 await stream.ReceiveAsync。
我不确定您为什么需要多个线程,或者根本不需要任何专用线程。
您似乎对async
和await
的工作方式有些困惑。我建议按顺序阅读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
/EndAccept
或 AcceptAsync
。以上是关于异步/等待和任务的主要内容,如果未能解决你的问题,请参考以下文章