Don’t use Suspend and Resume, but don’t poll either.

http://www.paradicesoftware.com/blog/2014/02/dont-use-suspend-and-resume-but-dont-poll-either/

Don’t use Suspend and Resume, but don’t poll either.

Category: Code, Good coding guidelines / Tag: event, FPC, queue, resume, sleep, suspend, thread, worker /Add Comment
 

So, using thread Suspend and Resume functionality is deprecated in Delphi, Freepascal, and even Windows MSDN itself warns against using it for synchronization.

There are good reasons for trying to kill this paradigm: suspending and resuming other threads from the one you’re currently on is a deadlock waiting to happen, and it’s typically not supported at all in OS’es other that Windows. The only circumstance where it’s needed is to start execution of a thread that was initially created suspended (to allow additional initialization to take place). This is still supported and a new command has been added to FPC/Delphi called “TThread.Start” which implements this.

However, a number of people are confused about how to correctly re-implement their “worker thread” without using suspend/resume; and some of the advice given out hasn’t been that great, either.

Let’s say you have a worker thread which normally remains suspended. When the main thread wants it to do something, it pushes some parameters somewhere and then resumes the thread. When the thread is complete, it suspends itself. (note: critical sections or other access protection on the “work to do” data needs to be here too, but is removed for clarity):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{the worker thread's execution}
procedure TMyThread.Execute;
begin
   repeat
      if (work_to_do) then
         //...do_some_work...
      else
         Suspend;
   until Terminated;
end;
 
{called from the main thread:}
procedure TMyThread.QueueWork(details);
begin
   //...add_work_details...
   if Suspended then
      Resume;
end;

Although the particular example above still works, now’s a good time to go ahead and clean this up so that you’re not depending on deprecated functions.

Here’s where we get to the inspiration for today’s post. The suggested ‘clean up’ is often implemented using polling. Let’s take something I saw suggested on * as a replacement for the above:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
procedure TMyThread.Execute;
begin
   repeat
      if (work_to_do) then
         //...do_some_work...
      else
         Sleep(10);
   until Terminated;
end;
 
procedure TMyThread.QueueWork(details);
begin
   //...add_work_details...
end;

Yuck! What’s the problem with this design? It’s not particularly “busy”, since it sleeps all the time, but there are issues. Firstly: if the thread is idle, it can be 10-milliseconds before it gets around to realizing there’s any work to do. Depending on your application that may or may not be a big deal, but it’s not exactly elegant.

Secondly (and this is the bigger one for me), this thread is going to eat 200 context switches per second (2 per 10ms), whether busy or not. A far worse design than the original! Context switches aren’t free! If we assume 50,000 nanoseconds per context switch (0.05ms), which seems a reasonable finding, 200 of them per second just ate 1% of the total capacity of a processor core, to achieve nothing except wait. There’s a better solution, right?

Use Event Objects

Fortunately, there are better ways than Sleeping and Polling. The best replacement for the above scenario is just to deploy an event. Events can be “signalled” or “nonsignalled”, and you can let the operating system know “hey, I’m waiting for this event”. It will then go away and not waste any more cycles on you until the event is signalled. Brilliant! How do you do this? Well, it depends on your language:

  • Win32 itself exposes event handles (see CreateEvent) which can be waited on with the WaitFor family of calls
  • Freepascal provides a TEventObject, which encapsulates the Win32 (or other OS equivalent) functionality
  • Delphi uses TEvent, which does the same thing
  • C# uses System.Threading.ManualResetEvent (and related)

Here’s how to rewrite the above handler using a waitevent, so it consumes no CPU cycles until an event arrives. (I’ll use the FPC mechanism, but they’re functionally identical in all the other languages).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
constructor TMyThread.Create;
begin
   //...normal initialization stuff...
   mEvent := TEventObject.Create(nil,true,false,'');
end;
 
{the worker thread's execution}
procedure TMyThread.Execute;
begin
   repeat
      mEvent.WaitFor(INFINITE);
      mEvent.ResetEvent;
      if (work_to_do) then
         //...do_some_work...
 until Terminated;
end;
 
{called from the main thread:}
procedure TMyThread.QueueWork(details);
begin
 //...add_work_details...
 mEvent.SetEvent;
end;

Presto! Thread will happily wait until a new piece of work comes in, without consuming any CPU cycles at all, and it will respond immediately once there’s something to do. The only caveat on this design? The only way out of the .WaitFor() call is for the event to be signalled, so you also need to account for this when you want to terminate the thread for good. (note that FPC’s TThread.Terminate isn’t virtual, so we have to cast to TThread to get the correct call):

1
2
3
4
5
6
7
8
procedure TMyThread.Terminate;
begin
   // Base Terminate method (to set Terminated=true)
   TThread(self).Terminate;
 
   // Signal event to wake up the thread
   mEvent.SetEvent;
end;

Sorted!

上一篇:IIS应用程序池配置详解及优化


下一篇:c#-连接Console.Beep声音的方法