如何在SignalR客户端中使用async/await和hub.On?

13

我有一个 .Net 的Windows服务(客户端),它正在与 SignalR Hub(服务器)通信。大多数客户端方法需要一些时间才能完成。当从服务器接收到调用时,如何包装目标方法/hub.On以避免警告:

"因为未等待此调用,所以在调用完成之前,当前方法的执行将继续进行。请考虑对调用结果应用 await 运算符"

以下是客户端的启动/设置代码示例:

IHubProxy _hub
string hubUrl = @"http://localhost/";

var connection = new HubConnection(hubUrl, hubParams);
_hub = connection.CreateHubProxy("MyHub");
await connection.Start();

_hub.On<Message>("SendMessageToClient", i => OnMessageFromServer(i.Id, i.Message));
_hub.On<Command>("SendCommandToClient", i => OnCommandFromServer(i.Id, i.Command));

此外,在客户端上,这是一些方法的示例:

public static async Task<bool> OnMessageFromServer(string Id, string message)
{
    try
    {
        var result = await processMessage(message);  //long running task
    }
    catch (Exception ex)
    {
        throw new Exception("There was an error processing the message: ", ex);
    }
    return result;
}

public static async Task<bool> OnCommandFromServer(string Id, string command)
{
    try
    {
        var result = await processCommand(command);  //long running task
    }
    catch (Exception ex)
    {
        throw new Exception("There was an error processing the message: ", ex);
    }
    return result;
}

最终,我认为_hub.On是注册回调函数,而不是来自服务器的实际执行(调用)。 我认为我需要介入实际执行的中间过程,在等待On [X] FromServer的结果后返回结果。

************* 带有更正代码的更新示例*********************

IHubProxy _hub
string hubUrl = @"http://localhost/";

var connection = new HubConnection(hubUrl, hubParams);
_hub = connection.CreateHubProxy("MyHub");
await connection.Start();

//original
//_hub.On<Message>("SendMessageToClient", i => OnMessageFromServer(i.Id, i.Message));
//_hub.On<Command>("SendCommandToClient", i => OnCommandFromServer(i.Id, i.Command));

//new async 
_hub.On<Message>("SendMessageToClient", 
    async (i) => await OnMessageFromServer(i.Id, i.Message));

_hub.On<Message>("SendCommandToClient", 
    async (i) => await OnCommandFromServer(i.Id, i.Message));

//expanding to multiple parameters
_hub.On<Message, List<Message>, bool, int>("SendComplexParamsToClient", 
    async (p1, p2, p3, p4) => 
       await OnComplexParamsFromServer(p1.Id, p1.Message, p2, p3, p4));    

然后目标方法的签名将类似于

public static async Task<bool> OnComplexParamsFromServer(string id, string message,
                 List<Message> incommingMessages, bool eatMessages, int repeat)
{
    try
    {
        var result = await processCommand(message);  //long running task
        if (result) 
        {
             // eat up your incoming parameters
        }
    }
    catch (Exception ex)
    {
        throw new Exception("There was an error processing the message: ", ex);
    }
    return result;
}

感谢@AgentFire的迅速回复!!!


2
建议不要使用.Wait();,而是使用await connection.Start() - AgentFire
谢谢建议...我已经更新了代码以便未来的读者。 - Greg Grater
3个回答

14

这是一种void-awaitable模式,使用方法如下:

_hub.On<Message>("SendMessageToClient", async i => await OnMessageFromServer(i.Id, i.Message))

感谢您的回答!!!从可读性的角度来看,“void-awaitable”返回一个bool?从代码上看,它似乎没有返回任何东西。是否有更好的方法来注册回调,以便其他开发人员查看我的代码时更清晰明了?还是这对于真正的开发人员非常清楚,只是对像我这样的新手不太清楚? :-) - Greg Grater
2
是的,async-void方法对每个人都非常熟悉。有三种类型的异步方法...实际上,您应该了解整个过程。这不会伤害到你的:] - AgentFire
5
您的回答是互联网上关于“void-awaitable”这个术语的唯一资源。此外,我也不明白“async void”与此有何关系,除此之外,“async void”也被极力不推荐使用。 - user247702
这不是异步空类型。这是异步任务<bool>,因为OnMessageFromServer返回Task<bool>,这完全没问题。 - bboyle1234
@bboyle1234,您也可以在文档中看到这一点。HubProxyExtensions.On采用具有任意数量参数的Action。如果您查看Action(以及定义参数数量的所有Action<T>类型)的文档,则它们都在页面的第一行上说,“不返回值”。返回值为匿名函数是Func<TResult> - Gabriel Luci
显示剩余2条评论

4
SignalR客户端旨在按顺序调用处理程序方法,而不进行交错。换句话说,“SingleThreaded”。您通常可以设计信号R客户端代码,依赖于所有处理程序方法都被称为“SingleThreaded”。 (我在引号中使用“SingleThreaded”,因为……它并不是单线程的,但我们似乎没有语言来表达异步方法以概念上的单线程方式顺序调用而不进行交错)。
然而,在这里讨论的“async-void”方法打破了这个设计假设,并导致意外的副作用,即客户端处理程序方法现在被同时调用。以下是导致副作用的代码示例:
/// Yes this looks like a correct async method handler but the compiler is
/// matching the connection.On<int>(string methodName, Action<int> method)
/// overload and we get the "async-void" behaviour discussed above
connection.On<int>(nameof(AsyncHandler), async (i) => await AsyncHandler(i)));

