如何将此代码转换为 async await?

5

我有很多这样的代码:

        var feed = new DataFeed(host, port);
        feed.OnConnected += (conn) =>
        {
            feed.BeginLogin(user, pass);
        };
        feed.OnReady += (f) =>
        {
             //Now I'm ready to do stuff.
        };

        feed.BeginConnect();

如您所见,我使用了常规的异步操作方式。我该如何修改此代码以使用async await呢?最好是像这样:

public async void InitConnection()
{
    await feed.BeginConnect();
    await feed.BeginLogin(user, pass);

    //Now I'm ready
}

BeginXXX 操作是否会返回 IAsyncResult? - Sergey Berezovskiy
例如,BeginConnect 返回 void。在内部,它调用 Socket.BeginConnect 并等待调用 Socket.EndConnect。一旦调用了 EndConnect,它就会触发 OnConnected 事件。 - nakiya
2
这有点违背它异步的目的,不是吗?那跟直接调用“Connect”有什么区别呢? - Luaan
@Luaan:假设它确实返回了IAsyncResult - nakiya
如果是这样,您可以使用 Task.FromAsync - Luaan
@nakiya,请查看"将事件转换为任务的可重用模式" - noseratio - open to work
2个回答

10

您可以使用TaskCompletionSource<T>将EAP (基于事件的异步模式) 封装为Tasks。由于不清楚您如何在DataFeed类中处理错误和取消操作,因此需要修改此代码并添加错误处理 (示例):

private Task ConnectAsync(DataFeed feed)
{
    var tcs = new TaskCompletionSource<object>();
    feed.OnConnected += _ => tcs.TrySetResult(null);
    feed.BeginConnect();
    return tcs.Task;
}

private Task LoginAsync(DataFeed feed, string user, string password)
{
    var tcs = new TaskCompletionSource<object>();
    feed.OnReady += _ => tcs.TrySetResult(null);
    feed.BeginLogin(user, pass);
    return tcs.Task;
}

现在您可以使用这些方法:

public async void InitConnection()
{
    var feed = new DataFeed(host, port);
    await ConnectAsync(feed);
    await LoadAsync(feed, user, pass);
    //Now I'm ready
}

注意:您可以将这些异步方法移动到DataFeed类中。但如果您可以修改DataFeed,则最好使用TaskFactory.FromAsync来封装APM API为Tasks。

不幸的是,没有非泛型的TaskCompletionSource可以返回非泛型的Task,所以通常的解决方法是使用Task<object>


你的方法里面没有看到 async 关键字,难道不应该加上吗? - nakiya
2
+1,这就是TaskCompletionSource<T>的作用。但有一个问题,如果失败会发生什么?出于好奇,使用TaskCompletionSource<object>TaskCompletionSource<bool>相比是否有任何好处,我怀疑没有区别? - Jodrell
@nakiya 不应该在这些方法上使用 async - 内部没有任何等待的操作。这些方法只是返回任务。 - Sergey Berezovskiy
2
你仍然可以让方法返回 Task,因为 Task<T> 继承自 Task - Mike Zboray
@SergeyBerezovskiy:当我收到登录响应并将其识别为正确时,将调用OnReady。因此,没有像EndConnect那样的直接回调。当发生这种情况时,我会触发OnReady。如果我删除该事件,如何转换为异步等待? - nakiya
显示剩余3条评论

0
你需要修改你的 DataFeed 类来支持这个。你只需要在里面使用任务异步模式。也就是说,DataFeed 中的所有异步方法都必须返回一个 Task(或一些 Task<T>),并且它们应该被命名为 ConnectAsync(例如)。
现在,对于 Socket,这并不完全容易,因为 Socket 上的 XXXAsync 方法实际上不能等待!最简单的方法是分别使用 TcpClientTcpListener(如果你正在使用 TCP):
public async Task<bool> LoginAsync(TcpClient client, ...)
{
  var stream = client.GetStream();

  await stream.WriteAsync(...);

  // Make sure you read all that you need, and ideally no more. This is where
  // TCP can get very tricky...
  var bytesRead = await stream.ReadAsync(buf, ...);

  return CheckTheLoginResponse(buf);
}

然后从外部使用它:

await client.ConnectAsync(...);

if (!(await LoginAsync(client, ...))) throw new UnauthorizedException(...);

// We're logged in

这只是示例代码,我假设您实际上能够编写体面的TCP代码。如果您能够这样做,使用await异步编写它并不是很难。只需确保始终等待某些异步I/O操作。

如果您想仅使用Socket完成相同的操作,则可能需要使用Task.FromAsync,它提供了一个包装器,围绕BeginXXX/EndXXX方法 - 非常方便。


这并不理想,因为即使在我收到登录响应之前,我仍将从服务器那里接收心跳等内容。 - nakiya
@nakiya 嗯,是的,这就是为什么你需要避免直接使用Write和Read。应该有一个消息处理器来处理消息,你应该等待它。所以,你不是await stream.ReadAsync,而是await messageProcessor.WaitForMessageAsync<LoginMessage>(...)或其他东西。这一切都取决于你想要多深度地支持可等待性——如果你想慢慢来,像Sergey建议的那样将事件包装在TaskCompletionSource中是一个很好的方法。如果你不介意将所有东西都改成可等待模式,我的方法可能更好。不过,它确实需要更多的管道工作。 - Luaan
我该如何实现你提到的 WaitForMessageAsync<LoginMessage>?我有些困惑。你能否提供一个例子给我参考一下吗? - nakiya
@nakiya 是的,那是比较棘手的部分。基本上,你需要在 TCP 连接之上拥有整个消息传递的抽象。你的应用程序不应该关心它使用的是 TCP,而这个抽象的消息处理程序将自行处理消息并提供可等待的方法来发送和接收消息。我将在我的 TCP 网络文章中展示如何做到这一点,但可能需要几周时间才能完成。虽然它比使用事件更复杂,但它使异步处理变得更加容易从外部操作。 - Luaan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接