dead-lock free design

Joerg-Cyril.Hoehle at Joerg-Cyril.Hoehle at
Fri Jun 17 10:49:52 CDT 2011


there are two guidelines that I identified while trying to prevent
dead-locks with critical sections and access to uninitialiased memory
in Wine.  My latest patch shows both applications.

1. In callbacks, detect when it should not run anymore and exit *early*.

void CALLBACK mycallback (void* user) {
    struct mycbdata * mycb = user;
    if(!mycb->started || mycb->shutdown)
        { LeaveCriticalSection; return; }
    /* else do actual work */

This is best done after EnterCS, because the critical section is the
most likely place where the callback thread will have been preempted.

Note that this doesn't prevent mycb from pointing to free'ed memory.
You may only deallocate resources once you've made sure that the
callback cannot be invoked any more.

2. Use two phases to end cleanly.  Delegate part of the work to a later stage.
   Pause is not Stop.
   Stop is not Close.
   Close is not Shutdown.
   Shutdown is not DLL Unload.

My current patch applies this idea partially.
DeleteTimerQueueTimer() is called within a CS, whereas Wait() is
called afterwards (which is safe because the mmdevapi object is not
used any more).  The original deadlock happened because
DeleteTimerQueue and Wait were combined in one call, within the CS.

Waiting for completion is *essential* with the DeleteTimerQueueTimer
API because that's the only means to ensure that callbacks are done.
Then only can you free the memory that mycb above points to.

A variation is to partition the work among AudioClient_Stop and
AudioClient_Release.  In Stop() call DeleteTimerQueueTimer() but Wait
for completion of the callbacks in Release().

Alternatively, in Stop() use ChangeTimerQueueTimer(DueTime=INFINITE)
to prevent firing new callbacks and use DeleteTimerQueue in Release().

I didn't write my patch that way yet because it requires some
attention in AudioClient_Start() when restarting the timer callbacks.

Why does the rule work?  Functions likes Play/Pause/Stop
typically prevent concurrent access (either from the app or from
internal callback threads) using critical sections.  At Close/Release
time, the typical assumption is that there's no more concurrency
-- at least from the side of the application.

Partitioning the work allows for plenty of new possibilities.
E.g. one known bug in MS-Windows is that MCI/winmm's Stop() may take a
lot of time, freezing the application.  However, if Stop() simply
sends a stop signal to the player thread, without waiting for the
thread to actually stop, it can be plenty fast.  Likewise, if
mmdevapi's AudioClient_Stop() simply sets This->started to FALSE, the
callback will still be ticking, but not disturb.  Fast too if it calls
DeleteTimerQueueTimer() but delegates the Wait to AudioClient_Release().

One drawback is that resources will be allocated for longer than with
the naïve approach.

Sometimes, the partitioning decision depends on what state you want to
guarantee after Pause, Stop or Close.  This is rarely documented.
E.g. for MCI's audio player you want to make sure that an immediately
following Open won't fail when Wine's winmm ALSA or OSS drivers only
support opening one device at a time.

	Jörg Höhle

More information about the wine-devel mailing list