/// This method runs interleaved, "multi-threaded" since the SignalR client is just
/// "fire and forgetting" it.
async Task AsyncHandler(int value) {
    Console.WriteLine($"Async Starting {value}");
    await Task.Delay(1000).ConfigureAwait(false);
    Console.WriteLine($"Async Ending {value}");
}

/* Example output:
Async Starting 0
Async Starting 1
Async Starting 2
Async Starting 3
Async Starting 4
Async Starting 5
Async Starting 6
Async Starting 7
Async Starting 8
Async Ending 2
Async Ending 3
Async Ending 0
Async Ending 1
Async Ending 8
Async Ending 7
*/

如果您正在使用ASP.NET Core,我们可以附加异步方法处理程序,让客户端按顺序依次调用它们,而不会交织,也不会阻塞任何线程。 我们利用了在SignalR for ASP.NET Core中引入的以下覆盖
IDisposable On(this HubConnection hubConnection, string methodName, Type[] parameterTypes,
                Func<object[], Task> handler)

这是实现它的代码。不幸的是,你编写用于附加处理程序的代码有点晦涩,但以下是它:
/// Properly attaching an async method handler
connection.On(nameof(AsyncHandler), new[] { typeof(int) }, AsyncHandler);

/// Now the client waits for one handler to finish before calling the next.
/// We are back to the expected behaviour of having the client call the handlers
/// one at a time, waiting for each to finish before starting the next.
async Task AsyncHandler(object[] values) {
    var value = values[0];
    Console.WriteLine($"Async Starting {value}");
    await Task.Delay(1000).ConfigureAwait(false);
    Console.WriteLine($"Async Ending {value}");
}

/* Example output
Async Starting 0
Async Ending 0
Async Starting 1
Async Ending 1
Async Starting 2
Async Ending 2
Async Starting 3
Async Ending 3
Async Starting 4
Async Ending 4
Async Starting 5
Async Ending 5
Async Starting 6
Async Ending 6
Async Starting 7
Async Ending 7
*/

当然,现在你知道如何根据需要实现任意一种客户端行为。如果选择使用 async-void 行为,最好要非常详细地注释代码,以免陷入其他程序员的困境,并确保不会抛出未处理的任务异常。

1
对于未来的读者,你所称呼的“SignalR客户端的最新版本”是指适用于ASP.NET Core的SignalR(Microsoft.AspNetCore.SignalR.Client)。而适用于ASP.NET的SignalR的最新版本(Microsoft.AspNet.SignalR.Client))没有这个方法。这很重要,因为客户端版本必须与服务器版本匹配。如果服务器不是ASP.NET Core,则无法使用ASP.NET Core客户端。 - Gabriel Luci
@GabrielLuci 很好的发现,如果你愿意,可以自由编辑它。 - bboyle1234
非常有趣的信息!你在哪里找到的?我能找到的任何文档都仅限于如何编写一个Hello World客户端,根本没有涉及线程等主题... - curious coder
1
这么多年过去了,这行代码仍然非常有用:"connection.On(nameof(AsyncHandler), new[] { typeof(int) }, AsyncHandler);" 谢谢! - undefined

4

我知道这已经很旧了,但是被接受的答案创建了一个 async void 的 lambda。

但是如果存在未处理的异常,async void 方法可能会导致应用程序崩溃。请阅读 这里这里

这些文章确实说到 async void 仅仅由于事件而被允许使用,并且我们现在所说的正是事件。但是依然如此,异常可以使整个应用程序崩溃。因此,如果你要使用它,请确保在任何可能抛出异常的地方都有 try/catch 块。

但是,async void 方法也可能导致意外行为,因为调用它的代码在继续执行其他操作之前不等待它完成。

记住,使用await的好处是ASP.NET可以去做其他事情,稍后再回到代码的其余部分。通常情况下这是很好的。但在这种特定情况下,它可能意味着两个或更多的传入消息可以同时被处理,而哪一个先完成处理是不确定的(完成处理的第一个不一定是最先进来的)。虽然这可能对你的情况有所影响,也可能没有影响。

你最好等待它:

_hub.On<Message>("SendMessageToClient",
                 i => OnMessageFromServer(i.Id, i.Message).GetAwaiter().GetResult());

请参见此处此处,了解使用.GetAwaiter().GetResult()而不是.Wait()的好处。


另一方面,所有的中心方法都涉及到普通调用和异步调用。那么,你建议使用哪种?我应该在连接方面使用异步方法,而在Hub类中使用其他方法吗?还是有些异步,有些普通? - user5871859
如果你正在调用一个方法,并且有一个异步方法可用,那么最好使用await。但在这种情况下,On<T>()方法不是设计用来接受一个async方法的,这意味着如果你传递一个async方法给它,它将不会等待你的方法完成,这可能会导致我所描述的问题。 - Gabriel Luci

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