【问题标题】:C# in Unity: Calling Promise Style Async method without Blocking the Main ThreadUnity 中的 C#:在不阻塞主线程的情况下调用 Promise 风格的异步方法
【发布时间】:2019-05-05 06:28:29
【问题描述】:

我有一个代码 sn-p,它的作用类似于 Unity 中的 Grpc 客户端。代码风格是为控制台应用程序设计的,可以在Main方法中调用,阻塞它并一直接收数据。现在,我想在 Unity 中使用它,显然我希望我的应用程序同时在 Unity 中运行。此外,我的最终目标是拥有像 Udp 客户端一样工作的东西。您只需调用一次,它就会一直为您接收数据,而不会阻塞宿主应用程序的任何部分。

这个设计最重要的部分是,如果有任何事件,我会得到更新,如果没有新事件,相应地,我不会收到任何数据。它发生在 ObserveEvents(channel).Wait(); 中。问题是 Wait();。这一直是,保持主线程,工作线程,听更新。在播放模式下,Unity 不再响应!

我可以绕过它说,我不需要这样的设计,我可以每隔一秒或每隔几帧接收一次事件。通过这样做,我拥有了我的 Unity 应用程序,但我失去了很多帧速率,不管我的数据没有顺利地流向我在 Unity 中的主机应用程序。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Grpc.Core;
using UnityEngine;

namespace Scripts
{
    public class GrpcChannel
    {       
        public void GrpcServer(string ip, int port) 
        {
            var channel = new Channel(ip, port, ChannelCredentials.Insecure);           
            ObserveEvents(channel).Wait();
            channel.ShutdownAsync().Wait();
        }

        private async Task ObserveEvents(Channel channel)
        {
            Debug.Log("Observing events...");

            var eventService = new EventService.EventServiceClient(channel);
            var request = new RegisterOnEvent();

            using (var call = eventService.OnEvent(request))
            {
                var responseStream = call.ResponseStream;

                while (await responseStream.MoveNext())
                {
                    //var receivedEvent = responseStream.Current;

                    // on change of any data, this method will be fired
                    GetJointPosition(channel, "Flower_id_22134");
                }
            }
        }

        private void GetJointPosition(Channel channel, string flowerName)
        {
            var client = new JointPositionService.JointPositionServiceClient(channel);

            var request = new GetJointPositionRequest
            {
                FlowerName = flowerName
            };

            var response = client.GetJointPositionAsync(request);

            SavePositions(response.ResponseAsync.Result.Positions);
        }

        private void SavePositions(IEnumerable<JointPosition> positions)
        {
            var jointPositions = positions.ToList();

            for (var i = 0; i < Instance.Ref.AxesValues.Length; i++)
            {
                var axeValueDegree = jointPositions[i].Value * 180 / Math.PI;
                Instance.Ref.AxesValues[i] = (float)axeValueDegree;
            }
        }
    }
}

我这样称呼它:

var grpcChannel = new GrpcChannel();
grpcChannel.GrpcServer("192.168.123.16", 30201);

在 Update() 方法中。不幸的是,它在 Start() 方法中不起作用。是的,显然,它需要创建一个新 Channel 的每一帧,否则它将无法工作。

目前的实现是这样的,每 7 帧调用一次,没有使用特殊的等待事件设计:

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Grpc.Core;
using TMPro;
using UnityEngine;

namespace Assets.Scripts
{
    public class AxisValuesService : MonoBehaviour
    {
        public TextMeshProUGUI[] AxesValuesTexts;

        [HideInInspector] public Dictionary<uint, float> AxisValues = new Dictionary<uint, float>();
        [HideInInspector] private int counter = 0;

        private void Update()
        {
            counter++;

            if (counter == 7)
            {
                try
                {
                    var channel = new Channel("192.168.123.16", 30201, ChannelCredentials.Insecure);

                    GetJointPosition(channel, "Flower_id_22134");
                    //ObserveEvents(channel).Wait();

                    channel.ShutdownAsync().Wait();
                }
                catch (RpcException e)
                {
                    Debug.LogError("Connection Error: " + e);
                }

                counter = 0;
            }

        }

        private void GetJointPosition(Channel channel, string robotName)
        {
            // Request Axis Values
            var client = new JointPositionService.JointPositionServiceClient(channel);
            var request = new GetJointPositionRequest { RobotName = robotName };
            var response = client.GetJointPositionAsync(request);

            // Fill Dictionary
            foreach (var i in response.ResponseAsync.Result.Positions)
            {
                double value = toDegree((double)i.Value);
                AxisValues[i.Index] = (float)Math.Round(value, 2);
            }

            try
            {
                AxesValuesTexts[0].text = AxisValues[1].ToString();
                AxesValuesTexts[1].text = AxisValues[2].ToString();
                AxesValuesTexts[2].text = AxisValues[3].ToString();
                AxesValuesTexts[3].text = AxisValues[4].ToString();
                AxesValuesTexts[4].text = AxisValues[5].ToString();
                AxesValuesTexts[5].text = AxisValues[6].ToString();
            }
            catch (Exception e)
            {
                Debug.Log("Dictionary problem.");
            }


        }

        private double toDegree(double rad)
        {
            return (float)(180 / Math.PI) * rad;
        }
    }
}

我的问题是,首先,如果这个方法是完全异步,为什么它仍然在 Unity 中阻塞应用程序,以及我如何重新设计它以实现某些目标喜欢 Udp 或 Tcp 样式

