Something went wrong. Try again.
Reactos
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767/* * COPYRIGHT: See COPYING in the top level directory * PROJECT: ReactOS Kernel * FILE: ntoskrnl/ex/work.c * PURPOSE: Manage system work queues and worker threads * PROGRAMMER: Alex Ionescu (alex@relsoft.net) */
/* INCLUDES ******************************************************************/
#include <ntoskrnl.h>#define NDEBUG#include <debug.h>
/* DATA **********************************************************************/
/* Number of worker threads for each Queue */#define EX_HYPERCRITICAL_WORK_THREADS 1#define EX_DELAYED_WORK_THREADS 3#define EX_CRITICAL_WORK_THREADS 5
/* Magic flag for dynamic worker threads */#define EX_DYNAMIC_WORK_THREAD 0x80000000
/* Worker thread priority increments (added to base priority) */#define EX_HYPERCRITICAL_QUEUE_PRIORITY_INCREMENT 7#define EX_CRITICAL_QUEUE_PRIORITY_INCREMENT 5#define EX_DELAYED_QUEUE_PRIORITY_INCREMENT 4
/* The actual worker queue array */EX_WORK_QUEUE ExWorkerQueue[MaximumWorkQueue];
/* Accounting of the total threads and registry hacked threads */ULONG ExCriticalWorkerThreads;ULONG ExDelayedWorkerThreads;ULONG ExpAdditionalCriticalWorkerThreads;ULONG ExpAdditionalDelayedWorkerThreads;
/* Future support for stack swapping worker threads */BOOLEAN ExpWorkersCanSwap;LIST_ENTRY ExpWorkerListHead;FAST_MUTEX ExpWorkerSwapinMutex;
/* The worker balance set manager events */KEVENT ExpThreadSetManagerEvent;KEVENT ExpThreadSetManagerShutdownEvent;
/* Thread pointers for future worker thread shutdown support */PETHREAD ExpWorkerThreadBalanceManagerPtr;PETHREAD ExpLastWorkerThread;
/* PRIVATE FUNCTIONS *********************************************************/
/*++ * @name ExpWorkerThreadEntryPoint * * The ExpWorkerThreadEntryPoint routine is the entrypoint for any new * worker thread created by teh system. * * @param Context * Contains the work queue type masked with a flag specifing whether the * thread is dynamic or not. * * @return None. * * @remarks A dynamic thread can timeout after 10 minutes of waiting on a queue * while a static thread will never timeout. * * Worker threads must return at IRQL == PASSIVE_LEVEL, must not have * active impersonation info, and must not have disabled APCs. * * NB: We will re-enable APCs for broken threads but all other cases * will generate a bugcheck. * *--*/VOIDNTAPIExpWorkerThreadEntryPoint(IN PVOID Context){ PWORK_QUEUE_ITEM WorkItem; PLIST_ENTRY QueueEntry; WORK_QUEUE_TYPE WorkQueueType; PEX_WORK_QUEUE WorkQueue; LARGE_INTEGER Timeout; PLARGE_INTEGER TimeoutPointer = NULL; PETHREAD Thread = PsGetCurrentThread(); KPROCESSOR_MODE WaitMode; EX_QUEUE_WORKER_INFO OldValue, NewValue;
/* Check if this is a dyamic thread */ if ((ULONG_PTR)Context & EX_DYNAMIC_WORK_THREAD) { /* It is, which means we will eventually time out after 10 minutes */ Timeout.QuadPart = Int32x32To64(10, -10000000 * 60); TimeoutPointer = &Timeout; }
/* Get Queue Type and Worker Queue */ WorkQueueType = (WORK_QUEUE_TYPE)((ULONG_PTR)Context & ~EX_DYNAMIC_WORK_THREAD); WorkQueue = &ExWorkerQueue[WorkQueueType];
/* Select the wait mode */ WaitMode = (UCHAR)WorkQueue->Info.WaitMode;
/* Nobody should have initialized this yet, do it now */ ASSERT(Thread->ExWorkerCanWaitUser == 0); if (WaitMode == UserMode) Thread->ExWorkerCanWaitUser = TRUE;
/* If we shouldn't swap, disable that feature */ if (!ExpWorkersCanSwap) KeSetKernelStackSwapEnable(FALSE);
/* Set the worker flags */ do { /* Check if the queue is being disabled */ if (WorkQueue->Info.QueueDisabled) { /* Re-enable stack swapping and kill us */ KeSetKernelStackSwapEnable(TRUE); PsTerminateSystemThread(STATUS_SYSTEM_SHUTDOWN); }
/* Increase the worker count */ OldValue = WorkQueue->Info; NewValue = OldValue; NewValue.WorkerCount++; } while (InterlockedCompareExchange((PLONG)&WorkQueue->Info, *(PLONG)&NewValue, *(PLONG)&OldValue) != *(PLONG)&OldValue);
/* Success, you are now officially a worker thread! */ Thread->ActiveExWorker = TRUE;
/* Loop forever */ProcessLoop: for (;;) { /* Wait for something to happen on the queue */ QueueEntry = KeRemoveQueue(&WorkQueue->WorkerQueue, WaitMode, TimeoutPointer);
/* Check if we timed out and quit this loop in that case */ if ((NTSTATUS)(ULONG_PTR)QueueEntry == STATUS_TIMEOUT) break;
/* Increment Processed Work Items */ InterlockedIncrement((PLONG)&WorkQueue->WorkItemsProcessed);
/* Get the Work Item */ WorkItem = CONTAINING_RECORD(QueueEntry, WORK_QUEUE_ITEM, List);
/* Make sure nobody is trying to play smart with us */ ASSERT((ULONG_PTR)WorkItem->WorkerRoutine > MmUserProbeAddress);
/* Call the Worker Routine */ WorkItem->WorkerRoutine(WorkItem->Parameter);
/* Make sure APCs are not disabled */ if (Thread->Tcb.CombinedApcDisable != 0) { /* We're nice and do it behind your back */ DPRINT1("Warning: Broken Worker Thread: %p %p %p came back " "with APCs disabled!\n", WorkItem->WorkerRoutine, WorkItem->Parameter, WorkItem); ASSERT(Thread->Tcb.CombinedApcDisable == 0); Thread->Tcb.CombinedApcDisable = 0; }
/* Make sure it returned at right IRQL */ if (KeGetCurrentIrql() != PASSIVE_LEVEL) { /* It didn't, bugcheck! */ KeBugCheckEx(WORKER_THREAD_RETURNED_AT_BAD_IRQL, (ULONG_PTR)WorkItem->WorkerRoutine, KeGetCurrentIrql(), (ULONG_PTR)WorkItem->Parameter, (ULONG_PTR)WorkItem); }
/* Make sure it returned with Impersionation Disabled */ if (Thread->ActiveImpersonationInfo) { /* It didn't, bugcheck! */ KeBugCheckEx(IMPERSONATING_WORKER_THREAD, (ULONG_PTR)WorkItem->WorkerRoutine, (ULONG_PTR)WorkItem->Parameter, (ULONG_PTR)WorkItem, 0); } }
/* This is a dynamic thread. Terminate it unless IRPs are pending */ if (!IsListEmpty(&Thread->IrpList)) goto ProcessLoop;
/* Don't terminate it if the queue is disabled either */ if (WorkQueue->Info.QueueDisabled) goto ProcessLoop;
/* Set the worker flags */ do { /* Decrease the worker count */ OldValue = WorkQueue->Info; NewValue = OldValue; NewValue.WorkerCount--; } while (InterlockedCompareExchange((PLONG)&WorkQueue->Info, *(PLONG)&NewValue, *(PLONG)&OldValue) != *(PLONG)&OldValue);
/* Decrement dynamic thread count */ InterlockedDecrement(&WorkQueue->DynamicThreadCount);
/* We're not a worker thread anymore */ Thread->ActiveExWorker = FALSE;
/* Re-enable the stack swap */ KeSetKernelStackSwapEnable(TRUE); return;}
/*++ * @name ExpCreateWorkerThread * * The ExpCreateWorkerThread routine creates a new worker thread for the * specified queue. * * @param QueueType * Type of the queue to use for this thread. Valid values are: * - DelayedWorkQueue * - CriticalWorkQueue * - HyperCriticalWorkQueue * * @param Dynamic * Specifies whether or not this thread is a dynamic thread. * * @return None. * * @remarks HyperCritical work threads run at priority 7; Critical work threads * run at priority 5, and delayed work threads run at priority 4. * * This, worker threads cannot pre-empty a normal user-mode thread. * *--*/VOIDNTAPIExpCreateWorkerThread(WORK_QUEUE_TYPE WorkQueueType, IN BOOLEAN Dynamic){ PETHREAD Thread; HANDLE hThread; ULONG Context; KPRIORITY Priority; NTSTATUS Status;
/* Check if this is going to be a dynamic thread */ Context = WorkQueueType;
/* Add the dynamic mask */ if (Dynamic) Context |= EX_DYNAMIC_WORK_THREAD;
/* Create the System Thread */ Status = PsCreateSystemThread(&hThread, THREAD_ALL_ACCESS, NULL, NULL, NULL, ExpWorkerThreadEntryPoint, UlongToPtr(Context)); if (!NT_SUCCESS(Status)) { /* Well... */ DPRINT1("Failed to create worker thread: 0x%08x\n", Status); return; }
/* If the thread is dynamic */ if (Dynamic) { /* Increase the count */ InterlockedIncrement(&ExWorkerQueue[WorkQueueType].DynamicThreadCount); }
/* Set the priority */ if (WorkQueueType == DelayedWorkQueue) { /* Priority == 4 */ Priority = EX_DELAYED_QUEUE_PRIORITY_INCREMENT; } else if (WorkQueueType == CriticalWorkQueue) { /* Priority == 5 */ Priority = EX_CRITICAL_QUEUE_PRIORITY_INCREMENT; } else { /* Priority == 7 */ Priority = EX_HYPERCRITICAL_QUEUE_PRIORITY_INCREMENT; }
/* Get the Thread */ ObReferenceObjectByHandle(hThread, THREAD_SET_INFORMATION, PsThreadType, KernelMode, (PVOID*)&Thread, NULL);
/* Set the Priority */ KeSetBasePriorityThread(&Thread->Tcb, Priority);
/* Dereference and close handle */ ObDereferenceObject(Thread); ObCloseHandle(hThread, KernelMode);}
/*++ * @name ExpDetectWorkerThreadDeadlock * * The ExpDetectWorkerThreadDeadlock routine checks every queue and creates * a dynamic thread if the queue seems to be deadlocked. * * @param None * * @return None. * * @remarks The algorithm for deciding if a new thread must be created is based * on whether the queue has processed no new items in the last second, * and new items are still enqueued. * *--*/VOIDNTAPIExpDetectWorkerThreadDeadlock(VOID){ ULONG i; PEX_WORK_QUEUE Queue;
/* Loop the 3 queues */ for (i = 0; i < MaximumWorkQueue; i++) { /* Get the queue */ Queue = &ExWorkerQueue[i]; ASSERT(Queue->DynamicThreadCount <= 16);
/* Check if stuff is on the queue that still is unprocessed */ if ((Queue->QueueDepthLastPass) && (Queue->WorkItemsProcessed == Queue->WorkItemsProcessedLastPass) && (Queue->DynamicThreadCount < 16)) { /* Stuff is still on the queue and nobody did anything about it */ DPRINT1("EX: Work Queue Deadlock detected: %lu\n", i); ExpCreateWorkerThread(i, TRUE); DPRINT1("Dynamic threads queued %d\n", Queue->DynamicThreadCount); }
/* Update our data */ Queue->WorkItemsProcessedLastPass = Queue->WorkItemsProcessed; Queue->QueueDepthLastPass = KeReadStateQueue(&Queue->WorkerQueue); }}
/*++ * @name ExpCheckDynamicThreadCount * * The ExpCheckDynamicThreadCount routine checks every queue and creates * a dynamic thread if the queue requires one. * * @param None * * @return None. * * @remarks The algorithm for deciding if a new thread must be created is * documented in the ExQueueWorkItem routine. * *--*/VOIDNTAPIExpCheckDynamicThreadCount(VOID){ ULONG i; PEX_WORK_QUEUE Queue;
/* Loop the 3 queues */ for (i = 0; i < MaximumWorkQueue; i++) { /* Get the queue */ Queue = &ExWorkerQueue[i];
/* Check if still need a new thread. See ExQueueWorkItem */ if ((Queue->Info.MakeThreadsAsNecessary) && (!IsListEmpty(&Queue->WorkerQueue.EntryListHead)) && (Queue->WorkerQueue.CurrentCount < Queue->WorkerQueue.MaximumCount) && (Queue->DynamicThreadCount < 16)) { /* Create a new thread */ DPRINT1("EX: Creating new dynamic thread as requested\n"); ExpCreateWorkerThread(i, TRUE); } }}
/*++ * @name ExpWorkerThreadBalanceManager * * The ExpWorkerThreadBalanceManager routine is the entrypoint for the * worker thread balance set manager. * * @param Context * Unused. * * @return None. * * @remarks The worker thread balance set manager listens every second, but can * also be woken up by an event when a new thread is needed, or by the * special shutdown event. This thread runs at priority 7. * * This routine must run at IRQL == PASSIVE_LEVEL. * *--*/VOIDNTAPIExpWorkerThreadBalanceManager(IN PVOID Context){ KTIMER Timer; LARGE_INTEGER Timeout; NTSTATUS Status; PVOID WaitEvents[3]; PAGED_CODE(); UNREFERENCED_PARAMETER(Context);
/* Raise our priority above all other worker threads */ KeSetBasePriorityThread(KeGetCurrentThread(), EX_CRITICAL_QUEUE_PRIORITY_INCREMENT + 1);
/* Setup the timer */ KeInitializeTimer(&Timer); Timeout.QuadPart = Int32x32To64(-1, 10000000);
/* We'll wait on the periodic timer and also the emergency event */ WaitEvents[0] = &Timer; WaitEvents[1] = &ExpThreadSetManagerEvent; WaitEvents[2] = &ExpThreadSetManagerShutdownEvent;
/* Start wait loop */ for (;;) { /* Wait for the timer */ KeSetTimer(&Timer, Timeout, NULL); Status = KeWaitForMultipleObjects(3, WaitEvents, WaitAny, Executive, KernelMode, FALSE, NULL, NULL); if (Status == 0) { /* Our timer expired. Check for deadlocks */ ExpDetectWorkerThreadDeadlock(); } else if (Status == 1) { /* Someone notified us, verify if we should create a new thread */ ExpCheckDynamicThreadCount(); } else if (Status == 2) { /* We are shutting down. Cancel the timer */ DPRINT1("System shutdown\n"); KeCancelTimer(&Timer);
/* Make sure we have a final thread */ ASSERT(ExpLastWorkerThread);
/* Wait for it */ KeWaitForSingleObject(ExpLastWorkerThread, Executive, KernelMode, FALSE, NULL);
/* Dereference it and kill us */ ObDereferenceObject(ExpLastWorkerThread); PsTerminateSystemThread(STATUS_SYSTEM_SHUTDOWN); }
/* * If WinDBG wants to attach or kill a user-mode process, and/or * page-in an address region, queue a debugger worker thread. */ if (ExpDebuggerWork == WinKdWorkerStart) { ExInitializeWorkItem(&ExpDebuggerWorkItem, ExpDebuggerWorker, NULL); ExpDebuggerWork = WinKdWorkerInitialized; ExQueueWorkItem(&ExpDebuggerWorkItem, DelayedWorkQueue); } }}
/*++ * @name ExpInitializeWorkerThreads * * The ExpInitializeWorkerThreads routine initializes worker thread and * work queue support. * * @param None. * * @return None. * * @remarks This routine is only called once during system initialization. * *--*/CODE_SEG("INIT")VOIDNTAPIExpInitializeWorkerThreads(VOID){ ULONG WorkQueueType; ULONG CriticalThreads, DelayedThreads; HANDLE ThreadHandle; PETHREAD Thread; ULONG i; NTSTATUS Status;
/* Setup the stack swap support */ ExInitializeFastMutex(&ExpWorkerSwapinMutex); InitializeListHead(&ExpWorkerListHead); ExpWorkersCanSwap = TRUE;
/* Set the number of critical and delayed threads. We shouldn't hardcode */ DelayedThreads = EX_DELAYED_WORK_THREADS; CriticalThreads = EX_CRITICAL_WORK_THREADS;
/* Protect against greedy registry modifications */ ExpAdditionalDelayedWorkerThreads = min(ExpAdditionalDelayedWorkerThreads, 16); ExpAdditionalCriticalWorkerThreads = min(ExpAdditionalCriticalWorkerThreads, 16);
/* Calculate final count */ DelayedThreads += ExpAdditionalDelayedWorkerThreads; CriticalThreads += ExpAdditionalCriticalWorkerThreads;
/* Initialize the Array */ for (WorkQueueType = 0; WorkQueueType < MaximumWorkQueue; WorkQueueType++) { /* Clear the structure and initialize the queue */ RtlZeroMemory(&ExWorkerQueue[WorkQueueType], sizeof(EX_WORK_QUEUE)); KeInitializeQueue(&ExWorkerQueue[WorkQueueType].WorkerQueue, 0); }
/* Dynamic threads are only used for the critical queue */ ExWorkerQueue[CriticalWorkQueue].Info.MakeThreadsAsNecessary = TRUE;
/* Initialize the balance set manager events */ KeInitializeEvent(&ExpThreadSetManagerEvent, SynchronizationEvent, FALSE); KeInitializeEvent(&ExpThreadSetManagerShutdownEvent, NotificationEvent, FALSE);
/* Create the built-in worker threads for the critical queue */ for (i = 0; i < CriticalThreads; i++) { /* Create the thread */ ExpCreateWorkerThread(CriticalWorkQueue, FALSE); ExCriticalWorkerThreads++; }
/* Create the built-in worker threads for the delayed queue */ for (i = 0; i < DelayedThreads; i++) { /* Create the thread */ ExpCreateWorkerThread(DelayedWorkQueue, FALSE); ExDelayedWorkerThreads++; }
/* Create the built-in worker thread for the hypercritical queue */ ExpCreateWorkerThread(HyperCriticalWorkQueue, FALSE);
/* Create the balance set manager thread */ Status = PsCreateSystemThread(&ThreadHandle, THREAD_ALL_ACCESS, NULL, 0, NULL, ExpWorkerThreadBalanceManager, NULL); if (!NT_SUCCESS(Status)) { KeBugCheckEx(PHASE1_INITIALIZATION_FAILED, Status, 0, 0, 0); }
/* Get a pointer to it for the shutdown process */ ObReferenceObjectByHandle(ThreadHandle, THREAD_ALL_ACCESS, NULL, KernelMode, (PVOID*)&Thread, NULL); ExpWorkerThreadBalanceManagerPtr = Thread;
/* Close the handle and return */ ObCloseHandle(ThreadHandle, KernelMode);}
VOIDNTAPIExpSetSwappingKernelApc(IN PKAPC Apc, OUT PKNORMAL_ROUTINE *NormalRoutine, IN OUT PVOID *NormalContext, IN OUT PVOID *SystemArgument1, IN OUT PVOID *SystemArgument2){ PBOOLEAN AllowSwap; PKEVENT Event = (PKEVENT)*SystemArgument1;
/* Make sure it's an active worker */ if (PsGetCurrentThread()->ActiveExWorker) { /* Read the setting from the context flag */ AllowSwap = (PBOOLEAN)NormalContext; KeSetKernelStackSwapEnable(*AllowSwap); }
/* Let caller know that we're done */ KeSetEvent(Event, 0, FALSE);}
VOIDNTAPIExSwapinWorkerThreads(IN BOOLEAN AllowSwap){ KEVENT Event; PETHREAD CurrentThread = PsGetCurrentThread(), Thread; PEPROCESS Process = PsInitialSystemProcess; KAPC Apc; PAGED_CODE();
/* Initialize an event so we know when we're done */ KeInitializeEvent(&Event, NotificationEvent, FALSE);
/* Lock this routine */ ExAcquireFastMutex(&ExpWorkerSwapinMutex);
/* New threads cannot swap anymore */ ExpWorkersCanSwap = AllowSwap;
/* Loop all threads in the system process */ Thread = PsGetNextProcessThread(Process, NULL); while (Thread) { /* Skip threads with explicit permission to do this */ if (Thread->ExWorkerCanWaitUser) goto Next;
/* Check if we reached ourselves */ if (Thread == CurrentThread) { /* Do it inline */ KeSetKernelStackSwapEnable(AllowSwap); } else { /* Queue an APC */ KeInitializeApc(&Apc, &Thread->Tcb, InsertApcEnvironment, ExpSetSwappingKernelApc, NULL, NULL, KernelMode, &AllowSwap); if (KeInsertQueueApc(&Apc, &Event, NULL, 3)) { /* Wait for the APC to run */ KeWaitForSingleObject(&Event, Executive, KernelMode, FALSE, NULL); KeClearEvent(&Event); } }
/* Next thread */Next: Thread = PsGetNextProcessThread(Process, Thread); }
/* Release the lock */ ExReleaseFastMutex(&ExpWorkerSwapinMutex);}
/* PUBLIC FUNCTIONS **********************************************************/
/*++ * @name ExQueueWorkItem * @implemented NT4 * * The ExQueueWorkItem routine acquires rundown protection for * the specified descriptor. * * @param WorkItem * Pointer to an initialized Work Queue Item structure. This structure * must be located in nonpaged pool memory. * * @param QueueType * Type of the queue to use for this item. Can be one of the following: * - DelayedWorkQueue * - CriticalWorkQueue * - HyperCriticalWorkQueue * * @return None. * * @remarks This routine is obsolete. Use IoQueueWorkItem instead. * * Callers of this routine must be running at IRQL <= DISPATCH_LEVEL. * *--*/VOIDNTAPIExQueueWorkItem(IN PWORK_QUEUE_ITEM WorkItem, IN WORK_QUEUE_TYPE QueueType){ PEX_WORK_QUEUE WorkQueue = &ExWorkerQueue[QueueType]; ASSERT(QueueType < MaximumWorkQueue); ASSERT(WorkItem->List.Flink == NULL);
/* Don't try to trick us */ if ((ULONG_PTR)WorkItem->WorkerRoutine < MmUserProbeAddress) { /* Bugcheck the system */ KeBugCheckEx(WORKER_INVALID, 1, (ULONG_PTR)WorkItem, (ULONG_PTR)WorkItem->WorkerRoutine, 0); }
/* Insert the Queue */ KeInsertQueue(&WorkQueue->WorkerQueue, &WorkItem->List); ASSERT(!WorkQueue->Info.QueueDisabled);
/* * Check if we need a new thread. Our decision is as follows: * - This queue type must support Dynamic Threads (duh!) * - It actually has to have unprocessed items * - We have CPUs which could be handling another thread * - We haven't abused our usage of dynamic threads. */ if ((WorkQueue->Info.MakeThreadsAsNecessary) && (!IsListEmpty(&WorkQueue->WorkerQueue.EntryListHead)) && (WorkQueue->WorkerQueue.CurrentCount < WorkQueue->WorkerQueue.MaximumCount) && (WorkQueue->DynamicThreadCount < 16)) { /* Let the balance manager know about it */ DPRINT1("Requesting a new thread. CurrentCount: %lu. MaxCount: %lu\n", WorkQueue->WorkerQueue.CurrentCount, WorkQueue->WorkerQueue.MaximumCount); KeSetEvent(&ExpThreadSetManagerEvent, 0, FALSE); }}
/* EOF */