/*++ Copyright (c) 1995-2000 Microsoft Corporation Module Name: sched.cxx Abstract: This module contains a simple timer interface for scheduling future work items Author: John Ludeman (johnl) 17-Jul-1995 Project: Internet Servers Common Server DLL Revisions: Murali R. Krishnan (MuraliK) 16-Sept-1996 Added scheduler items cache George V. Reilly (GeorgeRe) May-1999 Removed the global variables; turned into refcounted objects, so that code will survive stops and restarts when work items take a long time to complete --*/ // // Include Headers // #include "precomp.hxx" #include #define DLL_IMPLEMENTATION #define IMPLEMENTATION_EXPORT #include #include "sched.hxx" // Initialize class static members CSchedData* CSchedData::sm_psd = NULL; CLockedDoubleList CSchedData::sm_lstSchedulers; LONG CSchedData::sm_nID = 0; LONG CThreadData::sm_nID = 1000; LONG SCHED_ITEM::sm_lSerialNumber = SCHED_ITEM::SERIAL_NUM_INITIAL_VALUE; // SCHED_ITEM Finite State Machine SCHED_ITEM_STATE sg_rgSchedNextState[SI_OP_MAX][SI_MAX_ITEMS] = { // operation = SI_OP_ADD { // old state SI_ERROR, // SI_ERROR SI_ACTIVE, // SI_IDLE SI_ERROR, // SI_ACTIVE SI_ERROR, // SI_ACTIVE_PERIODIC SI_ERROR, // SI_CALLBACK_PERIODIC SI_ERROR, // SI_TO_BE_DELETED }, // operation = SI_OP_ADD_PERIODIC { // old state SI_ERROR, // SI_ERROR SI_ACTIVE_PERIODIC, // SI_IDLE SI_ERROR, // SI_ACTIVE SI_ERROR, // SI_ACTIVE_PERIODIC SI_ACTIVE_PERIODIC, // SI_CALLBACK_PERIODIC: rescheduling // periodic item SI_ERROR, // SI_TO_BE_DELETED }, // operation = SI_OP_CALLBACK { // old state SI_ERROR, // SI_ERROR SI_ERROR, // SI_IDLE SI_TO_BE_DELETED, // SI_ACTIVE: to be removed after completing // callbacks SI_CALLBACK_PERIODIC, // SI_ACTIVE_PERIODIC SI_ERROR, // SI_CALLBACK_PERIODIC SI_ERROR, // SI_TO_BE_DELETED }, // operation = SI_OP_DELETE { // old state SI_ERROR, // SI_ERROR SI_ERROR, // SI_IDLE SI_IDLE, // SI_ACTIVE SI_IDLE, // SI_ACTIVE_PERIODIC SI_TO_BE_DELETED, // SI_CALLBACK_PERIODIC: mark this to be // deleted after return SI_TO_BE_DELETED, // SI_TO_BE_DELETED: idempotent delete ops } }; // // Global data items // LONG cSchedInits = 0; LONG cSchedUninits = 0; /************************************************************ * Public functions of Scheduler ************************************************************/ BOOL SchedulerInitialize( VOID ) /*++ Routine Description: Initializes the scheduler/timer package Arguments: Return Value: TRUE if successful, FALSE on error (call GetLastError) --*/ { // g_fErrorFlags |= DEBUG_SCHED; // g_pDebug->m_iControlFlag |= DEBUG_SCHED; ++cSchedInits; unsigned idThread; LONG i, numThreads; CSchedData* const psd = new CSchedData(); if (psd == NULL || !psd->IsValid()) return FALSE; IF_DEBUG(SCHED) { DBGPRINTF(( DBG_CONTEXT, "SchedulerInitialize: inits=%d, uninits=%d\n", cSchedInits, cSchedUninits)); } if ( TsIsNtServer() ) { numThreads = max(NumProcessors(), NUM_SCHEDULE_THREADS_NTS); } else { numThreads = NUM_SCHEDULE_THREADS_PWS; } numThreads = min(numThreads, MAX_THREADS); // numThreads = MAX_THREADS; DBG_ASSERT(numThreads > 0); for (i = 0; i < numThreads; ++i) { CThreadData* ptd = new CThreadData(psd); if (ptd == NULL || !ptd->IsValid()) { numThreads = i; if (ptd != NULL) ptd->Release(); break; } } if (numThreads == 0) { delete psd; return FALSE; } // Kick scheduler threads into life now that everything has been // initialized. psd->m_lstThreads.Lock(); for (CListEntry* ple = psd->m_lstThreads.First(); ple != psd->m_lstThreads.HeadNode(); ple = ple->Flink) { CThreadData* ptd = CONTAINING_RECORD(ple, CThreadData, m_leThreads); DBG_ASSERT(ptd->CheckSignature()); ResumeThread(ptd->m_hThreadSelf); } psd->m_lstThreads.Unlock(); // Update the global pointer to the scheduler CSchedData* const psd2 = (CSchedData*) InterlockedExchangePointer((VOID**)&CSchedData::sm_psd, psd); DBG_ASSERT(psd2 == NULL); return TRUE; } // SchedulerInitialize() VOID SchedulerTerminate( VOID ) /*++ Routine Description: Terminates and cleans up the scheduling package. Any items left on the list are *not* called during cleanup. --*/ { // Grab the global pointer, then set it to NULL CSchedData* const psd = (CSchedData*) InterlockedExchangePointer((VOID**)&CSchedData::sm_psd, NULL); DBG_ASSERT(psd == NULL || !psd->m_fShutdown); ++cSchedUninits; IF_DEBUG(SCHED) { DBGPRINTF(( DBG_CONTEXT, "SchedulerTerminate: inits=%d, uninits=%d\n", cSchedInits, cSchedUninits)); } if (psd == NULL || psd->m_fShutdown) { // already shutting down return; } psd->Terminate(); } void CSchedData::Terminate() { HANDLE ahThreadIds[MAX_THREADS]; int i; CListEntry* ple; m_fShutdown = TRUE; const int nMaxTries = 1; const DWORD dwTimeOut = INFINITE; for (int iTries = 0; iTries < nMaxTries; ++iTries) { int nThreads = 0; m_lstThreads.Lock(); for (ple = m_lstThreads.First(), i = 0; ple != m_lstThreads.HeadNode(); ple = ple->Flink, i++) { CThreadData* ptd = CONTAINING_RECORD(ple, CThreadData, m_leThreads); DBG_ASSERT(ptd->CheckSignature()); // Set the shutdown event once for each thread DBGPRINTF(( DBG_CONTEXT, "CSchedData::Terminate: iteration %d, " "notifying %ld\n", iTries, ptd->m_nID)); DBG_REQUIRE( SetEvent(ptd->m_hevtShutdown) ); ahThreadIds[i] = ptd->m_hThreadSelf; ++nThreads; } m_lstThreads.Unlock(); if (nThreads == 0) break; // Wait for all the threads to shut down DWORD dw = WaitForMultipleObjects(nThreads, ahThreadIds, TRUE, dwTimeOut); DBGPRINTF(( DBG_CONTEXT, "CSchedData::Terminate: WFMO = %x\n", dw)); } LockItems(); // // Delete all of the items that were scheduled, note we do *not* // call any scheduled items in the list (there shouldn't be any) // if ( !m_lstItems.IsEmpty() ) { DBGPRINTF(( DBG_CONTEXT, "[CSchedData::Terminate] Warning - Items in schedule list " "at termination\n" )); int c = 0; CListEntry* pleSave = NULL; for (ple = m_lstItems.First(); ple != m_lstItems.HeadNode(); ple = pleSave) { SCHED_ITEM* psiList = CONTAINING_RECORD( ple, SCHED_ITEM, _ListEntry ); DBG_ASSERT(psiList->CheckSignature()); IF_DEBUG(SCHED) { DBGPRINTF(( DBG_CONTEXT, "[%8p] ser=%d ctxt=%p fncbk=%p state=%d thrd=%d\n", psiList, psiList->_dwSerialNumber, psiList->_pContext, psiList->_pfnCallback, psiList->_siState, psiList->_dwCallbackThreadId )); } pleSave = ple->Flink; if (!psiList->FInsideCallbackOnOtherThread()) { CDoubleList::RemoveEntry( &psiList->_ListEntry ); ple->Flink = NULL; DeleteSchedItem(psiList); ++c; } } DBGPRINTF(( DBG_CONTEXT, "[CSchedData::Terminate] %d items deleted\n", c )); } UnlockItems(); Release(); // release the last reference to `this' } // CSchedData::Terminate() CSchedData::~CSchedData() { DBG_ASSERT(m_lstThreads.IsEmpty()); DBG_ASSERT(m_lstItems.IsEmpty()); DBG_ASSERT(m_cThreads == 0); DBG_ASSERT(m_cRefs == 0); sm_lstSchedulers.RemoveEntry(&m_leGlobalList); CloseHandle(m_hevtNotify); delete m_pachSchedItems; CListEntry* pleSave = NULL; for (CListEntry* ple = m_lstDeadThreads.First(); ple != m_lstDeadThreads.HeadNode(); ple = pleSave) { pleSave = ple->Flink; CThreadData* ptd = CONTAINING_RECORD(ple, CThreadData, m_leThreads); DBG_ASSERT(ptd->CheckSignature()); delete ptd; } m_dwSignature = SIGNATURE_SCHEDDATA_FREE; } DWORD WINAPI ScheduleWorkItem( PFN_SCHED_CALLBACK pfnCallback, PVOID pContext, DWORD msecTime, BOOL fPeriodic ) /*++ Routine Description: Adds a timed work item to the work list Arguments: pfnCallback - Function to call pContext - Context to pass to the callback msecTime - number of milliseconds to wait before calling timeout nPriority - Thread priority to set for work item Return Value: zero on failure, non-zero on success. The return value can be used to remove the scheduled work item. --*/ { CSchedData* const psd = CSchedData::Scheduler(); if (psd == NULL) return 0; // // 1. alloc a new scheduler item // SCHED_ITEM* psi = psd->NewSchedItem(pfnCallback, pContext, msecTime); if ( !psi ) { // unable to create the item - return error cookie '0' return 0; } DWORD dwRet = psi->_dwSerialNumber; SCHED_OPS siop = ((fPeriodic)? SI_OP_ADD_PERIODIC : SI_OP_ADD); psi->_siState = sg_rgSchedNextState[siop][SI_IDLE]; // // 2. Insert the scheduler item into the active scheduler work-items list. // psd->LockItems(); psd->InsertIntoWorkItemList( psi); psd->UnlockItems(); // // 3. Indicate to scheduler threads that there is one new item on the list // DBG_REQUIRE( SetEvent( psd->m_hevtNotify )); IF_DEBUG(SCHED) { DBGPRINTF(( DBG_CONTEXT, "ScheduleWorkItem: (%d) " "[%8p] ser=%d ctxt=%p fncbk=%p state=%d thrd=%d\n", psd->m_nID, psi, psi->_dwSerialNumber, psi->_pContext, psi->_pfnCallback, psi->_siState, psi->_dwCallbackThreadId )); } return dwRet; } // ScheduleWorkItem() BOOL WINAPI RemoveWorkItem( DWORD dwCookie ) /*++ Routine Description: Removes a scheduled work item Arguments: dwCookie - The return value from a previous call to ScheduleWorkItem Return Value: TRUE if the item was found, FALSE if the item was not found. --*/ { CSchedData* const psd = CSchedData::Scheduler(); if (psd == NULL) return FALSE; SCHED_ITEM* psi = NULL; BOOL fWait = FALSE; IF_DEBUG(SCHED) { DBGPRINTF(( DBG_CONTEXT, "RemoveWorkItem: cookie=%d sched=%d\n", dwCookie, psd->m_nID)); } // // 1. lock the list // psd->LockItems(); // // 2. Find scheduler item on the list. // psi = psd->FindSchedulerItem( dwCookie); if ( NULL != psi) { // // 3. based on the state of the item take necessary actions. // SCHED_ITEM_STATE st = sg_rgSchedNextState[SI_OP_DELETE][psi->_siState]; psi->_siState = st; switch ( st) { case SI_TO_BE_DELETED: { DBGPRINTF(( DBG_CONTEXT, "SchedItem(%08p) marked to be deleted\n", psi)); // item will be deleted later. if (psi->FInsideCallbackOnOtherThread()) { // need to wait till callback complete psi->AddEvent(); fWait = TRUE; break; } } // fallthru case SI_IDLE: { // delete immediately CDoubleList::RemoveEntry( &psi->_ListEntry ); psi->_ListEntry.Flink = NULL; IF_DEBUG(SCHED) { DBGPRINTF(( DBG_CONTEXT, "RemoveWorkItem: " "[%8p] ser=%d ctxt=%p fncbk=%p state=%d thrd=%d\n", psi, psi->_dwSerialNumber, psi->_pContext, psi->_pfnCallback, psi->_siState, psi->_dwCallbackThreadId )); } psd->DeleteSchedItem(psi); break; } default: DBG_ASSERT( FALSE); break; } // switch() } // 4. Unlock the list psd->UnlockItems(); // 5. Wait for callback event and release the item if (fWait) { LONG l = psi->WaitForEventAndRelease(); IF_DEBUG(SCHED) { DBGPRINTF(( DBG_CONTEXT, "RemoveWorkItem: %d " "WaitForEventAndRelease returned %d.\n", dwCookie, l)); } if (l == 0) psd->DeleteSchedItem(psi); } IF_DEBUG(SCHED) { if ( NULL == psi) DBGPRINTF(( DBG_CONTEXT, "RemoveWorkItem: %d not found\n", dwCookie)); } // return TRUE if we found the item return ( NULL != psi); } // RemoveWorkItem() DWORD WINAPI ScheduleAdjustTime( DWORD dwCookie, DWORD msecNewTime ) /*++ This function finds the scheduler object for given cookie and changes the interval for next timeout of the item. Returns a Win32 error code: NO_ERROR => success. --*/ { CSchedData* const psd = CSchedData::Scheduler(); if (psd == NULL) return ERROR_NO_DATA; DBG_ASSERT( 0 != dwCookie); psd->LockItems(); // 1. Find the work item for given cookie SCHED_ITEM* psi = psd->FindSchedulerItem( dwCookie); if ( NULL != psi) { // 2. Remove the item from the list CDoubleList::RemoveEntry( &psi->_ListEntry ); psi->_ListEntry.Flink = NULL; // 3. Change the timeout value psi->ChangeTimeInterval( msecNewTime); // 4. Recalc expiry time and reinsert into the list of work items. psi->CalcExpiresTime(); psd->InsertIntoWorkItemList( psi); IF_DEBUG(SCHED) { DBGPRINTF(( DBG_CONTEXT, "ScheduleAdjustTime: " "[%8p] ser=%d ctxt=%p fncbk=%p state=%d thrd=%d\n", psi, psi->_dwSerialNumber, psi->_pContext, psi->_pfnCallback, psi->_siState, psi->_dwCallbackThreadId )); } } psd->UnlockItems(); // 5. Indicate to scheduler threads that there is one new item on the list if (NULL != psi) DBG_REQUIRE( SetEvent( psd->m_hevtNotify )); return ( (NULL != psi) ? NO_ERROR : ERROR_INVALID_PARAMETER); } // ScheduleAdjustTime() /************************************************************ * Internal functions of Scheduler ************************************************************/ VOID CSchedData::InsertIntoWorkItemList(SCHED_ITEM* psi) { SCHED_ITEM* psiList; CListEntry* ple; DBG_ASSERT( NULL != psi); DBG_ASSERT(psi->CheckSignature()); DBG_ASSERT( (psi->_siState == SI_ACTIVE) || (psi->_siState == SI_ACTIVE_PERIODIC) || (psi->_siState == SI_CALLBACK_PERIODIC ) ); // Assumed that the scheduler list is locked. DBG_ASSERT(m_lstItems.IsLocked()); // // Insert the list in order based on expires time // for ( ple = m_lstItems.First(); ple != m_lstItems.HeadNode(); ple = ple->Flink ) { psiList = CONTAINING_RECORD( ple, SCHED_ITEM, _ListEntry ); DBG_ASSERT(psiList->CheckSignature()); if ( psiList->_msecExpires > psi->_msecExpires ) { break; } } // // Insert the item psi in front of the item ple // This should work in whether the list is empty or this is the last item // on the circular list // psi->_ListEntry.Flink = ple; psi->_ListEntry.Blink = ple->Blink; ple->Blink->Flink = &psi->_ListEntry; ple->Blink = &psi->_ListEntry; return; } // InsertIntoWorkItemList() SCHED_ITEM* CSchedData::FindSchedulerItem(DWORD dwCookie) { CListEntry* ple; SCHED_ITEM* psi = NULL; // Should be called with the scheduler list locked. DBG_ASSERT(m_lstItems.IsLocked()); for ( ple = m_lstItems.First(); ple != m_lstItems.HeadNode(); ple = ple->Flink ) { psi = CONTAINING_RECORD( ple, SCHED_ITEM, _ListEntry ); DBG_ASSERT( psi->CheckSignature() ); if ( dwCookie == psi->_dwSerialNumber ) { // found the match - return return ( psi); } } // for return ( NULL); } // FindSchedulerItem() unsigned __stdcall SchedulerWorkerThread( void* pvParam ) /*++ Routine Description: ThreadProc for scheduler. Arguments: Unused. Return Value: TRUE if successful, FALSE on error (call GetLastError) --*/ { CThreadData* const ptd = (CThreadData*) pvParam; DBG_ASSERT( ptd != NULL ); CSchedData* const psd = ptd->m_psdOwner; DBG_ASSERT( psd != NULL ); IF_DEBUG(SCHED) { DBGPRINTF(( DBG_CONTEXT, "SchedulerWorkerThread (%d) starting: " "CThreadData=%p CSchedData=%p, %d\n", ptd->m_nID, ptd, psd, psd->m_nID)); } int cExecuted = 0; DWORD cmsecWait = INFINITE; __int64 TickCount; SCHED_ITEM * psi, * psiExpired; CListEntry * ple; BOOL fListLocked = FALSE; DWORD dwWait; HRESULT hr; HANDLE ahEvt[2] = {psd->m_hevtNotify, ptd->m_hevtShutdown}; hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); if (hr != S_OK && hr != S_FALSE) { DBG_ASSERT(FALSE); return FALSE; } while (!psd->m_fShutdown) { DBG_ASSERT(!fListLocked); // the list must be unlocked here while ( TRUE ) { MSG msg; // // Need to do MsgWait instead of WaitForSingleObject // to process windows msgs. We now have a window // because of COM. // dwWait = MsgWaitForMultipleObjects( 2, ahEvt, FALSE, // wait for anything cmsecWait, QS_ALLINPUT ); if (psd->m_fShutdown) goto exit; if ( (dwWait == WAIT_OBJECT_0) || // psd->m_hevtNotify (dwWait == WAIT_OBJECT_0 + 1) || // ptd->m_hevtShutdown (dwWait == WAIT_TIMEOUT) ) { break; } while ( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE )) { TranslateMessage( &msg ); DispatchMessage( &msg ); } } switch (dwWait) { default: DBGPRINTF(( DBG_CONTEXT, "[Scheduler] Error %d waiting on SchedulerEvent\n", GetLastError() )); // Fall through case WAIT_OBJECT_0: // Means a new item has been scheduled, reset the timeout, or // we are shutting down psd->LockItems(); fListLocked = TRUE; // Get the timeout value for the first item in the list if (!psd->m_lstItems.IsEmpty()) { psi = CONTAINING_RECORD( psd->m_lstItems.First(), SCHED_ITEM, _ListEntry ); DBG_ASSERT(psi->CheckSignature()); // Make sure the front item hasn't already expired TickCount = GetCurrentTimeInMilliseconds(); if (TickCount > psi->_msecExpires) { // Run scheduled items break; } // the delay is guaranteed NOT to be > 1<<32 // as per parameter to SCHED_ITEM constructor cmsecWait = (DWORD)(psi->_msecExpires - TickCount); } else { cmsecWait = INFINITE; } psd->UnlockItems(); fListLocked = FALSE; // Wait for something else (back to sleep) continue; case WAIT_TIMEOUT: // Run scheduled items break; } // Run scheduled items while (!psd->m_fShutdown) { // Lock the list if needed if (!fListLocked) { psd->LockItems(); fListLocked = TRUE; } // No timeout by default (if no items found) cmsecWait = INFINITE; if (psd->m_lstItems.IsEmpty()) break; // Find the first expired work item TickCount = GetCurrentTimeInMilliseconds(); psiExpired = NULL; for ( ple = psd->m_lstItems.First(); ple != psd->m_lstItems.HeadNode(); ple = ple->Flink ) { psi = CONTAINING_RECORD(ple, SCHED_ITEM, _ListEntry); DBG_ASSERT(psi->CheckSignature()); if ( ((psi->_siState == SI_ACTIVE) || (psi->_siState == SI_ACTIVE_PERIODIC)) ) { if (TickCount > psi->_msecExpires) { // Found Expired Item psiExpired = psi; } else { // Since they are in sorted order, once we hit one // that's not expired, we don't need to look further cmsecWait = (DWORD)(psi->_msecExpires - TickCount); } break; } } // If no expired work items found, go back to sleep if (psiExpired == NULL) { break; } // Take care of the found expired work item SCHED_ITEM_STATE st = sg_rgSchedNextState[SI_OP_CALLBACK][psiExpired->_siState]; psiExpired->_siState = st; psiExpired->_dwCallbackThreadId = GetCurrentThreadId(); DBG_ASSERT(st == SI_TO_BE_DELETED || st == SI_CALLBACK_PERIODIC); // Unlock the list while in the callback DBG_ASSERT(fListLocked); psd->UnlockItems(); fListLocked = FALSE; // While in PERIODIC callback the list is kept unlocked // leaving the object exposed IF_DEBUG(SCHED) { DBGPRINTF((DBG_CONTEXT, "SchedulerWorkerThread (%d): starting %scall: " "ser=%d ctxt=%p fncbk=%p state=%d\n", ptd->m_nID, (st == SI_CALLBACK_PERIODIC) ? "periodic " : "", psiExpired->_dwSerialNumber, psiExpired->_pContext, psiExpired->_pfnCallback, psiExpired->_siState)); } // Is this still a valid function? There have been problems // with scheduler clients (such as isatq.dll) getting unloaded, // without first calling RemoveWorkItem to clean up. if (IsBadCodePtr(reinterpret_cast(psiExpired->_pfnCallback))) { DWORD dwErr = GetLastError(); IF_DEBUG(SCHED) { DBGPRINTF((DBG_CONTEXT, "SchedulerWorkerThread (%d): " "invalid callback function %p, error %d\n", ptd->m_nID, psiExpired->_pfnCallback, dwErr)); } psiExpired->_siState = SI_TO_BE_DELETED; goto relock; } __try { psiExpired->_pfnCallback(psiExpired->_pContext); } __except (EXCEPTION_EXECUTE_HANDLER) { DWORD dwErr = GetExceptionCode(); IF_DEBUG(SCHED) { DBGPRINTF((DBG_CONTEXT, "SchedulerWorkerThread (%d): " "exception %d in callback function %p\n", ptd->m_nID, dwErr, psiExpired->_pfnCallback)); } psiExpired->_siState = SI_TO_BE_DELETED; } ++cExecuted; IF_DEBUG(SCHED) { DBGPRINTF((DBG_CONTEXT, "SchedulerWorkerThread (%d): finished %scall: " "ser=%d ctxt=%p fncbk=%p state=%d\n", ptd->m_nID, (st == SI_CALLBACK_PERIODIC) ? "periodic " : "", psiExpired->_dwSerialNumber, psiExpired->_pContext, psiExpired->_pfnCallback, psiExpired->_siState)); } relock: // Relock the list DBG_ASSERT(!fListLocked); psd->LockItems(); fListLocked = TRUE; psiExpired->_dwCallbackThreadId = 0; // While in the callback the state can change if (psiExpired->_siState == SI_TO_BE_DELETED) { // User requested delete // Remove this item from the list CDoubleList::RemoveEntry( &psiExpired->_ListEntry ); psiExpired->_ListEntry.Flink = NULL; // While in callback RemoveWorkItem() could have attached // an event to notify itself when callback is done if (psiExpired->_hCallbackEvent) { // Signal the event after item is gone from the list SetEvent(psiExpired->_hCallbackEvent); // RemoveWorkItem() will remove the item } else { // Get rid of the item psd->DeleteSchedItem(psiExpired); } } else { // no events attached DBG_ASSERT(psiExpired->_hCallbackEvent == NULL); // must still remain SI_CALLBACK_PERIODIC unless deleted DBG_ASSERT(psiExpired->_siState == SI_CALLBACK_PERIODIC); // NYI: For now remove from the list and reinsert it CDoubleList::RemoveEntry( &psiExpired->_ListEntry ); psiExpired->_ListEntry.Flink = NULL; // recalc the expiry time and reinsert into the list psiExpired->_siState = sg_rgSchedNextState[SI_OP_ADD_PERIODIC] [psiExpired->_siState]; psiExpired->CalcExpiresTime(); psd->InsertIntoWorkItemList(psiExpired); } // Start looking in the list from the beginning in case // new items have been added or other threads removed them } // while if (fListLocked) { psd->UnlockItems(); fListLocked = FALSE; } } // while exit: CoUninitialize(); // Destroy the thread object ptd->Release(); return cExecuted; } // SchedulerWorkerThread()