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

时间:2018-07-23 06:01:44

标签: c# unity3d asynchronous

我有一个名为SendWithReplyAsync的方法,它使用TaskCompletionSource表示何时完成并从服务器返回答复。

我正在尝试从服务器获取2个答复,该方法还需要更改场景,更改变换等,因此据我所知,它必须位于主线程上。

此方法以统一的方式绑定到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);
}

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

我感觉到我不了解异步/等待的统一性,并且我的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看到了这个

3 个答案:

答案 0 :(得分:1)

尝试通过协同程序和回调执行异步请求,...

首先,启动协程。 在例行程序中,您准备并触发请求。 在这之后,您用以下方法打破常规:

while(!request.isDone) {
    yield return null;
}

获得响应后,使用回调函数更改场景

答案 1 :(得分:0)

假设您正在通过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}");
}

输出:

  

前景:1

     

工人:1

     

背景:48

     

前景:1

答案 2 :(得分:0)

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

https://github.com/Cysharp/UniTask