With Windows Vista we got a new tool for communicating over multiple threads: condition variables. I’ll try to give you a short overview, tell you, why it is better then events and illustrate the whole theory with some code snippets. Hopefully it will be funny.  :)

What is a conditional variable?

Condition variables are nothing new. They are alread long known in the Unix world, and there are some thread frameworks already offering that feature. But since Windows Vista we have native support for them from the API.

Condition variables are a combination of an event and a corresponding lock. And that’s where their big advantage lies: With their help you can wait for an event and enter a critical section for example in one atomic operation. This avoids race conditions. At the same time condition variables save resources. In contrast to wait-for procedures they try to avoid trips to kernel mode when possible.

Usage

Usage of condition variables is relatively simple. There are just five functions you have to use. Additionally they follow the new course that they don’t have to be freed by the application. But foremost they have to be initialized. And that’s quite straight forward:

var   
  CondVar : TConditionVariable;
begin
  InitializeConditionVariable(CondVar);
end

You need not be irritated by the content of the TConditionVariable record. In most cases the Ptr variable is nil after initialization. It works nonetheless.

A word of caution (born out of painful experience): TConditionVariable is a record. But it is not the content of the variable that is important but its address in memory. Therefore a simple assignment of CondVar2:=CondVar is no usable condition variable anymore.

Now imagine two threads which work in the same list. One is the producer filling the list. The other one is the consumer taking the items out again.

var
  cvListFilled : TConditionVariable;
  CommonList : TList;
  ListLock : TCriticalSection;

procedure TProducer.Execute;
begin
  while not Terminated do
  begin
    EnterCriticalSection(ListLock);
    try
      CommonList.Add(SomeData);
    finally
      LeaveCriticalSection(ListLock);
    end;

    WakeConditionVariable(cvListFilled);
    Sleep(500); //simulate some work
  end;
end;

procedure TConsumer.Execute;
begin
  while not Terminated do
  begin
    EnterCriticalSection(ListLock);
    try
      while CommonList.Count = 0 do
      begin
        SleepConditionVariableCS(cvListFilled, ListLock, INFINITE);
      end;

      CommonList.Clear;
    finally
      LeaveCriticalSection(ListLock);
    end;

    Sleep(100); //simulate some work 
  end;
end;

The TProducer’s code ist not really surprising. The only really interesting point is WakeConditionVariable(cvListFilled); which wakes the next thread which listens to cvListFilled.

TConsumer is much more interesting. After entering the critical section it checks whether there are items that have to be processed at all. If not SleepConditionVariableCS is called. This call leaves the CriticalSection and puts the thread in sleeping state until the timeout is reached or someone calls a wake procedure. After the sleep call the CriticalSection is entered automatically again. At this point the condition should be checked again since the wake-up call could have caused other threads to do work and change the condition. In our example this could happen if there are more than one consumer and WakeAllConditionVariable is used.

But what happens when there are more than one TConsumer but you only call WakeConditionVariable. Each call of WakeConditionVariable will wake the next thread in line until all TConsumer threads are called. Then a new round starts. As its name says WakeAllConditionVariable will wake up all threads in line at once.

And now?

All neccessary types and functions are available in the Jedi API svn-trunk. An usage example is TWaitList.

Thus it remains only to say: Do it!