0

I have run into some legacy code Winforms code that basically does this...

private static bool _connected = false;
private static bool _closing = false;

static void Main(string[] args)
{
    // Stuff
    ThreadPool.QueueUserWorkItem(ThreadProc, null);
    // More stuff

    while (!_closing)
    {
        if (_connected)
        {
            break;
        }
        // Do stuff
        Application.DoEvents();
        Thread.Sleep(100);
        // Do stuff
    }

    if (_connected)
    {
        // Finally...
        Application.Run(new MyForm());
    }
}

private static void ThreadProc(object obj)
{
    while (!_closing)
    {
        // Stuff
        if (!Method2())
        {
            // Error
            _closing = true;
            return;
        }
        else
        {
            /// Success
            break;
        }
    }

    _connected = true;
}

private static async Task<SomeData> Method1()
{
    // There is a splash screen that gets displayed so we are
    // waiting for it to be available
    while (Application.OpenForms.Count == 0) Thread.Sleep(100);

    // So this gets run on the Windows GUI thread
    SomeData result = await (Application.OpenForms[0].Invoke(new Func<Task<SomeData>>(() => { SomethingAsync())));
    return result;
}

private static bool Method2()
{
    TaskAwaiter<SomeData> awaiter = Method1().GetAwaiter();
    while (!awaiter.IsCompleted)
    {
        // This appears to be why this code is done this way... 
        // to not block Windows events while waiting.
        Application.DoEvents();
        Thread.Sleep(100);
    }

    var SomeData = awaiter.GetResult();
    if (result == null) { return false; } // error

    // do something with result
    return true;
}

I have read up a bit on the negatives on using GetAwaiter().GetResult() and I'm searching for a better solution. I believe the reason this was done this way was so application events didn't block while Method1() executes since we are checking IsCompleted on the awaiter and once it is complete, we then get the result.

Is this a legit usage of GetAwaiter() and GetResult() or is there a better solution?

The above legacy code works and I am unsure as to whether this is a "good" solution to the problem or is a timebomb waiting to happen.

Clarification: The Method2 is called from a thread that was created using ThreadPool.QueueUserWorkItem() from inside the static Main() method, before the Application.Run() which is not called until after the thread terminates and is thus called on the same thread that entered the static Main() method. I have attempted to update the code with a simplified version of what I am dealing with that showcases the additional thread and when Application.Run() is invoked.

Purpose The purpose appears to be that when the application runs, it creates a thread that calls Method1 which basically does user login/authentication. Once authenticated, it attempts to connect to a server process using a JWT. The Main() thread is looping to wait for the connection thread to either succeed or error out while also calling Application.DoEvents(). I don't quite understand the purpose of the second thread.

rhoonah
  • 131
  • 6
  • 5
    Convert everything to async-await – Daniel A. White Jun 19 '23 at 22:22
  • @DanielA.White How does that solve the problem that this code appears to be concerned with of allowing the Windows events to run while waiting for the async method to complete? – rhoonah Jun 19 '23 at 22:27
  • @rhoonah Directly. Async-await allows the Windows events to run while waiting for the async method to complete by design. – GSerg Jun 19 '23 at 22:35
  • How is the `Method2` called? Is it an event handler of a button click or something? – Theodor Zoulias Jun 19 '23 at 22:49
  • Yes, this is a time bomb. Lots has been written about avoiding `DoEvents`. – Stephen Cleary Jun 20 '23 at 00:28
  • @TheodorZoulias It is actually called during application startup. – rhoonah Jun 21 '23 at 11:43
  • When exactly? Before or after calling the `Application.Run`? Inside the main `Form` constructor? In the `Load` event of the main `Form`? – Theodor Zoulias Jun 21 '23 at 12:54
  • @TheodorZoulias inside the static Main() method before Application.Run(). – rhoonah Jun 22 '23 at 12:49
  • In this case the `Application.DoEvents` does nothing. There is no Windows message loop running before the `Application.Run`. There is no `SynchronizationContext` installed either, so the `GetAwaiter().GetResult()` can't cause a deadlock. Your application at this point of its lifetime is basically a console application. – Theodor Zoulias Jun 22 '23 at 12:56
  • @TheodorZoulias Not sure you're right, I think `DoEvents` would still process ThreadPool queued tasks, which are also queued up via the message loop IIRC – Charlieface Jun 22 '23 at 13:03
  • @Charlieface what is the observable effect of the `Application.DoEvents` in a console application? How could you prove that the `Application.DoEvents` does anything? – Theodor Zoulias Jun 22 '23 at 13:11
  • 1
    @TheodorZoulias True without a `SynchronizationContext` it won't do anything. I must say, I can't seem to force a deadlock even when not calling `DoEvents`. – Charlieface Jun 22 '23 at 13:29
  • What is the intended behavior of your code? Do you want to execute the `Method1` before opening the main form of the WinForms application? Or you want to run the `Method1` in parallel with the opening of the main form? – Theodor Zoulias Jun 22 '23 at 14:21
  • @TheodorZoulias First, thank you for taking such an interest in this issue. I really appreciate it. Second, I have updated the sample to better reflect a simplified version of the code I am dealing with. This all predates me so I am not fully sure on the purpose of all of it but in general, the application runs and spins off a thread (ThreadProc ) that calls Method1 and then attempts to connect to server using a JWT returned from Method2. So Main() loops, waiting to either connect or fail prior to calling Application.Run() so it can error out and shutdown the process. – rhoonah Jun 22 '23 at 19:27
  • The `if (!Method2())` indicates that the `Method2` returns `bool`, but in the definition it's `void`. Also the `MyForm.Invoke` is confusing. Is this a custom `static` method `Invoke` on the class `MyForm`, or the known [`Invoke`](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.control.invoke) on an instance variable named `MyForm`? Anyway, my guess is that the original author wanted to call periodically an async method on the UI thread, and didn't know a simple way to do it. See [this](https://stackoverflow.com/questions/30462079/) question for ideas. – Theodor Zoulias Jun 23 '23 at 00:06
  • @TheodorZoulias Apologies I was mixing my original attempt at simplified code with my updated more complete code. Method2 does return a bool and MyForm was a dummy variable. The code actually uses Application.OpenForms[0] to execute the Invoke(). – rhoonah Jun 23 '23 at 03:09
  • The splash screen is an important part of your scenario. By leaving it out, you handicapped your question to a point that it's not understandable, let alone answerable. Considering that there is a splash screen in the mix, the `Application.DoEvents()` starts to make sense. I am not sure that your question is salvageable at this point. – Theodor Zoulias Jun 23 '23 at 03:23
  • You could consider forgetting your existing code, open a new question, and ask how to implement from scratch the desirable functionality described in details. I assume that you want to show a splash screen, do something for a while, and then show the main form. You must be very specific about this "something", if you hope to get any useful advice. – Theodor Zoulias Jun 23 '23 at 03:23

1 Answers1

1

No this is not the right way to go about this. You are running an infinite loop on a UI thread, putting it to sleep for 100ms, and then forcing it to wake and handle events then putting it to sleep again.

It is not a "time-bomb waiting to happen", but it is an inefficient method of waiting.

Instead, just use either async Task or async void which handles the waiting for you, but does not block the UI thread at all. It puts the continuation of the method (everything after the await) on a callback which only gets called once the await completes.

Note that async void should only be used in event handlers coming off the UI, otherwise use async Task and friends.

private static async Task Method2()
// alternatively
private static async void Method2()
{
    var data = await Method1();
    // do something with result
}
Charlieface
  • 52,284
  • 6
  • 19
  • 43
  • Seems reasonable. I will take a look at changing things to your solution. Thank you. – rhoonah Jun 21 '23 at 11:42
  • 1
    No it doesn't, they should still do the above, except they should change their `Main` to *also* be `async Task`. – Charlieface Jun 22 '23 at 13:02
  • Why make the `Main` `async Task`? You have defined the `Method2` as `async void`, so it can't be awaited. What are you going to `await` in the `Main` then? – Theodor Zoulias Jun 22 '23 at 13:26
  • 1
    I already wrote *"Note that async void should only be used in event handlers coming off the UI, otherwise use async Task and friends."* Edited to make it clearer – Charlieface Jun 22 '23 at 13:27
  • Hmm, I agree with this. Your answer is against this advice though, since it advises and endorses, primarily, the use of `async void` in a non-event-handler method. Hence my downvote. – Theodor Zoulias Jun 22 '23 at 13:31
  • I said, I put a note in warning when it should be used (OP had not made clear when I posted as to what they were doing, it looked like an event handler at first glance). OK happy now??? – Charlieface Jun 22 '23 at 13:34
  • The `Method2` doesn't look even remotely like an event handler. There is no `(object sender, EventArgs e)` in sight. IMHO, in view of the recent clarifications by the OP, suggesting `async void` even as a secondary option is downvotable. – Theodor Zoulias Jun 22 '23 at 13:55
  • OP said "I have run into some legacy code Winforms code" WinForms code is nearly always an event handler or called from an event handler, so wasn't such a jump. Not withstanding the fact they have now said in comments rather than the actual post, that it was actually called from `Main` and the `Application.Run` hadn't been called, so it's actually nothing to do with WinForms at all. Honestly, the question *as asked* was answered, if OP wants to move the goalposts that's not my fault. You're welcome to go [edit] the question *and* answer together to make it something different if you like. – Charlieface Jun 22 '23 at 14:00
  • I edited the question, but I am not planning to answer it because the OP's intentions are still unclear to me. – Theodor Zoulias Jun 22 '23 at 14:13
  • Thanks for the comments and I apologize for the incomplete question with no intent on "moving te goalposts" but as I stated originally, this is not code that I wrote and is something that I simply ran across and was looking for clarification or a better solution. I appreciate the comments but we can move on and call it a day. – rhoonah Jun 23 '23 at 19:12