0

I have an async function which still freezes / lags the UI thread for me when I execute it. This is my function calling it.

private void TcpListenerLogic(object sender, string e)
        {
            Application.Current.Dispatcher.BeginInvoke((Action)async delegate {
                try
                {
                    dynamic results = JsonConvert.DeserializeObject<dynamic>(e);
                    if (results.test_id != null)
                    {
                        // Get properties for new anchor
                        string testInformation = await CommunicationCommands.getJsonFromURL(
                            "http://" + ServerIP + ":" + ServerPort + "/api/" + results.test_id);
                    }                    
                }
                catch (Exception exception)
                {
                    // Writing some Trace.WriteLine()'s
                }
            });
           
        }

And this is the async function that freezes my UI Thread

        public static async Task<string> getJsonFromURL(string url)
        {
            
            try
            {
                string returnString = null;
                using (System.Net.WebClient client = new System.Net.WebClient())
                {
                    returnString = await client.DownloadStringTaskAsync(url);
                }
                return returnString;
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.ToString());
                return null;
            }

        }

I already tried to make everything in TcpListenerLogic run in a new Thread:

            new Thread(() =>
            {
                Thread.CurrentThread.IsBackground = true;

            }).Start();

Which resulted in the whole UI completely freezing. And I tried to make TcpListenerLogic async and await the dispatcher, which also made everything freeze permanently. I also tried to make TcpListenerLogic async and leave the dispatcher. The dispatcher is only there because I normally have some UI code in there, which I left out for my tests.

I have ventured far through the internet, but no BackgroundWorker, ThreadPool or other methods helped me in my endeavour. If anyone has help for this particular problem, or a resource that would improve my understanding of async functions in C#, I would much appreciate it.

Edit

As requested a deeper insight in how this event handler is called. I have System.Net.Websocket, which is connected to the Backend API I am working with and triggers an event, everytime he receives new Data. To guarantee the socket listens as longs as it is open, there is a while loop which checks for the client state:

        public event EventHandler<string> TcpReceived;
        public async void StartListener(string ip, int port, string path)
        {
            try
            {
                using (client = new ClientWebSocket())
                {
                    try
                    {   // Connect to backend
                        Uri serverUri = new Uri("ws://" + ip + ":" + port.ToString() + path );
                        await client.ConnectAsync(serverUri, CancellationToken.None);
                    }
                    catch (Exception ex)
                    {
                        BackendSettings.IsConnected = false;
                        Debug.WriteLine("Error connecting TCP Socket: " + ex.ToString());
                    }
                    state = client.State;
                    // Grab packages send in backend
                    while (client.State == WebSocketState.Open || client.State == WebSocketState.CloseSent)
                    {
                        try
                        {
                            // **Just formatting the received data until here and writing it into the "message" variable**//
                            TcpReceived(this, message);

                            // Close connection on command
                            if (result.MessageType == WebSocketMessageType.Close)
                            {
                                Debug.WriteLine("Closing TCP Socket.");
                                shouldstayclosed = true;
                                await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
                                break;
                            }
                            state = client.State;
                        }
                        catch
                        {
                            BackendSettings.IsConnected = false;
                            state = client.State;
                        }
                    }
                    state = client.State;
                }
            }
            catch (Exception ex)
            {
                // Some error messages and settings handling
            }
        }

The Event has a handler attached:

TcpReceived += TcpListener_TcpReceived;

And this is the Handler, which calls the previously seen "TcpListenereLogic".

        private void TcpListener_TcpReceived(object sender, string e)
        {
            TcpListenerLogic(sender, e);

            //App.Current.Dispatcher.BeginInvoke(new Action(() => {
            //      TcpListenerLogic(sender, e);
            //}));

            //new Thread(() =>
            //{
            //    Thread.CurrentThread.IsBackground = true;
            //    TcpListenerLogic(sender, e);
            //}).Start();
        }

I previously had the "TcpListenereLogic" as the handler, but I wanted to try different methods to call it. I also left in the commented out part, to show how the call of "TcpListenereLogic" looked already. All my attempts were with all mentioned setups and sadly lead to nothing.

ds600
  • 51
  • 9
  • `await` won't automatically start a new task, that's why your UI is still freezing. Use `Task.Run`. You may want to read through [this](https://stackoverflow.com/a/18015586/10196137) answer. – devsmn Nov 12 '21 at 10:03
  • 1
    How and where is the `TcpListenerLogic` method called? – Theodor Zoulias Nov 12 '21 at 10:36
  • 2
    ^^ Is it an event handler? – Fildor Nov 12 '21 at 10:36
  • First thing I'd recommend is to _not_ use WebClient. – Fildor Nov 12 '21 at 10:37
  • Not all async methods are non-blocking, even if perhaps they should be. I would also suggest looking at [Task based asynchronous programming](https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/task-based-asynchronous-programming) for the modern way to run things in the background. – JonasH Nov 12 '21 at 10:39
  • Assuming this _is_ in fact an event handler: You can make it `async void TcpListenerLogic...` (event handlers are _the_ exception from the "no async void" rule). – Fildor Nov 12 '21 at 10:42
  • @TheodorZoulias it is called in a new Background thread, by a method that is triggered everytime a new package arrives at a System.net.WebSocket: ``` new Thread(() => { Thread.CurrentThread.IsBackground = true; TcpListenerLogic(sender, e); }).Start();``` – ds600 Nov 12 '21 at 11:44
  • @Fildor It originally was an event handler, but is now called by an event handler, that triggers everytime a new package arrives at a System.net.WebSocket – ds600 Nov 12 '21 at 11:47
  • There is lots of indirection going on here. We need to know the original/initial caller of the `TcpListenerLogic` method. It's called from a `new Thread` delegate, OK. Who is starting this thread, and when? – Theodor Zoulias Nov 12 '21 at 11:55
  • If you do operation on separate thread then you don't need `async/await`. Do all long lasting operations on the thread and then do update GUI with `Dispatcher`. – Rekshino Nov 12 '21 at 12:06
  • @TheodorZoulias Alright, here is my straight setup: I create a System.Net.WebSocket and connect it to the Backend API I am working with. Then there is a "while (client.State == WebSocketState.Open)", which sends everything it receives to my "event EventHandler TcpReceived;". Everytime this event Handler receives something it calls the code you can see in my question (TcpListenerLogic) – ds600 Nov 12 '21 at 12:12
  • @Rekshino You are right and this has been my previous setup, it sadly had the same effect, seperating the methods and adding additional threads and asyncs has just been my latest desperate attempt to fix everything. – ds600 Nov 12 '21 at 12:15
  • ds600 could you include in the question the `while (client.State == WebSocketState.Open)` loop, including the code that raises the `TcpReceived` event, and the handler of this event? – Theodor Zoulias Nov 12 '21 at 12:27
  • *"The dispatcher is only there because I normally have some UI code in there, which I left out for my tests."* <== It would also help if you could include at least one UI-related line of code inside the `Dispatcher.BeginInvoke` delegate. – Theodor Zoulias Nov 12 '21 at 12:49
  • @ds600 Have you tried to localize which function cause the UI Freeze? Is it JSON parsing or waiting on response or something else? – Rekshino Nov 12 '21 at 12:49
  • @TheodorZoulias I added the requested information to my question. I sadly can't really add the UI part in, without going fairly deep into my program, as I am not directly editting the UI, but creating a class, that works with an observable collection, which edits the UI. I also removed all the code from my programm, so it shouldn't interfere with any suggestions / answers I get here – ds600 Nov 12 '21 at 13:04
  • @Rekshino Yes, I stripped everything except the await method and it let to the same lag, leaving everything but this method resulted in no further problems – ds600 Nov 12 '21 at 13:07
  • As a side note, the `StartListener` being an [`async void`](https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming#avoid-async-void) is a red flag (assuming that this method is not an event handler), but it's probably unrelated with the UI freezing problem. – Theodor Zoulias Nov 12 '21 at 13:40
  • How frequently is the `TcpReceived` event raised on average? Is it possible that it's raised thousands of times per second? – Theodor Zoulias Nov 12 '21 at 13:41
  • Having at least one UI related call in your code would help up provide some meaningful example of how to invoke this line in a way that doesn't freeze the UI. Without it, any answer will have to invent some imaginary call, and work with it. – Theodor Zoulias Nov 12 '21 at 13:46
  • @TheodorZoulias Thank you for mentioning the red flag, I'll look into that. TcpReceived is actually called around ~120 times per second, may this be the culprit? I'll seek something out I can add UI related – ds600 Nov 12 '21 at 14:06
  • ~120 times per second is quite a lot. Essentially if whatever you are going with the UI takes more than ~8 milliseconds per event, the UI message loop will be saturated, and won't be able to process the normal user activity. You should either ensure that the UI-related code is very lightweight, or aggregate somehow the events so that the UI is invoked less frequently. – Theodor Zoulias Nov 12 '21 at 14:13
  • @TheodorZoulias I now limited it to 20 times per second and that did the trick, should I create an answer for that, or do you want to create something to get the credit you deserve? – ds600 Nov 12 '21 at 15:24
  • ds600 I think that you are in a better position to [write an answer](https://stackoverflow.com/help/self-answer) that will be focused and useful to future viewers, than I am. :-) – Theodor Zoulias Nov 12 '21 at 15:33

1 Answers1

0

Thank you very much @TheodorZoulias for helping me to find the solution to my problem.

It turns out it wasn't the async function itself, but rather how often it gets called. It got called roughly ~120 times every second. My solution starts by calling the Listener method over a new Thread:

new Thread(() =>
{
    Thread.CurrentThread.IsBackground = true;
    MainWindow.tcpListener.StartListener(ip, portNumber, "/api/");
}).Start();

To limit the amount of calls that happen every second I added a dispatcher timer, that resets a bool after it has been used for a call, by my Event.

readonly System.Windows.Threading.DispatcherTimer packageIntervallTimer = 
                            new System.Windows.Threading.DispatcherTimer();
        bool readyForNewPackage = true;
        private void ReadyForPackage(object sender, EventArgs e)
        {
            readyForNewPackage = true;
        }

        public async void StartListener(string ip, int port, string path)
        {
            packageIntervallTimer.Interval = TimeSpan.FromMilliseconds(50);
            packageIntervallTimer.Tick += (s, e) => { Task.Run(() => ReadyForPackage(s, e)); };
            packageIntervallTimer.Start();

Then I wrapped everything inside the while loop into an if condition based on the bool, the most important part was to have my "event EventHandler TcpReceived" in there:

// Grab packages sent in backend
while (client.State == WebSocketState.Open || client.State == WebSocketState.CloseSent)
{
    if (readyForNewPackage == true)
    {
        readyForNewPackage = false;
        try
        {
           ....
           TcpReceived(this, message);
           ....
        }
        catch
        {
            ...
        }
    }
}

I added my TcpListenerLogic to the Eventhandler:

TcpReceived += TcpListenerLogic;

And my TcpListenerLogic now looked like this (names have been changed):

private async void TcpListenerLogic(object sender, string e)
{
    try
    {
        dynamic results = JsonConvert.DeserializeObject<dynamic>(e);
        if (results.test_id != null)
        {
            string testID = "";
            if (results.test_id is JValue jValueTestId)
            {
                testID = jValueTestId.Value.ToString();
            }
            else if (results.test_id is string)
            {
                testID = results.test_id;
            }

            // Get properties for new object
            string information = await CommunicationCommands.getJsonFromURL(
                "http://" + ServerIP + ":" + ServerPort + "/api/" + testID );
            if (information != null)
            {
                await App.Current.Dispatcher.BeginInvoke(new Action(() =>
                {
                    // Create object out of the json string
                    TestStatus testStatus = new TestStatus();
                    testStatus.Deserialize(information);
                    if (CommunicationCommands.isNameAlreadyInCollection(testStatus.name) == false)
                    {
                        // Add new object to the list
                        CommunicationCommands.allFoundTests.Add(testStatus);
                    }
                }));
            {
    }
    catch (Exception exception)
    {
        ....
    }

}

Adding a new Thread to execute any step results in problems, so keep in mind that all this uses the thread created at the beginning for "StartListener"

ds600
  • 51
  • 9