首页 > 解决方案 > 如何从必须保留在主线程上的方法统一调用异步方法

问题描述

我有一个名为 的方法SendWithReplyAsync,它用于TaskCompletionSource在完成时发出信号并从服务器返回回复。

我正在尝试以一种还需要更改场景、更改变换等的方法从服务器获得 2 个回复,因此据我所知,它需要位于主线程上。

这个方法OnClick()统一绑定到一个ui按钮的:

public async void RequestLogin()
{
    var username = usernameField.text;
    var password = passwordField.text;
    var message  = new LoginRequest() { Username = username, Password = password };

    var reply = await client.SendWithReplyAsync<LoginResponse>(message);

    if (reply.Result)
    {
        Debug.Log($"Login success!");
        await worldManager.RequestSpawn();
    }
    else Debug.Log($"Login failed! {reply.Error}");
}

如您所见,有一个电话await WorldManager.RequestSpawn();

该方法如下所示:

public async Task RequestSpawn()
{
    //Get the initial spawn zone and transform
    var spawnReply = await client.SendWithReplyAsync<PlayerSpawnResponse>(new PlayerSpawnRequest());

    //Load the correct zone
    SceneManager.LoadScene("TestingGround");

    //Move the player to the correct location
    var state = spawnReply.initialState;
    player.transform.position = state.Position.Value.ToVector3();
    player.transform.rotation = Quaternion.Euler(0, state.Rotation.Value, 0);

    //The last step is to get the visible entities at our position and create them before closing the loading screen
    var statesReply = await client.SendWithReplyAsync<InitialEntityStatesReply>(new InitialEntityStatesRequest());

    SpawnNewEntities(statesReply);
}

因此,当我单击按钮时,我可以看到(服务器端)所有消息(登录请求、生成请求和初始实体状态请求)都在发出。然而,没有任何事情发生在合一中。没有场景变化和(显然)没有位置或旋转更新。

我有一种感觉,当涉及到统一时,我对 async/await 不了解,而且我的RequestSpawn方法没有在主线程上运行。

我尝试在所有方法上使用client.SendWithReplyAsync(...).Result和删除async关键字,但这只会导致死锁。我在 Stephen Cleary 的博客上阅读了更多关于死锁的信息(似乎他的网站消耗了 100% 的 cpu ......我是唯一的一个吗?)

我真的不知道如何让这个工作。

如果您需要它们,以下是发送/接收消息的方法:

public async Task<TReply> SendWithReplyAsync<TReply>(Message message) where TReply : Message
{
    var task = msgService.RegisterReplyHandler(message);
    Send(message);

    return (TReply)await task;
}

public Task<Message> RegisterReplyHandler(Message message, int timeout = MAX_REPLY_WAIT_MS)
{
    var replyToken = Guid.NewGuid();

    var completionSource = new TaskCompletionSource<Message>();
    var tokenSource = new CancellationTokenSource();
    tokenSource.CancelAfter(timeout);
    //TODO Make sure there is no leakage with the call to Token.Register() 
    tokenSource.Token.Register(() =>
    {
        completionSource.TrySetCanceled();
        if (replyTasks.ContainsKey(replyToken))
            replyTasks.Remove(replyToken);
    },
        false);

    replyTasks.Add(replyToken, completionSource);

    message.ReplyToken = replyToken;
    return completionSource.Task;
}

这是完成任务的位置/方式:

private void HandleMessage<TMessage>(TMessage message, object sender = null) where TMessage : Message
{
    //Check if the message is in reply to a previously sent one.
    //If it is, we can complete the reply task with the result
    if (message.ReplyToken.HasValue &&
        replyTasks.TryGetValue(message.ReplyToken.Value, out TaskCompletionSource<Message> tcs) &&
        !tcs.Task.IsCanceled)
    {
        tcs.SetResult(message);
        return;
    }

    //The message is not a reply, so we can invoke the associated handlers as usual
    var messageType = message.GetType();
    if (messageHandlers.TryGetValue(messageType, out List<Delegate> handlers))
    {
        foreach (var handler in handlers)
        {
            //If we have don't have a specific message type, we have to invoke the handler dynamically
            //If we do have a specific type, we can invoke the handler much faster with .Invoke()
            if (typeof(TMessage) == typeof(Message))
                handler.DynamicInvoke(sender, message);
            else
                ((Action<object, TMessage>)handler).Invoke(sender, message);
        }
    }
    else
    {
        Debug.LogError(string.Format("No handler found for message of type {0}", messageType.FullName));
        throw new NoHandlersException();
    }
}

手指交叉传奇的斯蒂芬克利里看到了这个

标签: c#unity3dasynchronous

解决方案


我建议您使用 UniRx.Async 库中的 UniTask,它为您提供了开箱即用的功能:

https://github.com/Cysharp/UniTask


推荐阅读