我有一个名为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看到了这个
答案 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,它为您提供了开箱即用的功能: