c# - 如何从必须留在主线程上的方法统一调用异步方法

标签 c# unity3d asynchronous

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

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

unity中该方法绑定(bind)到一个ui按钮的OnClick():

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 的博客上阅读了更多关于死锁的信息 here (他的网站似乎占用了 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();
    }
}

万幸,传奇的 Stephen Cleary 看到了这个

最佳答案

假设您通过最新版本的 Unity 使用异步/等待,并将“.NET 4.x 等效”设置为脚本运行时版本,那么您编写的 RequestSpawn() 方法应该是在 Unity 的主线程上运行。您可以通过调用验证:

Debug.Log(System.Threading.Thread.CurrentThread.ManagedThreadId);

以下简单测试使用 Unity 2018.2(下面的输出)为我正确加载新场景:

public async void HandleAsync()
{
    Debug.Log($"Foreground: {System.Threading.Thread.CurrentThread.ManagedThreadId}");
    await WorkerAsync();
    Debug.Log($"Foreground: {System.Threading.Thread.CurrentThread.ManagedThreadId}");
}

private async Task WorkerAsync()
{
    await Task.Delay(500);
    Debug.Log($"Worker: {Thread.CurrentThread.ManagedThreadId}");
    await Task.Run((System.Action)BackgroundWork);
    await Task.Delay(500);
    SceneManager.LoadScene("Scene2");
}

private void BackgroundWork()
{
    Debug.Log($"Background: {Thread.CurrentThread.ManagedThreadId}");
}

输出:

Foreground: 1

Worker: 1

Background: 48

Foreground: 1

关于c# - 如何从必须留在主线程上的方法统一调用异步方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51472393/

相关文章:

c# - async static Task<T> someMethod() 与 static async Task<T> someOtherMethod() 之间有什么区别

c# - ReSharper 如何知道 "Expression is always true"?

c# - HTMLBody 拒绝输出我指定的字体大小,总是以不同的大小结束

c# - 捕获组的一部分

c# - 将 Sprite 作为参数传递给 Unity 中的构造函数

ios - 带有 Google Cardboard 的 Unity 5.6 在每只眼睛中显示非常不同的图像

c# - 用于 Rolling Sky x 轴控制的统一触摸输入

python - Celery:如何忽略和弦或链中的任务结果?

c# - 如何存储在线程池中运行的任务的结果?

c# - 测试异步方法时如何使用序列?