简体   繁体   中英

Application.DoEvents vs await Task.Delay in a loop

Much to my discontent, I need to use a WebBrowser control in one of my apps.

One of the things I also need to do is wait for an element to become visible/class changes/etc, which happens well after the DocumentCompleted event is fired, making the event close to useless in my case.

So currently I have something like...

while (webBrowser.Document?.GetElementById("id")?.GetAttribute("classname") != "class")
{
    Application.DoEvents();
    Thread.Sleep(1);
}

Now I've read in multiple places that DoEvents() is evil and can cause lots of issues, so I thought about replacing it with Task.Delay() as such:

while (webBrowser.Document?.GetElementById("id")?.GetAttribute("classname") != "class")
{
    await Task.Delay(10);
}

So my question is, other than the obvious facts that the Thread.Sleep() will block events for 1ms and that the Task.Delay() has a bigger delay set in the example above, what are the actual differences between doing the two approaches, which is better and why?

PS: Please stick to the question, while I wouldn't necessarily mind other ideas on how to fix the WebBrowser control issue itself by using something else (js injection comes to mind), this is not the place to answer that, this question is about how these two bits of code differ and which would be considered better.

what are the actual differences between doing the two approaches, which is better and why?

The differences are in how messages are processed while waiting.

DoEvents will install a nested message loop; this means that your stack will have (at least) two message processing loops. This causes reentrancy issues , which IMO is the biggest reason to avoid DoEvents . There's endless questions about which kinds of events the nested message loop should process, because there's deadlocks on both sides of that decision, and there's no solution that's right for all applications. For an in-depth discussion of message pumping, see the classic Apartments and Pumping in the CLR blog post.

In contrast, await will return . So it doesn't use a nested message loop to process messages; it just allows the original message loop to process them. When the async method is ready to resume, it will send a special message to the message loop that resumes executing the async method.

So, await enables concurrency but without all the really difficult reentrancy concerns inherent in DoEvents . await is definitely the superior approach.

Basically there's an ongoing argument that DoEvents() is "better" because it doesn't consume any threads from the thread pool

Well, await doesn't consume any threads from the thread pool, either. Boom.

While await Task.Delay(10) is executing the UI can process events. While Thread.Sleep(1); is running the UI is frozen. So the await version is better and will not cause any major issues.

Clearly, spinning in a busy loop has the risk of burning CPU and battery. You seem to be aware of those drawbacks.

My 2 cents on this one:

Basically there's an ongoing argument that DoEvents() is "better" because it doesn't consume any threads from the thread pool and that in this case since I'm waiting for a control that seems like the right thing to do.

Broadly speaking, the WebBrowser control needs the UI thread to do some bits of its parsing/rendering job. By spinning a busy-waiting loop like yours with Application.DoEvents(); Thread.Sleep(1) Application.DoEvents(); Thread.Sleep(1) on the UI thread, you essentially make this thread less available to other UI controls (including the WebBrowser itself). So, the polling operation from you example will most likely take longer to finish , than when you use an asynchronous loop with Task.Delay .

Moreover, the use of the thread pool by Task.Delay is not an issue here. A thread pool thread gets engaged here for a very short time, only to post an await completion message to the synchronization context of the main thread. That's because you don't use ConfigureAwait(false) here (correctly in this case, as you indeed need to do you polling logic on the UI thread).

If nevertheless you're still concerned about the use the thread pool, you can easily replicate Task.Delay with System.Windows.Forms.Timer class (which uses a low-priority WM_TIMER message ) and TaskCompletionSource (to turn the Timer.Tick even into an awaitable task).

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