C# 如何从必须留在主线程上的方法调用unity中的异步方法

C# 如何从必须留在主线程上的方法调用unity中的异步方法,c#,unity3d,asynchronous,C#,Unity3d,Asynchronous,我有一个名为SendWithReplyAsync的方法,它使用TaskCompletionSource在完成时发出信号,并从服务器返回回复 我试图从服务器上得到2个回复,这个回复的方法还需要更改场景、更改变换等,因此,据我所知,它需要在主线程上 此方法绑定到unity中ui按钮的OnClick(): public async void RequestLogin() { var username = usernameField.text; var password = passwo

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

我试图从服务器上得到2个回复,这个回复的方法还需要更改场景、更改变换等,因此,据我所知,它需要在主线程上

此方法绑定到unity中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}");
}
因此,当我单击按钮时,我可以看到(服务器端)所有消息(登录请求、生成请求和初始实体状态请求)都在发出。然而,在统一中没有任何事情发生。没有场景更改和(显然)没有位置或旋转更新

我有一种感觉,当谈到unity时,我对async/await有些不了解,并且我的
RequestSpawn
方法没有在主线程上运行

我尝试使用
client.sendwithreplysync(…).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;
}
公共异步任务SendWithReplyAsync(消息消息),其中三层:消息
{
var task=msgService.RegisterReplyHandler(消息);
发送(消息);
返回(三重)等待任务;
}
公共任务注册表replyhandler(消息消息,int timeout=MAX\u REPLY\u WAIT\u MS)
{
var replyToken=Guid.NewGuid();
var completionSource=new TaskCompletionSource();
var tokenSource=new CancellationTokenSource();
tokenSource.CancelAfter(超时);
//TODO确保对Token.Register()的调用没有泄漏
tokenSource.Token.Register(()=>
{
completionSource.TrySetCanceled();
if(replyTasks.ContainsKey(replyToken))
replyTasks.Remove(replyToken);
},
假);
添加(replyToken,completionSource);
message.ReplyToken=ReplyToken;
返回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();
    }
}
private void HandleMessage(TMessage message,object sender=null),其中TMessage:message
{
//检查邮件是否回复了以前发送的邮件。
//如果是,我们可以用结果完成回复任务
如果(message.ReplyToken.HasValue&&
replyTasks.TryGetValue(message.ReplyToken.Value,out TaskCompletionSource tcs)&&
!tcs.Task.IsCanceled)
{
设置结果(消息);
返回;
}
//该消息不是应答,因此我们可以像往常一样调用相关的处理程序
var messageType=message.GetType();
if(messageHandlers.TryGetValue(messageType,out-List处理程序))
{
foreach(处理程序中的变量处理程序)
{
//如果没有特定的消息类型,则必须动态调用处理程序
//如果我们确实有一个特定的类型,那么使用.invoke()可以更快地调用处理程序
if(typeof(TMessage)=typeof(Message))
handler.DynamicInvoke(发送方,消息);
其他的
((操作)处理程序).Invoke(发送方,消息);
}
}
其他的
{
LogError(string.Format(“未找到类型为{0}”、messageType.FullName的消息的处理程序”);
抛出新的NoHandlersException();
}
}

传说中的斯蒂芬·克利里(Stephen Cleary)看到了这一点,他希望通过协同路由和回调实现异步请求

首先,开始一次合作。。。。 在例行程序中,你准备并发出请求。。。。 在此之后,你可以用以下方法打破常规:

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

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

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

Debug.Log(System.Threading.Thread.CurrentThread.ManagedThreadId);
以下简单测试使用Unity 2018.2为我正确加载新场景(输出如下):

输出:

前景:1

工人:1

背景:48

前景:1


我建议您使用UniRx.Async库中的UniTask,该库为您提供了现成的功能:


我添加了完成回复任务的位。问题是,在其他方法中,我调用了相同的函数,
SendWithReplyAsync
,它们工作得很好。您是否排除了目标场景未包含在构建中等问题?您知道吗,您的示例代码对我帮助很大。它帮助我认识到我的问题根本与async/await无关,而是我的网络代码产生了错误的长度前缀。如果不是你帮我做的腿活,我还是会把头发从上面扯下来。谢谢伟大的如果您认为它充分解决了您的原始问题,您可以将其标记为正确的。:)
Debug.Log(System.Threading.Thread.CurrentThread.ManagedThreadId);
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}");
}