【问题讨论】:

  • ObserveEvents 可能是 async,但是您在返回的 Task 上调用 WaitWait 将阻塞直到 Task 完成。
  • @Ilian 感谢您的评论。如果我不调用 Wait,那么我的 ObserveEvents(channel);方法只会被调用一次。我会尝试寻找替代方案并回信给您。
  • 在我看来,您根本没有使用线程,所有通信都可能发生在一个线程中,该线程只是将新数据汇集到更新可以检查每一帧的队列中
  • await ObserveEvents()ObserveEvents().Wait() 之间存在很大差异。第一个是异步的,另一个实际上阻塞了等待它完成的线程。我敢打赌,在 UI 线程上阻塞是应该避免的。我建议阅读 async/await 最佳实践。 msdn.microsoft.com/en-us/magazine/jj991977.aspxdocs.microsoft.com/en-us/windows/uwp/debug-test-perf/…

标签: c# multithreading unity3d asynchronous grpc


【解决方案1】:

感谢@Ilian、@zambari、@Jan Tattermusch,当然还有我的同事 Rainer,他是我们 Grpc Api 连接接口的创建者。我在某种程度上改变了我的结构,现在在发送方和接收方计算机上都非常高效。下面,我将解释我所做的更改。

我有两个类,它们都附加到 Unity 层次结构中的一个游戏对象:GrpcSetup.cs 和 AxeValuesConnectionInterface.cs。我对脚本发表了评论,希望对您有所帮助。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using FlowerShop.Grpc.Service.Joint;
using FlowerShop.Grpc.Service.Joint.Event;
using FlowerShop.Grpc.Service.System;
using UnityEngine;

namespace Scripts
{
    public class GrpcSetup : MonoBehaviour
    {
        private int _loopController = 1;
        private Channel _grpcChannel;

        private EventService.EventServiceClient _eventService;
        private RegisterOnEvent _request;
        private IAsyncStreamReader<Any> _responseStream;
        private Any _receivedEvent;
        private JointPositionChangedEvent _positionChangedEvent;

        // this method will be called only one time in start method in another class
        // .. I am initializing a channel for Grpc and some required properties for my ObserveEvents method
        public void GrpcChannelInitialization()
        {
            _grpcChannel = new Channel("192.168.100.16", 50001, ChannelCredentials.Insecure);

            _eventService = new EventService.EventServiceClient(_grpcChannel);
            _request = new RegisterOnEvent();
        }

        // this method will be called in Update in another class
        public async void GrpcUpdateMethod()
        {
            try
            {
                // to get the initial axesVales only one time
                if (_loopController == 1)
                {
                    await GetJointPositionOnDemand(_grpcChannel, "Flower_li35443");
                    _loopController = 0;
                }

                // receiving Events only when they are available
                await ObserveEvents();
            }
            catch (RpcException e)
            {
                Debug.LogError("Connection Error: " + e);
            }
        }

        // this method will be called every frame, the good thing about it is that, I will only receive events, 
        // .. when there are some available.
        private async Task ObserveEvents()
        {
            using (var call = _eventService.OnEvent(_request))
            {
                _responseStream = call.ResponseStream;

                if (await _responseStream.MoveNext())
                {
                    Debug.Log("New Event is available.");

                    _receivedEvent = call.ResponseStream.Current;

                    if (_receivedEvent.TypeUrl.EndsWith(JointPositionChangedEvent.Descriptor.FullName))
                    {
                        _positionChangedEvent = _receivedEvent.Unpack<JointPositionChangedEvent>();

                        _positionChangedEvent.Positions.ToList().ForEach(i =>
                            Instance.Ref.AxesValues[i.Index - 1] = (float) Math.Round(i.Value * Mathf.Rad2Deg, 2));
                    }
                }
            }
        }

        // if I want to get Joint values whenever I like, regardless of ObserveEvents method architecture
        // .. at this moment, I am calling it, one time in Update method
        private async Task GetJointPositionOnDemand(Channel channel, string flowerName)
        {
            var client = new JointPositionService.JointPositionServiceClient(channel);
            var requestLocal = new GetJointPositionRequest {FlowerName= flowerName};
            var response = await client.GetJointPositionAsync(requestLocal);

            foreach (var i in response.Positions)
            {
                var value = i.Value * Mathf.Rad2Deg;
                Instance.Ref.AxesValues[i.Index - 1] = (float) Math.Round(value, 2);
            }
        }

        // this will be called in Unity reserved method: OnApplicationQuit
        // .. so we are trying to get rid of our opened channel
        public async Task ChannelShutDown()
        {
            await _grpcChannel.ShutdownAsync();
        }
    }
}

我的 AxeValuesConnectionInterface.cs 是这样的:

using System.Threading.Tasks;
using UnityEngine;

namespace Scripts
{
    [RequireComponent(typeof(GrpcSetup))]
    public class AxeValuesConnectionInterface : MonoBehaviour
    {
        private GrpcSetup _grpcSetup;
        private float _timeElapsed;
        private int _loopController = 2;
        private int _loopController1 = 1;
        private int _loopController2 = 1;
        private int _counterLoop;

        private void Start()
        {
            _grpcSetup = GetComponent<GrpcSetup>();
        }

        private void Update()
        {    
            GrpcInitialization();
            GrpcUpdateMethods();
        }

        private void OnApplicationQuit()
        {
            Task.Run(_grpcSetup.ChannelShutDown);
        }

        private void GrpcInitialization()
        {
            if (_loopController2 != 1) return;

            if (Instance.Ref.ConnectionInterface != Instance.Ci.Grpc) return;

            _grpcSetup.GrpcChannelInitialization();
            _loopController2 = 0;
        }

        private void GrpcUpdateMethods()
        {
            if (Instance.Ref.ConnectionInterface != Instance.Ci.Grpc || !Instance.Ref.FlowerIsPresent) return;

            Task.Run(() => _grpcSetup.GrpcUpdateMethod());
        }
    }
}

【讨论】:

    猜你喜欢
    • 2016-07-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多