Pregunta

The following program hangs on the DoTheStuff().Wait(); line, if running as a Console application:

namespace Test
{
    using System.Threading.Tasks;
    using System.Windows.Forms;

    class Program
    {
        static void Main(string[] args)
        {
            new Form();
            DoTheStuff().Wait();
        }

        private static async Task DoTheStuff()
        {
            await Task.Delay(1000);
        }
    }
}

It works just as expected though, if you comment out the new Form(); line. (Runs for 1sec, then quits). How can I keep the expected behaviour and still have a Form instance?

Now, some background if you are interested:

I have an application which is hosted as a windows service (as console when testing locally).

It requires to have access to the SystemEvents.TimeChanged event.

However, as per the documentation, this only works when having a windows Form (thus not in a service or console app). A workaround is presented in the linked documentation, and consists of creating a hidden form.

Unfortunately, the program now completely freezes instead, which is caused by the combination of await and having a Form instance.

So how on earth can I still have the expected async/await behaviour while accessing the SystemEvents.TimeChanged event?


Thanks to help below, here comes modified code which works without freeze:

namespace Test
{
    using System.Threading;
    using System.Threading.Tasks;
    using System.Windows.Forms;

    class Program
    {
        static void Main(string[] args)
        {
            new Thread(() => Application.Run(new Form())).Start();
            // SynchronizationContext.SetSynchronizationContext(null);
            DoTheStuff().Wait();
        }

        private static async Task DoTheStuff()
        {
            await Task.Delay(1000);
        }
    }
}

In my program, I need to use "SynchronizationContext.SetSynchronizationContext(null);", since the threadpool should be used for awaiting tasks. I don't think that is a good practice, since Form obviously initialized it for a reason. But running the form hidden without user input (it is a service!), and can't see any harm right now.

The documentation feels a bit incomplete, with MS not even mentioning the issue that may arise using the example 2 (await/async implicitly changes behaviour when instantiating a Form).

¿Fue útil?

Solución

This is by design. Creating a new Form object get the Winforms plumbing to install a new SynchronizationContext. Something you can see in the debugger by looking at the SynchronizationContext.Current property.

That property is big deal whenever you do anything asynchronously. If it is null, the default, then using await gets code to run on threadpool threads. If it is not then the await plumbing will implement the await by calling the SynchronizationContext.Post() method. Which ensures that your code runs on the main thread.

But that doesn't work in your program because you broke the contract. You didn't call Application.Run(). Required.

The SystemEvents class will create its own hidden notification window and pumps a message loop if you don't provide one. No need to create a form. The consequence is that its events will fire on an arbitrary thread, not your main thread. So do watch out for locking requirements.

Otros consejos

The call to Wait causes a deadlock, as I explain on my blog and in a recent MSDN article.

In your case, you could use a simple MainAsync as such:

static void Main(string[] args)
{
    MainAsync().Wait();
}

static async Task MainAsync()
{
    new Form();
    await DoTheStuff();
}

private static async Task DoTheStuff()
{
    await Task.Delay(1000);
}

However, there are a couple of problems here. First, when you create a form (even a hidden form), you are expected to run an STA event loop, e.g., Application.Run. Second, as a Win32 service, you are expected to give your main thread back to the SCM, e.g., ServiceBase.Run.

So I would recommend a solution where you create a form and run an event loop on a secondary thread.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top