简体   繁体   中英

C# in Unity: Calling Promise Style Async method without Blocking the Main Thread

I have a code snippet which is acting like a Grpc Client in Unity. The code style is designed for a console application, which can be called in Main method, blocking it and receiving data all the time. Now, I want to use it in Unity and obviously I want my app to run in Unity at the same time. Also, my end goal is to have something that works like a Udp Client. You call it one time, and all the time will receive data for you, without blocking any part of the host application.

The most important part of this design is that, if there is any event, I will get update, if there is no new event, accordingly, I am not receiving any data. And it happens all in ObserveEvents(channel).Wait(); . The problem is Wait(); . Which is all the time, keep the main thread, working thread, listening to updates. In Play mode Unity does not respond anymore!

I can bypass it and say, I don't need such a design, I can receive events every other second or every some frames. By doing that, I have my Unity application, but I am losing so many frame rates, regardless of the fact that my data are NOT flowing smoothly to my host application in 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;
            }
        }
    }
}

And I am calling it like:

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

in Update() method. Unfortunately, it does not work in Start() method. And yes, apparently, every frame it needs to create a new Channel, otherwise it will not work.

And the current implementation is like this, which is calling it every 7 frames without using that special wait for events design:

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;
        }
    }
}

My Question is that, first, if this method is completely async , why does it still block the application in Unity, also how I can redesign it to achieve something like Udp or Tcp style ?

Thanks to @Ilian, @zambari, @Jan Tattermusch and of course my colleague Rainer, the creator of the Grpc Api connection interface for us. I changed my structure to some degree that now works very performant both on Sender and Receiver computer. Below, I am going to explain what I have changed.

I have two classes, both are attached to one gameObject in Unity hierarchy: GrpcSetup.cs and AxeValuesConnectionInterface.cs. I commented on scripts, I hope it helps.

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();
        }
    }
}

and my AxeValuesConnectionInterface.cs goes like this:

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());
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM