//+--------------------------------------------------------------------------- // // Microsoft Windows // Copyright (C) Microsoft Corporation, 1992 - 1997. // // File: thdpool.c // // Contents: Home of the SPM thread pool // // Classes: // // Functions: // // History: 6-08-93 RichardW Created // //---------------------------------------------------------------------------- #include #define POOL_SEM_LIMIT 0x7FFFFFFF #define MAX_POOL_THREADS_HARD 256 #define MAX_SUBQUEUE_THREADS (4 * MAX_POOL_THREADS_HARD) LSAP_TASK_QUEUE GlobalQueue; // // Local Prototypes: // typedef enum _LSAP_TASK_STATUS { TaskNotQueued, TaskQueued, TaskUnknown } LSAP_TASK_STATUS ; LSAP_TASK_STATUS EnqueueThreadTask( PLSAP_TASK_QUEUE pQueue, PLSAP_THREAD_TASK pTask, BOOLEAN fUrgent); #define LockQueue(q) EnterCriticalSection(&((q)->Lock)) #define UnlockQueue(q) LeaveCriticalSection(&((q)->Lock)) //+--------------------------------------------------------------------------- // // Function: InitializeTaskQueue // // Synopsis: Initialize a Queue Structure // // Arguments: [pQueue] -- // [Type] -- // // History: 11-10-95 RichardW Created // // Notes: // //---------------------------------------------------------------------------- BOOL InitializeTaskQueue( PLSAP_TASK_QUEUE pQueue, LSAP_TASK_QUEUE_TYPE Type) { OBJECT_HANDLE_FLAG_INFORMATION FlagInfo ; RtlZeroMemory( pQueue, sizeof(LSAP_TASK_QUEUE) ); InitializeListHead( &pQueue->pTasks ); pQueue->Type = Type; pQueue->hSemaphore = CreateSemaphore(NULL, 0, POOL_SEM_LIMIT, NULL); __try { InitializeCriticalSectionAndSpinCount( &pQueue->Lock, 5000 ); } __except (EXCEPTION_EXECUTE_HANDLER) { if ( pQueue->hSemaphore ) { NtClose( pQueue->hSemaphore ); pQueue->hSemaphore = NULL ; } } if (!pQueue->hSemaphore) { DebugLog((DEB_ERROR, "Could not create semaphore, %d\n", GetLastError() )); return( FALSE ); } FlagInfo.Inherit = FALSE ; FlagInfo.ProtectFromClose = TRUE ; NtSetInformationObject( pQueue->hSemaphore, ObjectHandleFlagInformation, &FlagInfo, sizeof( FlagInfo ) ); pQueue->StartSync = CreateEvent( NULL, TRUE, FALSE, NULL ); if ( pQueue->StartSync == NULL ) { DebugLog(( DEB_ERROR, "Could not create start sync event\n" )); CloseHandle( pQueue->hSemaphore ); return FALSE ; } return( TRUE ); } //+--------------------------------------------------------------------------- // // Function: InitializeThreadPool // // Synopsis: Initializes necessary data for the thread pool // // Arguments: (none) // // History: 7-13-93 RichardW Created // // Notes: // //---------------------------------------------------------------------------- BOOL InitializeThreadPool(void) { if (!InitializeTaskQueue(&GlobalQueue, QueueShared)) { return( FALSE ); } LsaIAddTouchAddress( &GlobalQueue, sizeof( GlobalQueue ) ); return(TRUE); } //+--------------------------------------------------------------------------- // // Function: QueueAssociateThread // // Synopsis: Associates the thread with the queue // // Arguments: [pQueue] -- // // History: 11-09-95 RichardW Created // // Notes: // //---------------------------------------------------------------------------- VOID QueueAssociateThread( PLSAP_TASK_QUEUE pQueue) { PSession pSession; LockQueue( pQueue ); pQueue->TotalThreads++ ; #if 0 // // We were already idle, so decrement this so the later call // to wait for thread task will leave the count correct. // DsysAssert(pQueue->IdleThreads > 0); pQueue->IdleThreads--; #endif // // Update the statistics: // if ( pQueue->MaxThreads < pQueue->TotalThreads ) { pQueue->MaxThreads = pQueue->TotalThreads ; } UnlockQueue( pQueue ); } //+--------------------------------------------------------------------------- // // Function: QueueDisassociateThread // // Synopsis: Disconnects a thread and a queue // // Arguments: [pQueue] -- // [pLastThread] -- OPTIONAL flag indicating last thread of queue // // History: 11-09-95 RichardW Created // // Notes: // //---------------------------------------------------------------------------- BOOL QueueDisassociateThread( PLSAP_TASK_QUEUE pQueue, BOOLEAN * pLastThread) { PSession pSession; LockQueue( pQueue ); DsysAssert(pQueue->TotalThreads > 0); if ( pQueue->TotalThreads == 1 ) { if ( pLastThread ) { *pLastThread = TRUE ; // // If the queue is being run down, set the // event since we are the last thread // if ( pQueue->Type == QueueZombie ) { SetEvent( pQueue->StartSync ); } } if ( pQueue->Tasks ) { // // Make sure that we never have more tasks queued // to a zombie // DsysAssert( pQueue->Type != QueueZombie ); UnlockQueue( pQueue ); return FALSE ; } } pQueue->TotalThreads--; UnlockQueue( pQueue ); return TRUE ; } //+--------------------------------------------------------------------------- // // Function: DequeueAnyTask // // Synopsis: Returns a task from this queue or any shared, if available // // Arguments: [pQueue] -- // // Requires: pQueue must be locked! // // History: 11-09-95 RichardW Created // // Notes: // //---------------------------------------------------------------------------- PLSAP_THREAD_TASK DequeueAnyTask( PLSAP_TASK_QUEUE pQueue) { PLSAP_THREAD_TASK pTask; PLSAP_TASK_QUEUE pShared; PLSAP_TASK_QUEUE pPrev; if ( !IsListEmpty(&pQueue->pTasks) ) { pTask = (PLSAP_THREAD_TASK) RemoveHeadList(&pQueue->pTasks); pQueue->Tasks --; pQueue->TaskCounter++; // // Reset the pointers. This is required by the recovery logic // in the Enqueue function below. // pTask->Next.Flink = NULL ; pTask->Next.Blink = NULL ; return( pTask ); } // // No pending on primary queue. Check secondaries: // if (pQueue->Type == QueueShared) { pShared = pQueue->pShared; while ( pShared ) { DWORD WaitStatus; // // We need to wait now to change the semaphore count // WaitStatus = WaitForSingleObject(pShared->hSemaphore, 0); LockQueue( pShared ); if ((WaitStatus == WAIT_OBJECT_0) && !IsListEmpty(&pShared->pTasks) ) { pTask = (PLSAP_THREAD_TASK) RemoveHeadList(&pShared->pTasks); pShared->Tasks--; pShared->TaskCounter++; UnlockQueue( pShared ); // Unlock shared queue return( pTask ); } pPrev = pShared; pShared = pShared->pNext; UnlockQueue( pPrev ); } } return( NULL ); } //+--------------------------------------------------------------------------- // // Function: WaitForThreadTask // // Synopsis: Function called by queue waiters // // Arguments: [pQueue] -- Queue to wait on // [TimeOut] -- timeout in seconds // // History: 11-10-95 RichardW Created // // Notes: // //---------------------------------------------------------------------------- PLSAP_THREAD_TASK WaitForThreadTask( PLSAP_TASK_QUEUE pQueue, DWORD TimeOut) { PLSAP_THREAD_TASK pTask; PLSAP_TASK_QUEUE pShared; PLSAP_TASK_QUEUE pPrev; int WaitResult; LockQueue( pQueue ); pTask = DequeueAnyTask( pQueue ); if (pTask) { UnlockQueue( pQueue ); return( pTask ); } // // No pending anywhere. // if (TimeOut == 0) { UnlockQueue( pQueue ); return( NULL ); } // // Principal of the loop: We do this loop so long as we were awakened // by the semaphore being released. If there was no task to pick up, then // we go back and wait again. We return NULL only after a timeout, or an // error. // do { pQueue->IdleThreads++; UnlockQueue( pQueue ); WaitResult = WaitForSingleObject( pQueue->hSemaphore, TimeOut ); LockQueue( pQueue ); // // In between the wait returning and the lock succeeding, another // thread might have queued up a request. DsysAssert(pQueue->IdleThreads > 0); pQueue->IdleThreads--; // // In between the wait returning and the lock succeeding, another // thread might have queued up a request, so don't blindly // bail out. Check the pending count, and skip over this // exit path if something is there // if ( pQueue->Tasks == 0 ) { if (WaitResult != WAIT_OBJECT_0) { UnlockQueue( pQueue ); if ( WaitResult == -1 ) { DebugLog((DEB_ERROR, "Error on waiting for semaphore, %d\n", GetLastError())); } return( NULL ); } } // // If the queue type is reset to Zombie, then this queue is // being killed. Return NULL immediately. If we're the last // thread, set the event so the thread deleting the queue will // wake up in a timely fashion. // if ( pQueue->Type == QueueZombie ) { return NULL ; } pTask = DequeueAnyTask( pQueue ); if (pTask) { UnlockQueue( pQueue ); return( pTask ); } // // Track number of times we woke up but didn't have anything to do // pQueue->MissedTasks ++ ; } while ( WaitResult != WAIT_TIMEOUT ); UnlockQueue( pQueue ); return( NULL ); } //+--------------------------------------------------------------------------- // // Function: SpmPoolThreadBase // // Synopsis: New Pool Thread Base // // Arguments: [pvQueue] -- OPTIONAL queue to use // // History: 11-09-95 RichardW Created // // Notes: // //---------------------------------------------------------------------------- DWORD SpmPoolThreadBase( PVOID pvSession) { PLSAP_THREAD_TASK pTask; PLSAP_TASK_QUEUE pQueue; PSession pSession; BOOLEAN ShrinkWS = FALSE; DWORD dwResult; DWORD Timeout; PSession ThreadSession ; PSession OriginalSession ; OriginalSession = GetCurrentSession(); if ( pvSession ) { ThreadSession = (PSession) pvSession ; SpmpReferenceSession( ThreadSession ); SetCurrentSession( ThreadSession ); } else { ThreadSession = OriginalSession ; SpmpReferenceSession( ThreadSession ); } pQueue = ThreadSession->SharedData->pQueue ; if (!pQueue) { pQueue = &GlobalQueue; } QueueAssociateThread( pQueue ); // // Share pool threads have short lifespans. Dedicated single, or // single read threads have infinite life span. // if (pQueue->Type == QueueShared) { Timeout = LsaTuningParameters.ThreadLifespan * 1000; } else { // // If we are the dedicated thread for this queue, the timeout // is infinite. If we are a temporary thread, the timeout is // the subqueue // if ( ThreadSession->ThreadId != GetCurrentThreadId() ) { Timeout = LsaTuningParameters.SubQueueLifespan ; } else { Timeout = INFINITE ; } } if ( pQueue->StartSync ) { DebugLog(( DEB_TRACE, "ThreadPool: Signaling start event\n" )); // // If a queue was passed in, the caller of CreateXxxQueue is blocked // waiting for us to strobe the start sync event. // SetEvent( pQueue->StartSync ); } while ( TRUE ) { pTask = WaitForThreadTask( pQueue, Timeout ); if ( pTask ) { SetCurrentSession( pTask->pSession ); dwResult = pTask->pFunction(pTask->pvParameter); #if DBG RtlCheckForOrphanedCriticalSections( NtCurrentThread() ); #endif SetCurrentSession( ThreadSession ); // // The session in this task was referenced during the AssignThread call. // This dereference cleans that up. // SpmpDereferenceSession(pTask->pSession); LsapFreePrivateHeap( pTask ); } else { // // We can never leave the queue empty of threads if // there are still tasks pending. QueueDisassociateThread // will fail if there are tasks pending. In that case, // skip up to the top of the loop again. // if ( !QueueDisassociateThread( pQueue, &ShrinkWS ) ) { continue; } // // Now that we are not part of that queue, reset our thread session // SpmpDereferenceSession( ThreadSession ); SetCurrentSession( OriginalSession ); if ( LsaTuningParameters.Options & TUNE_TRIM_WORKING_SET ) { if (ShrinkWS) { LsaTuningParameters.ShrinkOn = TRUE ; LsaTuningParameters.ShrinkCount++; SetProcessWorkingSetSize( GetCurrentProcess(), (SIZE_T)(-1), (SIZE_T)(-1) ); } else { LsaTuningParameters.ShrinkSkip++; } } DebugLog(( DEB_TRACE, "No tasks pending on queue %x, thread exiting\n", pQueue )); return( 0 ); } } return( 0 ); } //+--------------------------------------------------------------------------- // // Function: EnqueueThreadTask // // Synopsis: Enqueue a task, update counts, etc. // // Arguments: [pTask] -- Task to add // // History: 7-13-93 RichardW Created // // Notes: // //---------------------------------------------------------------------------- LSAP_TASK_STATUS EnqueueThreadTask( PLSAP_TASK_QUEUE pQueue, PLSAP_THREAD_TASK pTask, BOOLEAN fUrgent) { BOOLEAN NeedMoreThreads = FALSE; HANDLE hQueueSem ; HANDLE hParentSem = NULL ; PLSAP_TASK_QUEUE pThreadQueue = NULL; PSession pSession = NULL; LockQueue(pQueue); if ( pQueue->Type == QueueZombie ) { UnlockQueue( pQueue ); return TaskNotQueued ; } if (fUrgent) { InsertHeadList( &pQueue->pTasks, &pTask->Next ); } else { InsertTailList( &pQueue->pTasks, &pTask->Next ); } pQueue->Tasks++; pQueue->QueuedCounter++; if ( pQueue->Tasks > pQueue->TaskHighWater ) { pQueue->TaskHighWater = pQueue->Tasks ; } if (pQueue->Type == QueueShared) { if ((pQueue->Tasks > pQueue->IdleThreads) && (pQueue->TotalThreads < MAX_POOL_THREADS_HARD)) { NeedMoreThreads = TRUE; pThreadQueue = pQueue; } hParentSem = NULL ; } else if (pQueue->Type == QueueShareRead ) { DsysAssert( pQueue->pOriginal ); // // Here's the race potential. If the queue we have has no idle thread, // then make sure there is an idle thread at the parent queue, otherwise // we can deadlock (e.g. this call is in response to the job being executed // by the dedicated thread. Of course, this can also be a problem correctly // determining the parent queue's status, since locks should always flow down, // not up. So, if the number of jobs that we have pending exceeds the number // of idle threads, *always*, regardless of the other queue's real state or // total threads, queue up another thread. // if ( pQueue->Tasks > pQueue->IdleThreads ) { NeedMoreThreads = TRUE ; pSession = pTask->pSession ; if ( pQueue->TotalThreads < MAX_SUBQUEUE_THREADS ) { pThreadQueue = pQueue ; } else { pThreadQueue = pQueue->pOriginal; } } // // This is a safe read. The semaphore is not subject to change after creation, // and the worst that can happen is a bad handle. // hParentSem = pQueue->pOriginal->hSemaphore ; } hQueueSem = pQueue->hSemaphore ; UnlockQueue( pQueue ); // // Kick our semaphore. // ReleaseSemaphore( hQueueSem, 1, NULL ); // // Kick the parent semaphore // if ( hParentSem ) { ReleaseSemaphore( hParentSem, 1, NULL ); } if (NeedMoreThreads) { HANDLE hThread; DWORD tid; DebugLog((DEB_TRACE_QUEUE, "Queue %x needs more threads\n", pQueue)); // // Increment the number of threads now so we don't create more threads // while we wait for the first one to be created. // //// LockQueue(pThreadQueue); #if 0 pThreadQueue->TotalThreads++; pThreadQueue->IdleThreads++; // // Update the statistics: // if ( pThreadQueue->MaxThreads < (LONG) pThreadQueue->TotalThreads ) { pThreadQueue->MaxThreads = (LONG) pThreadQueue->TotalThreads ; } #endif //// pThreadQueue->ReqThread++; //// UnlockQueue(pThreadQueue); InterlockedIncrement( &pThreadQueue->ReqThread ); // // If the queue is a dedicated queue, supply the session from the task. // if the queue is a shared (global) queue, pass in NULL: // hThread = LsapCreateThread( NULL, 0, SpmPoolThreadBase, (pThreadQueue->Type == QueueShareRead ? pThreadQueue->OwnerSession : NULL ), 0, &tid); // // Check for failure // if (hThread == NULL) { #if 0 LockQueue(pThreadQueue); DsysAssert(pThreadQueue->TotalThreads > 0); DsysAssert(pThreadQueue->IdleThreads > 0); pThreadQueue->TotalThreads--; pThreadQueue->IdleThreads--; UnlockQueue(pThreadQueue); #endif // // This is extremely painful. The thread creation attempt // failed, but because of the nature of the queue, we don't // know if it was picked up and executed, or it was dropped, // or anything about it. // return TaskUnknown ; } else { NtClose(hThread); } } return TaskQueued ; } //+--------------------------------------------------------------------------- // // Function: SpmAssignThread // // Synopsis: Assigns a task to a thread pool thread. // // Arguments: [pFunction] -- Function to execute // [pvParameter] -- Parameter to function // [pSession] -- Session to execute as // // History: 11-24-93 RichardW Created // // Notes: // //---------------------------------------------------------------------------- PVOID LsapAssignThread( LPTHREAD_START_ROUTINE pFunction, PVOID pvParameter, PSession pSession, BOOLEAN fUrgent) { PLSAP_THREAD_TASK pTask; PLSAP_TASK_QUEUE pQueue; LSAP_TASK_STATUS TaskStatus ; pTask = (PLSAP_THREAD_TASK) LsapAllocatePrivateHeap( sizeof( LSAP_THREAD_TASK ) ); if (!pTask) { return( NULL ); } pTask->pFunction = pFunction; pTask->pvParameter = pvParameter; pTask->pSession = pSession; if ( pSession->SharedData->pQueue ) { LockSession(pSession); if( pSession->SharedData->pQueue ) { pQueue = pSession->SharedData->pQueue; } else { pQueue = &GlobalQueue; } UnlockSession(pSession); } else { pQueue = &GlobalQueue; } LsaTuningParameters.ShrinkOn = FALSE; // // Reference the session so that it will never go away while a thread // is working on this task. The worker function will deref the session. // SpmpReferenceSession( pSession ); TaskStatus = EnqueueThreadTask( pQueue, pTask, fUrgent ); if ( ( TaskStatus == TaskQueued ) || ( TaskStatus == TaskUnknown ) ) { return pTask ; } if ( TaskStatus == TaskNotQueued ) { // // Failed, therefore deref this session. // SpmpDereferenceSession( pSession ); LsapFreePrivateHeap( pTask ); } return NULL ; } //+--------------------------------------------------------------------------- // // Function: CreateSubordinateQueue // // Synopsis: Create a Queue hanging off an original queue. // // Arguments: [pQueue] -- // [pOriginalQueue] -- // // History: 11-17-95 RichardW Created // // Notes: // //---------------------------------------------------------------------------- BOOL CreateSubordinateQueue( PSession pSession, PLSAP_TASK_QUEUE pOriginalQueue) { HANDLE hThread; DWORD tid; PLSAP_TASK_QUEUE pQueue ; pQueue = (LSAP_TASK_QUEUE *) LsapAllocatePrivateHeap( sizeof( LSAP_TASK_QUEUE ) ); if ( !pQueue ) { return FALSE ; } DebugLog(( DEB_TRACE_QUEUE, "Creating sub queue %x\n", pQueue )); if (InitializeTaskQueue( pQueue, QueueShareRead )) { LockQueue( pQueue ); LockQueue( pOriginalQueue ); pQueue->pNext = pOriginalQueue->pShared; pOriginalQueue->pShared = pQueue; pQueue->pOriginal = pOriginalQueue; pQueue->OwnerSession = pSession ; #if 0 pQueue->TotalThreads++; pQueue->IdleThreads++; #endif UnlockQueue( pOriginalQueue ); UnlockQueue( pQueue ); pSession->SharedData->pQueue = pQueue ; hThread = LsapCreateThread( NULL, 0, SpmPoolThreadBase, pSession, 0, &pSession->ThreadId ); if (hThread != NULL) { NtClose( hThread ); // // Wait for the thread to signal the event, so that // we know it's ready // WaitForSingleObject( pQueue->StartSync, INFINITE ); NtClose( pQueue->StartSync ); pQueue->StartSync = NULL ; return( TRUE ); } else { RtlDeleteCriticalSection( &pQueue->Lock ); LsapFreePrivateHeap( pQueue ); pQueue = NULL ; } } if ( pQueue ) { LsapFreePrivateHeap( pQueue ); } return( FALSE ); } //+--------------------------------------------------------------------------- // // Function: DeleteSubordinateQueue // // Synopsis: // // Effects: // // Arguments: [pQueue] -- // // Requires: // // Returns: // // Signals: // // Modifies: // // Algorithm: // // History: 8-05-96 RichardW Created // // Notes: // //---------------------------------------------------------------------------- BOOL DeleteSubordinateQueue( PLSAP_TASK_QUEUE pQueue, ULONG Flags) { PLSAP_TASK_QUEUE pOriginal; PLSAP_TASK_QUEUE pScan; PLSAP_TASK_QUEUE pPrev ; PLSAP_THREAD_TASK pTask ; DWORD dwResult ; PLIST_ENTRY List ; PSession ThreadSession = GetCurrentSession(); OBJECT_HANDLE_FLAG_INFORMATION FlagInfo ; // // Lock it // DebugLog(( DEB_TRACE, "Deleting queue %x\n", pQueue )); LockQueue( pQueue ); if ( pQueue->pShared ) { pOriginal = pQueue->pOriginal ; LockQueue( pOriginal ); // // Unlink Queue from parent: // if ( pOriginal->pShared != pQueue ) { pScan = pOriginal->pShared; LockQueue( pScan ); while ( pScan->pNext && (pScan->pNext != pQueue) ) { pPrev = pScan ; pScan = pScan->pNext; UnlockQueue( pPrev ); } if ( pScan->pNext ) { pScan->pNext = pQueue->pNext ; } UnlockQueue( pScan ); } else { pOriginal->pShared = pQueue->pNext; } pQueue->pNext = NULL ; // // Done with parent // UnlockQueue( pOriginal ); } // // Drain queue by removing all the tasks. // while ( !IsListEmpty( &pQueue->pTasks ) ) { List = RemoveHeadList( &pQueue->pTasks ); pQueue->Tasks-- ; pTask = CONTAINING_RECORD( List, LSAP_THREAD_TASK, Next ); // // A synchronous drain will have this thread execute // all remaining tasks. // if ( Flags & DELETEQ_SYNC_DRAIN ) { SpmpReferenceSession(pTask->pSession); SetCurrentSession( pTask->pSession ); dwResult = pTask->pFunction(pTask->pvParameter); SetCurrentSession( ThreadSession ); SpmpDereferenceSession(pTask->pSession); LsapFreePrivateHeap( pTask ); } else { // // Otherwise, send them to the global queue to be // executed by other threads // EnqueueThreadTask( &GlobalQueue, pTask, FALSE ); } } // // Now, kill off all the threads // pQueue->Type = QueueZombie ; // // We might be executing on our own worker thread. If there are // more than one thread associated with this queue, we also // need to do the sync. // if ( ( pQueue->OwnerSession != ThreadSession ) || ( pQueue->TotalThreads > 1 ) ) { // // We are not a worker thread. Sync with the other // threads to clean up: // pQueue->StartSync = CreateEvent( NULL, FALSE, FALSE, NULL ); // // Kick the semaphore for all the threads that need it // (all of them if we aren't a worker thread, or n-1 if // we are: // ReleaseSemaphore( pQueue->hSemaphore, (pQueue->OwnerSession == ThreadSession ? pQueue->TotalThreads - 1 : pQueue->TotalThreads), NULL ); UnlockQueue( pQueue ); // // if we failed to create an event, then we may cause an invalid handle // problem in the client threads. Sleep a little to let the other threads // go (hopefully), then close the semaphore and let the error handling // in the threads deal with it. // if ( pQueue->StartSync ) { WaitForSingleObjectEx( pQueue->StartSync, INFINITE, FALSE ); // // Synchronize with the last thread to own the queue: // LockQueue( pQueue ); CloseHandle( pQueue->StartSync ); pQueue->StartSync = NULL ; } else { // // kludge up a retry loop: // int i = 50 ; while ( i && pQueue->TotalThreads ) { Sleep( 100 ); i-- ; } // // If they're still there, forget it. Return FALSE. Leak. // if ( pQueue->TotalThreads ) { return FALSE ; } } } // // At this point, we close the queue down: // FlagInfo.Inherit = FALSE ; FlagInfo.ProtectFromClose = FALSE ; NtSetInformationObject( pQueue->hSemaphore, ObjectHandleFlagInformation, &FlagInfo, sizeof( FlagInfo ) ); CloseHandle( pQueue->hSemaphore ); UnlockQueue( pQueue ); RtlDeleteCriticalSection( &pQueue->Lock ); LsapFreePrivateHeap( pQueue ); return( TRUE ); }